← Back to team overview

openlp-core team mailing list archive

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

 

You have been requested to review the proposed merge of lp:~arjan-i/openlp/images_groups into lp:openlp.

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

OK, the images_groups branch is now feature-complete. As far as I can see there are no obvious bugs, everything works as it should.

-- 
https://code.launchpad.net/~arjan-i/openlp/images_groups/+merge/145368
Your team OpenLP Core is requested to review the proposed merge of lp:~arjan-i/openlp/images_groups into lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2013-01-24 20:08:52 +0000
+++ openlp/core/lib/__init__.py	2013-01-29 13:30:27 +0000
@@ -391,7 +391,7 @@
 from eventreceiver import Receiver
 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 settingsmanager import SettingsManager

=== removed file 'openlp/core/lib/listwidgetwithdnd.py'
--- openlp/core/lib/listwidgetwithdnd.py	2012-12-29 20:56:56 +0000
+++ openlp/core/lib/listwidgetwithdnd.py	1970-01-01 00:00:00 +0000
@@ -1,111 +0,0 @@
-# -*- 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 QListWidget to handle drag and drop functionality
-"""
-import os
-
-from PyQt4 import QtCore, QtGui
-
-from openlp.core.lib import Receiver
-
-class ListWidgetWithDnD(QtGui.QListWidget):
-    """
-    Provide a list widget to store objects and handle drag and drop events
-    """
-    def __init__(self, parent=None, name=u''):
-        """
-        Initialise the list widget
-        """
-        QtGui.QListWidget.__init__(self, parent)
-        self.mimeDataText = name
-        assert(self.mimeDataText)
-
-    def activateDnD(self):
-        """
-        Activate DnD of widget
-        """
-        self.setAcceptDrops(True)
-        self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
-        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'%s_dnd' % self.mimeDataText),
-            self.parent().loadFile)
-
-    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()
-        else:
-            event.ignore()
-
-    def dragMoveEvent(self, event):
-        if event.mimeData().hasUrls():
-            event.setDropAction(QtCore.Qt.CopyAction)
-            event.accept()
-        else:
-            event.ignore()
-
-    def dropEvent(self, event):
-        """
-        Receive drop event check if it is a file 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 in listing:
-                        files.append(os.path.join(localFile, file))
-            Receiver.send_message(u'%s_dnd' % self.mimeDataText, files)
-        else:
-            event.ignore()

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2013-01-23 21:05:25 +0000
+++ openlp/core/lib/mediamanageritem.py	2013-01-29 13:30:27 +0000
@@ -36,7 +36,7 @@
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import OpenLPToolbar, ServiceItem, StringContent, build_icon, translate, Receiver, \
-    ListWidgetWithDnD, ServiceItemContext, Settings, Registry, UiStrings
+    TreeWidgetWithDnD, ServiceItemContext, Settings, Registry, UiStrings
 from openlp.core.lib.searchedit import SearchEdit
 from openlp.core.lib.ui import create_widget_action, critical_error_message_box
 
@@ -213,8 +213,7 @@
         Creates the main widget for listing items the media item is tracking
         """
         # Add the List widget
-        self.listView = ListWidgetWithDnD(self, self.plugin.name)
-        self.listView.setSpacing(1)
+        self.listView = TreeWidgetWithDnD(self, self.plugin.name)
         self.listView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
         self.listView.setAlternatingRowColors(True)
         self.listView.setObjectName(u'%sListView' % self.plugin.name)
@@ -336,7 +335,7 @@
             self.validateAndLoad(files)
         Receiver.send_message(u'cursor_normal')
 
-    def loadFile(self, files):
+    def loadFile(self, data):
         """
         Turn file from Drag and Drop into an array so the Validate code can run it.
 
@@ -345,7 +344,7 @@
         """
         new_files = []
         error_shown = False
-        for file in files:
+        for file in data['files']:
             type = file.split(u'.')[-1]
             if type.lower() not in self.onNewFileMasks:
                 if not error_shown:
@@ -355,9 +354,15 @@
             else:
                 new_files.append(file)
         if new_files:
-            self.validateAndLoad(new_files)
-
-    def validateAndLoad(self, files):
+            self.validateAndLoad(new_files, data['target'])
+
+    def dndMoveInternal(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
@@ -367,9 +372,9 @@
         """
         names = []
         full_list = []
-        for count in range(self.listView.count()):
-            names.append(self.listView.item(count).text())
-            full_list.append(self.listView.item(count).data(QtCore.Qt.UserRole))
+        for count in range(self.listView.topLevelItemCount()):
+            names.append(self.listView.topLevelItem(count).text(0))
+            full_list.append(self.listView.topLevelItem(count).data(0, QtCore.Qt.UserRole))
         duplicates_found = False
         files_added = False
         for file in files:
@@ -380,11 +385,11 @@
                 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())
         if duplicates_found:
             critical_error_message_box(UiStrings().Duplicate,
                 translate('OpenLP.MediaManagerItem', 'Duplicate files were found on import and were ignored.'))
@@ -404,14 +409,14 @@
         """
         count = 0
         file_list = []
-        while count < self.listView.count():
-            bitem = self.listView.item(count)
-            filename = bitem.data(QtCore.Qt.UserRole)
+        while count < self.listView.topLevelItemCount():
+            bitem = self.listView.topLevelItem(count)
+            filename = bitem.data(0, QtCore.Qt.UserRole)
             file_list.append(filename)
             count += 1
         return file_list
 
-    def loadList(self, list):
+    def loadList(self, list, target_group):
         raise NotImplementedError(u'MediaManagerItem.loadList needs to be defined by the plugin')
 
     def onNewClick(self):
@@ -499,8 +504,8 @@
             self.live_controller.addServiceItem(serviceItem)
 
     def createItemFromId(self, item_id):
-        item = QtGui.QListWidgetItem()
-        item.setData(QtCore.Qt.UserRole, item_id)
+        item = QtGui.QTreeWidgetItem()
+        item.setData(0, QtCore.Qt.UserRole, item_id)
         return item
 
     def onAddClick(self):
@@ -571,15 +576,16 @@
         """
         Checks if the listView is empty and adds a "No Search Results" item.
         """
-        if self.listView.count():
+        if self.listView.topLevelItemCount():
             return
         message = translate('OpenLP.MediaManagerItem', 'No Search Results')
-        item = QtGui.QListWidgetItem(message)
+        item = QtGui.QTreeWidgetItem(message)
+        item.setText(0, message)
         item.setFlags(QtCore.Qt.NoItemFlags)
         font = QtGui.QFont()
         font.setItalic(True)
-        item.setFont(font)
-        self.listView.addItem(item)
+        item.setFont(0, font)
+        self.listView.addTopLevelItem(item)
 
     def _getIdOfItemToGenerate(self, item, remoteItem):
         """
@@ -596,7 +602,7 @@
                 item = self.listView.currentItem()
                 if item is None:
                     return False
-                item_id = item.data(QtCore.Qt.UserRole)
+                item_id = item.data(0, QtCore.Qt.UserRole)
             else:
                 item_id = remoteItem
         else:
@@ -611,7 +617,7 @@
         if self.autoSelectId == -1:
             item = self.listView.currentItem()
             if item:
-                self.autoSelectId = item.data(QtCore.Qt.UserRole)
+                self.autoSelectId = item.data(0, QtCore.Qt.UserRole)
 
     def search(self, string, showError=True):
         """
@@ -687,4 +693,4 @@
             self._service_manager = Registry().get(u'service_manager')
         return self._service_manager
 
-    service_manager = property(_get_service_manager)
\ No newline at end of file
+    service_manager = property(_get_service_manager)

=== 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-01-29 13:30:27 +0000
@@ -0,0 +1,129 @@
+# -*- 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 Receiver
+
+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.allowInternalDnD = False
+        self.header().close()
+        self.defaultIndentation = self.indentation()
+        self.setIndentation(0)
+        assert(self.mimeDataText)
+
+    def activateDnD(self):
+        """
+        Activate DnD of widget
+        """
+        self.setAcceptDrops(True)
+        self.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
+        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'%s_dnd' % self.mimeDataText),
+            self.parent().loadFile)
+        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'%s_dnd_internal' % self.mimeDataText),
+            self.parent().dndMoveInternal)
+
+    def doInternalDnD(self, accept):
+        self.allowInternalDnD = accept
+
+    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.allowInternalDnD:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        elif self.allowInternalDnD:
+            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 in listing:
+                        files.append(os.path.join(localFile, file))
+            Receiver.send_message(u'%s_dnd' % self.mimeDataText, {'files':files, 'target':self.itemAt(event.pos())})
+        elif self.allowInternalDnD:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            Receiver.send_message(u'%s_dnd_internal' % self.mimeDataText, self.itemAt(event.pos()))
+        else:
+            event.ignore()

=== modified file 'openlp/core/lib/uistrings.py'
--- openlp/core/lib/uistrings.py	2013-01-16 20:23:02 +0000
+++ openlp/core/lib/uistrings.py	2013-01-29 13:30:27 +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-01-29 13:30:27 +0000
@@ -54736,6 +54736,56 @@
 \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\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 +71285,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 +71420,11 @@
 \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\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 +71892,150 @@
 "
 
 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\x8a\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x86\
+\x00\x00\x00\xb4\x00\x02\x00\x00\x00\x08\x00\x00\x00\x7e\
+\x00\x00\x00\xd6\x00\x02\x00\x00\x00\x14\x00\x00\x00\x6a\
+\x00\x00\x00\x2c\x00\x02\x00\x00\x00\x02\x00\x00\x00\x68\
+\x00\x00\x00\x14\x00\x02\x00\x00\x00\x02\x00\x00\x00\x66\
+\x00\x00\x01\x06\x00\x02\x00\x00\x00\x05\x00\x00\x00\x61\
+\x00\x00\x00\xf4\x00\x02\x00\x00\x00\x01\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\x56\x00\x00\x00\x00\x00\x01\x00\x10\xda\x21\
+\x00\x00\x10\xc6\x00\x00\x00\x00\x00\x01\x00\x10\xe1\x6c\
+\x00\x00\x10\x06\x00\x00\x00\x00\x00\x01\x00\x10\xd4\xc5\
+\x00\x00\x11\xa4\x00\x00\x00\x00\x00\x01\x00\x10\xf3\xea\
+\x00\x00\x11\x26\x00\x00\x00\x00\x00\x01\x00\x10\xe9\x82\
+\x00\x00\x0f\xae\x00\x00\x00\x00\x00\x01\x00\x10\xcd\x8e\
+\x00\x00\x11\xf6\x00\x00\x00\x00\x00\x01\x00\x10\xf9\xf1\
+\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x10\xe4\xe0\
+\x00\x00\x12\x44\x00\x00\x00\x00\x00\x01\x00\x10\xff\x1d\
+\x00\x00\x0f\xd8\x00\x00\x00\x00\x00\x01\x00\x10\xd0\x2c\
+\x00\x00\x11\xd0\x00\x00\x00\x00\x00\x01\x00\x10\xf7\x1d\
+\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x10\xf1\x72\
+\x00\x00\x10\x2c\x00\x00\x00\x00\x00\x01\x00\x10\xd7\x9f\
+\x00\x00\x12\x20\x00\x00\x00\x00\x00\x01\x00\x10\xfc\xd6\
+\x00\x00\x11\x4e\x00\x00\x00\x00\x00\x01\x00\x10\xec\xc7\
+\x00\x00\x10\xa2\x00\x00\x00\x00\x00\x01\x00\x10\xde\x6f\
+\x00\x00\x10\x7c\x00\x00\x00\x00\x00\x01\x00\x10\xdc\x58\
+\x00\x00\x03\x78\x00\x00\x00\x00\x00\x01\x00\x00\x21\x92\
+\x00\x00\x0c\x6e\x00\x00\x00\x00\x00\x01\x00\x0f\x4f\x06\
+\x00\x00\x0c\xee\x00\x00\x00\x00\x00\x01\x00\x0f\x55\xcc\
+\x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x0f\x48\x02\
+\x00\x00\x0c\x96\x00\x00\x00\x00\x00\x01\x00\x0f\x51\xa6\
+\x00\x00\x0d\x18\x00\x00\x00\x00\x00\x01\x00\x0f\x58\x97\
+\x00\x00\x0c\x3c\x00\x00\x00\x00\x00\x01\x00\x0f\x4d\x1c\
+\x00\x00\x0c\x16\x00\x00\x00\x00\x00\x01\x00\x0f\x4a\xa9\
+\x00\x00\x0c\xcc\x00\x00\x00\x00\x00\x01\x00\x0f\x53\x28\
+\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\x0e\xfe\x00\x00\x00\x00\x00\x01\x00\x0f\x73\xe5\
+\x00\x00\x0f\x28\x00\x00\x00\x00\x00\x01\x00\x0f\x79\x4f\
+\x00\x00\x0f\x58\x00\x00\x00\x00\x00\x01\x00\x0f\xe7\xd8\
+\x00\x00\x0f\x78\x00\x00\x00\x00\x00\x01\x00\x0f\xee\x89\
+\x00\x00\x12\xc6\x00\x00\x00\x00\x00\x01\x00\x11\x06\x31\
+\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x11\x01\x30\
+\x00\x00\x14\xe2\x00\x00\x00\x00\x00\x01\x00\x11\x30\x36\
+\x00\x00\x13\x6c\x00\x00\x00\x00\x00\x01\x00\x11\x0e\x3b\
+\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x11\x09\x84\
+\x00\x00\x12\x92\x00\x00\x00\x00\x00\x01\x00\x11\x04\x61\
+\x00\x00\x14\x34\x00\x00\x00\x00\x00\x01\x00\x11\x21\x11\
+\x00\x00\x13\xd2\x00\x00\x00\x00\x00\x01\x00\x11\x18\xe1\
+\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x11\x33\x18\
+\x00\x00\x14\xae\x00\x00\x00\x00\x00\x01\x00\x11\x2c\x59\
+\x00\x00\x13\xfe\x00\x00\x00\x00\x00\x01\x00\x11\x1d\xed\
+\x00\x00\x15\x3a\x00\x00\x00\x00\x00\x01\x00\x11\x36\xd5\
+\x00\x00\x14\x56\x00\x00\x00\x00\x00\x01\x00\x11\x25\x80\
+\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x11\x16\x6b\
+\x00\x00\x14\x84\x00\x00\x00\x00\x00\x01\x00\x11\x29\xd1\
+\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x11\x0b\x8b\
+\x00\x00\x0a\x4e\x00\x00\x00\x00\x00\x01\x00\x0e\xef\xf5\
+\x00\x00\x0a\x6e\x00\x00\x00\x00\x00\x01\x00\x0e\xf3\xd2\
+\x00\x00\x0a\x28\x00\x00\x00\x00\x00\x01\x00\x0e\xed\x57\
+\x00\x00\x0d\xdc\x00\x00\x00\x00\x00\x01\x00\x0f\x62\xb3\
+\x00\x00\x0e\x7a\x00\x00\x00\x00\x00\x01\x00\x0f\x6d\x28\
+\x00\x00\x0e\xd4\x00\x00\x00\x00\x00\x01\x00\x0f\x71\xe1\
+\x00\x00\x0d\x72\x00\x00\x00\x00\x00\x01\x00\x0f\x5e\xc3\
+\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x0f\x69\xf5\
+\x00\x00\x0d\xa6\x00\x00\x00\x00\x00\x01\x00\x0f\x60\xba\
+\x00\x00\x0e\x10\x00\x00\x00\x00\x00\x01\x00\x0f\x64\xb1\
+\x00\x00\x0e\x32\x00\x00\x00\x00\x00\x01\x00\x0f\x66\xc4\
+\x00\x00\x0e\xb0\x00\x00\x00\x00\x00\x01\x00\x0f\x6f\x10\
+\x00\x00\x0d\x4a\x00\x00\x00\x00\x00\x01\x00\x0f\x5c\x07\
+\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\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\xa2\x00\x00\x00\x00\x00\x01\x00\x11\x40\x09\
+\x00\x00\x15\xd6\x00\x00\x00\x00\x00\x01\x00\x11\x42\xd9\
+\x00\x00\x15\x5e\x00\x00\x00\x00\x00\x01\x00\x11\x3a\x88\
+\x00\x00\x15\x82\x00\x00\x00\x00\x00\x01\x00\x11\x3d\x86\
+\x00\x00\x07\x1c\x00\x00\x00\x00\x00\x01\x00\x0e\xad\x8c\
+\x00\x00\x08\x02\x00\x00\x00\x00\x00\x01\x00\x0e\xbe\x80\
+\x00\x00\x0a\x08\x00\x00\x00\x00\x00\x01\x00\x0e\xe9\x64\
+\x00\x00\x09\xd6\x00\x00\x00\x00\x00\x01\x00\x0e\xe2\x4e\
+\x00\x00\x08\xbc\x00\x00\x00\x00\x00\x01\x00\x0e\xc7\x4e\
+\x00\x00\x09\x5c\x00\x00\x00\x00\x00\x01\x00\x0e\xda\x0d\
+\x00\x00\x09\x86\x00\x00\x00\x00\x00\x01\x00\x0e\xdc\x42\
+\x00\x00\x08\x5c\x00\x00\x00\x00\x00\x01\x00\x0e\xc3\x31\
+\x00\x00\x08\x8a\x00\x00\x00\x00\x00\x01\x00\x0e\xc5\xb1\
+\x00\x00\x07\x90\x00\x00\x00\x00\x00\x01\x00\x0e\xb7\x33\
+\x00\x00\x07\xe2\x00\x00\x00\x00\x00\x01\x00\x0e\xbb\x74\
+\x00\x00\x08\x2a\x00\x00\x00\x00\x00\x01\x00\x0e\xc1\xde\
+\x00\x00\x09\x0e\x00\x00\x00\x00\x00\x01\x00\x0e\xcd\xc3\
+\x00\x00\x08\xe2\x00\x00\x00\x00\x00\x01\x00\x0e\xcb\x60\
+\x00\x00\x09\xa8\x00\x00\x00\x00\x00\x01\x00\x0e\xe0\x46\
+\x00\x00\x06\xee\x00\x00\x00\x00\x00\x01\x00\x0e\xa6\xce\
+\x00\x00\x07\x40\x00\x00\x00\x00\x00\x01\x00\x0e\xb0\x78\
+\x00\x00\x07\x6c\x00\x00\x00\x00\x00\x01\x00\x0e\xb3\xba\
+\x00\x00\x07\xc0\x00\x00\x00\x00\x00\x01\x00\x0e\xb8\x2c\
+\x00\x00\x09\x2c\x00\x00\x00\x00\x00\x01\x00\x0e\xd0\x65\
+\x00\x00\x0b\x02\x00\x00\x00\x00\x00\x01\x00\x0f\x2d\x0a\
+\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x0f\x30\x0d\
+\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x0f\x3e\x45\
+\x00\x00\x0a\xd2\x00\x00\x00\x00\x00\x01\x00\x0f\x2a\xb1\
+\x00\x00\x0b\x7c\x00\x00\x00\x00\x00\x01\x00\x0f\x3b\x12\
+\x00\x00\x0b\xce\x00\x00\x00\x00\x00\x01\x00\x0f\x41\x1b\
+\x00\x00\x0a\x90\x00\x00\x00\x00\x00\x01\x00\x0e\xf7\x0c\
+\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x0f\x33\xe0\
+\x00\x00\x16\x06\x00\x00\x00\x00\x00\x01\x00\x11\x45\xd2\
+\x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x11\x4c\x48\
+\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x11\x49\x34\
+\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x11\x4e\xe6\
+\x00\x00\x06\x56\x00\x00\x00\x00\x00\x01\x00\x0d\x94\xad\
+\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x0d\x65\x6d\
+\x00\x00\x05\xc6\x00\x00\x00\x00\x00\x01\x00\x0d\x5a\xd9\
+\x00\x00\x06\x26\x00\x00\x00\x00\x00\x01\x00\x0d\x7a\xa2\
+\x00\x00\x06\x86\x00\x00\x00\x00\x00\x01\x00\x0d\x98\xca\
+\x00\x00\x06\xba\x00\x00\x00\x00\x00\x01\x00\x0e\x61\x63\
 "
 
 def qInitResources():

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py'
--- openlp/plugins/bibles/lib/mediaitem.py	2013-01-23 20:29:43 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -76,7 +76,7 @@
         """
         Check if the first item is a second bible item or not.
         """
-        bitem = self.listView.item(0)
+        bitem = self.listView.topLevelItem(0)
         if not bitem.flags() & QtCore.Qt.ItemIsSelectable:
             # The item is the "No Search Results" item.
             self.listView.clear()
@@ -95,7 +95,7 @@
             self.displayResults(bible, second_bible)
 
     def _decodeQtObject(self, bitem, key):
-        reference = bitem.data(QtCore.Qt.UserRole)
+        reference = bitem.data(0, QtCore.Qt.UserRole)
         obj = reference[unicode(key)]
         return unicode(obj).strip()
 
@@ -634,7 +634,7 @@
             self.second_search_results = self.plugin.manager.get_verses(second_bible, versetext, book_ref_id)
         if not self.advancedLockButton.isChecked():
             self.listView.clear()
-        if self.listView.count() != 0:
+        if self.listView.topLevelItemCount() != 0:
             self.__checkSecondBible(bible, second_bible)
         elif self.search_results:
             self.displayResults(bible, second_bible)
@@ -691,7 +691,7 @@
                 self.second_search_results = bibles[second_bible].get_verses(text)
         if not self.quickLockButton.isChecked():
             self.listView.clear()
-        if self.listView.count() != 0 and self.search_results:
+        if self.listView.topLevelItemCount() != 0 and self.search_results:
             self.__checkSecondBible(bible, second_bible)
         elif self.search_results:
             self.displayResults(bible, second_bible)
@@ -707,7 +707,7 @@
         """
         items = self.buildDisplayResults(bible, second_bible, self.search_results)
         for bible_verse in items:
-            self.listView.addItem(bible_verse)
+            self.listView.addTopLevelItem(bible_verse)
         self.listView.selectAll()
         self.search_results = {}
         self.second_search_results = {}
@@ -766,8 +766,9 @@
                     second_version)
             else:
                 bible_text = u'%s %d%s%d (%s)' % (book, verse.chapter, verse_separator, verse.verse, version)
-            bible_verse = QtGui.QListWidgetItem(bible_text)
-            bible_verse.setData(QtCore.Qt.UserRole, data)
+            bible_verse = QtGui.QTreeWidgetItem(bible_text)
+            bible_verse.setText(0, bible_text)
+            bible_verse.setData(0, QtCore.Qt.UserRole, data)
             items.append(bible_verse)
         return items
 
@@ -971,7 +972,7 @@
         return []
 
     def createItemFromId(self, item_id):
-        item = QtGui.QListWidgetItem()
+        item = QtGui.QTreeWidgetItem()
         bible = self.quickVersionComboBox.currentText()
         search_results = self.plugin.manager.get_verses(bible, item_id, False)
         items = self.buildDisplayResults(bible, u'', search_results)

=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
--- openlp/plugins/custom/lib/mediaitem.py	2013-01-24 20:08:52 +0000
+++ openlp/plugins/custom/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -99,15 +99,16 @@
         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()
         custom_slides.sort()
         for custom_slide in custom_slides:
-            custom_name = QtGui.QListWidgetItem(custom_slide.title)
-            custom_name.setData(QtCore.Qt.UserRole, custom_slide.id)
-            self.listView.addItem(custom_name)
+            custom_name = QtGui.QTreeWidgetItem(custom_slide.title)
+            custom_name.setText(0, custom_slide.title)
+            custom_name.setData(0, QtCore.Qt.UserRole, custom_slide.id)
+            self.listView.addTopLevelItem(custom_name)
             # Auto-select the custom.
             if custom_slide.id == self.autoSelectId:
                 self.listView.setCurrentItem(custom_name)
@@ -154,7 +155,7 @@
         """
         if check_item_selected(self.listView, UiStrings().SelectEdit):
             item = self.listView.currentItem()
-            item_id = item.data(QtCore.Qt.UserRole)
+            item_id = item.data(0, QtCore.Qt.UserRole)
             self.edit_custom_form.loadCustom(item_id, False)
             self.edit_custom_form.exec_()
             self.autoSelectId = -1

=== 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-01-29 13:30:27 +0000
@@ -0,0 +1,58 @@
+# -*- 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-01-29 13:30:27 +0000
@@ -0,0 +1,63 @@
+# -*- 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, addGroupDialog):
+        addGroupDialog.setObjectName(u'addGroupDialog')
+        addGroupDialog.resize(300, 10)
+        self.dialogLayout = QtGui.QVBoxLayout(addGroupDialog)
+        self.dialogLayout.setObjectName(u'dialogLayout')
+        self.nameLayout = QtGui.QFormLayout()
+        self.nameLayout.setObjectName(u'nameLayout')
+        self.parentGroupLabel = QtGui.QLabel(addGroupDialog)
+        self.parentGroupLabel.setObjectName(u'parentGroupLabel')
+        self.parentGroupComboBox = QtGui.QComboBox(addGroupDialog)
+        self.parentGroupComboBox.setObjectName(u'parentGroupComboBox')
+        self.nameLayout.addRow(self.parentGroupLabel, self.parentGroupComboBox)
+        self.nameLabel = QtGui.QLabel(addGroupDialog)
+        self.nameLabel.setObjectName(u'nameLabel')
+        self.nameEdit = QtGui.QLineEdit(addGroupDialog)
+        self.nameEdit.setObjectName(u'nameEdit')
+        self.nameLabel.setBuddy(self.nameEdit)
+        self.nameLayout.addRow(self.nameLabel, self.nameEdit)
+        self.dialogLayout.addLayout(self.nameLayout)
+        self.buttonBox = create_button_box(addGroupDialog, u'buttonBox', [u'cancel', u'save'])
+        self.dialogLayout.addWidget(self.buttonBox)
+        self.retranslateUi(addGroupDialog)
+        addGroupDialog.setMaximumHeight(addGroupDialog.sizeHint().height())
+
+    def retranslateUi(self, addGroupDialog):
+        addGroupDialog.setWindowTitle(translate('ImagePlugin.AddGroupForm', 'Add group'))
+        self.parentGroupLabel.setText(translate('ImagePlugin.AddGroupForm', 'Parent group:'))
+        self.nameLabel.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-01-29 13:30:27 +0000
@@ -0,0 +1,60 @@
+# -*- 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):
+        if clear:
+            self.nameEdit.clear()
+        self.nameEdit.setFocus()
+        return QtGui.QDialog.exec_(self)
+
+    def accept(self):
+        if not self.nameEdit.text():
+            critical_error_message_box(message=translate('ImagePlugin.AddGroupForm',
+                'You need to type in a group name.'))
+            self.nameEdit.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-01-29 13:30:27 +0000
@@ -0,0 +1,61 @@
+# -*- 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):
+    def setupUi(self, chooseGroupDialog):
+        chooseGroupDialog.setObjectName(u'chooseGroupDialog')
+        chooseGroupDialog.resize(440, 119)
+        self.chooseGroupLayout = QtGui.QFormLayout(chooseGroupDialog)
+        self.chooseGroupLayout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow)
+        self.chooseGroupLayout.setMargin(8)
+        self.chooseGroupLayout.setSpacing(8)
+        self.chooseGroupLayout.setObjectName(u'chooseGroupLayout')
+        self.groupQuestionLabel = QtGui.QLabel(chooseGroupDialog)
+        self.groupQuestionLabel.setWordWrap(True)
+        self.groupQuestionLabel.setObjectName(u'groupQuestionLabel')
+        self.chooseGroupLayout.setWidget(1, QtGui.QFormLayout.SpanningRole, self.groupQuestionLabel)
+        self.groupComboBox = QtGui.QComboBox(chooseGroupDialog)
+        self.groupComboBox.setObjectName(u'groupComboBox')
+        self.chooseGroupLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.groupComboBox)
+        self.groupButtonBox = create_button_box(chooseGroupDialog, u'buttonBox', [u'ok'])
+        self.chooseGroupLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.groupButtonBox)
+
+        self.retranslateUi(chooseGroupDialog)
+        QtCore.QMetaObject.connectSlotsByName(chooseGroupDialog)
+
+    def retranslateUi(self, chooseGroupDialog):
+        chooseGroupDialog.setWindowTitle(translate('ImagePlugin.ChooseGroupForm', 'Choose group'))
+        self.groupQuestionLabel.setText(translate('ImagePlugin.ChooseGroupForm',
+            'To which group do you want these images to be added?'))
+

=== 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-01-29 13:30:27 +0000
@@ -0,0 +1,46 @@
+# -*- 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.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)
+

=== modified file 'openlp/plugins/images/imageplugin.py'
--- openlp/plugins/images/imageplugin.py	2013-01-23 21:05:25 +0000
+++ openlp/plugins/images/imageplugin.py	2013-01-29 13:30:27 +0000
@@ -32,11 +32,14 @@
 import logging
 
 from openlp.core.lib import Plugin, StringContent, build_icon, translate, Receiver, ImageSource, Settings
+from openlp.core.lib.db import Manager
 from openlp.plugins.images.lib import ImageMediaItem, ImageTab
+from openlp.plugins.images.lib.db import init_schema
 
 log = logging.getLogger(__name__)
 
 __default_settings__ = {
+        u'images/db type': u'sqlite',
         u'images/background color': u'#000000',
         u'images/images files': []
     }
@@ -47,6 +50,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)

=== 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-01-29 13:30:27 +0000
@@ -0,0 +1,97 @@
+# -*- 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/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2013-01-23 20:29:43 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -37,6 +37,8 @@
     UiStrings
 from openlp.core.lib.ui import 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__)
 
@@ -51,6 +53,10 @@
         MediaManagerItem.__init__(self, parent, plugin, icon)
         self.quickPreviewAllowed = True
         self.hasSearch = True
+        self.manager = plugin.manager
+        self.choosegroupform = ChooseGroupForm(self)
+        self.addgroupform = AddGroupForm(self)
+        self.fillGroupsComboBox(self.addgroupform.parentGroupComboBox)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'live_theme_changed'), self.liveThemeChanged)
         # Allow DnD from the desktop
         self.listView.activateDnD()
@@ -60,6 +66,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)
@@ -76,20 +84,51 @@
         log.debug(u'initialise')
         self.listView.clear()
         self.listView.setIconSize(QtCore.QSize(88, 50))
+        self.listView.setIndentation(self.listView.defaultIndentation)
+        self.listView.doInternalDnD(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)
+        # Import old images list
+        images_old = Settings().value(self.settingsSection +  u'/images files')
+        if len(images_old) > 0:
+            for imageFile in images_old:
+                imagefilename = ImageFilenames()
+                imagefilename.group_id = 0
+                imagefilename.filename = imageFile
+                success = self.manager.save_object(imagefilename)
+            Settings().setValue(self.settingsSection + u'/images files', [])
+            Settings().remove(self.settingsSection + u'/images files')
+            Settings().remove(self.settingsSection + u'/images count')
+        # Load images from the database
+        self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), True)
 
     def addListViewToToolBar(self):
         MediaManagerItem.addListViewToToolBar(self)
         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
@@ -97,49 +136,164 @@
         # 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)
+            item_list = self.listView.selectedItems()
             Receiver.send_message(u'cursor_busy')
-            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)))
+                        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.main_window.incrementProgressBar()
-            SettingsManager.setValue(self.settingsSection + u'/images files', self.getFileList())
             self.main_window.finishedProgressBar()
             Receiver.send_message(u'cursor_normal')
         self.listView.blockSignals(False)
 
-    def loadList(self, images, initialLoad=False):
+    def addSubGroups(self, groupList, parentGroupId):
+        """
+        Recursively add subgroups to the given parent group
+        """
+        image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parentGroupId)
+        image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
+        for image_group in image_groups:
+            group = QtGui.QTreeWidgetItem()
+            group.setText(0, image_group.group_name)
+            group.setData(0, QtCore.Qt.UserRole, image_group)
+            if parentGroupId is 0:
+                self.listView.addTopLevelItem(group)
+            else:
+                groupList[parentGroupId].addChild(group)
+            groupList[image_group.id] = group
+            self.addSubGroups(groupList, image_group.id)
+
+    def fillGroupsComboBox(self, comboBox, parentGroupId=0, prefix='', showTopLevelGroup=True):
+        """
+        Recursively add groups to the combobox in the 'Add group' dialog
+        """
+        if parentGroupId is 0:
+            comboBox.clear()
+            if showTopLevelGroup is True:
+                comboBox.addItem(translate('ImagePlugin.MediaItem', '-- Top-level group --'), 0)
+        image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parentGroupId)
+        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.fillGroupsComboBox(comboBox, image_group.id, prefix+'   ')
+
+    def loadFullList(self, images, initialLoad=False):
+        """
+        Replace the list of images and groups in the interface.
+        """
         if not initialLoad:
             Receiver.send_message(u'cursor_busy')
             self.main_window.displayProgressBar(len(images))
+        self.listView.clear()
+        # Load the list of groups and add them to the treeView
+        group_items = {}
+        self.addSubGroups(group_items, 0)
         # 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)
+                    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:
+                if 0 not in group_items:
+                    # The 'Imported' group is only displayed when there are files that were imported from the
+                    # configuration file
+                    imported_group = QtGui.QTreeWidgetItem()
+                    imported_group.setText(0, translate('ImagePlugin.MediaItem', 'Imported'))
+                    self.listView.insertTopLevelItem(0, imported_group)
+                    group_items[0] = imported_group
+            group_items[imageFile.group_id].addChild(item_name)
             if not initialLoad:
                 self.main_window.incrementProgressBar()
         if not initialLoad:
             self.main_window.finishedProgressBar()
             Receiver.send_message(u'cursor_normal')
 
+    def loadList(self, images, target_group=None, initialLoad=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:
+            # Ask which group the images should be saved in
+            self.fillGroupsComboBox(self.choosegroupform.groupComboBox, showTopLevelGroup=False)
+            if self.choosegroupform.exec_():
+                group_id = self.choosegroupform.groupComboBox.itemData(
+                    self.choosegroupform.groupComboBox.currentIndex(), QtCore.Qt.UserRole)
+            parent_group = self.manager.get_object_filtered(ImageGroups, ImageGroups.id == group_id)
+        else:
+            parent_group = target_group.data(0, QtCore.Qt.UserRole)
+            if isinstance(parent_group, ImageFilenames):
+                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
+        for filename in images:
+            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 = parent_group.id
+            imageFile.filename = unicode(filename)
+            success = self.manager.save_object(imageFile)
+        self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
+            initialLoad)
+
+    def dndMoveInternal(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 isinstance(target_group.data(0, QtCore.Qt.UserRole), ImageFilenames):
+            target_group = target.parent()
+        # Don't allow moving to the Imported group
+        if target_group.data(0, QtCore.Qt.UserRole) is None:
+            return
+        # Move images in the treeview
+        items_to_save = []
+        for item in items_to_move:
+            if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
+                item.parent().removeChild(item)
+                target_group.addChild(item)
+                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.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'))
@@ -149,7 +303,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)
@@ -158,8 +316,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)
@@ -182,11 +348,58 @@
             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
+        """
+        if self.addgroupform.exec_():
+            new_group = ImageGroups.populate(parent_id=self.addgroupform.parentGroupComboBox.itemData(
+                self.addgroupform.parentGroupComboBox.currentIndex(), QtCore.Qt.UserRole),
+                group_name=self.addgroupform.nameEdit.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.fillGroupsComboBox(self.addgroupform.parentGroupComboBox)
+                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 backgound with the image selected,
@@ -207,9 +420,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.plugin.liveController.display.directImage(filename, background):
                     self.resetAction.setVisible(True)
@@ -222,11 +434,9 @@
                         '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])
+            filename = os.path.split(unicode(file.filename))[1]
+            results.append([file.filename, filename])
         return results

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2013-01-23 20:29:43 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -147,7 +147,7 @@
         if check_item_selected(self.listView,
                 translate('MediaPlugin.MediaItem', 'You must select a media file to replace the background with.')):
             item = self.listView.currentItem()
-            filename = item.data(QtCore.Qt.UserRole)
+            filename = item.data(0, QtCore.Qt.UserRole)
             if os.path.exists(filename):
                 service_item = ServiceItem()
                 service_item.title = u'webkit'
@@ -171,7 +171,7 @@
             item = self.listView.currentItem()
             if item is None:
                 return False
-        filename = item.data(QtCore.Qt.UserRole)
+        filename = item.data(0, QtCore.Qt.UserRole)
         if not os.path.exists(filename):
             if not remote:
                 # File is no longer present
@@ -253,10 +253,10 @@
             row_list = [item.row() for item in self.listView.selectedIndexes()]
             row_list.sort(reverse=True)
             for row in row_list:
-                self.listView.takeItem(row)
+                self.listView.takeTopLevelItem(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])
@@ -264,24 +264,28 @@
             track_info = QtCore.QFileInfo(track)
             if not os.path.exists(track):
                 filename = os.path.split(unicode(track))[1]
-                item_name = QtGui.QListWidgetItem(filename)
-                item_name.setIcon(ERROR)
-                item_name.setData(QtCore.Qt.UserRole, track)
+                item_name = QtGui.QTreeWidgetItem(filename)
+                item_name.setText(0, filename)
+                item_name.setIcon(0, ERROR)
+                item_name.setData(0, QtCore.Qt.UserRole, track)
             elif track_info.isFile():
                 filename = os.path.split(unicode(track))[1]
-                item_name = QtGui.QListWidgetItem(filename)
+                item_name = QtGui.QTreeWidgetItem(filename)
+                item_name.setText(0, filename)
                 if u'*.%s' % (filename.split(u'.')[-1].lower()) in self.media_controller.audio_extensions_list:
-                    item_name.setIcon(AUDIO)
+                    item_name.setIcon(0, AUDIO)
                 else:
-                    item_name.setIcon(VIDEO)
-                item_name.setData(QtCore.Qt.UserRole, track)
+                    item_name.setIcon(0, VIDEO)
+                item_name.setData(0, QtCore.Qt.UserRole, track)
             else:
                 filename = os.path.split(unicode(track))[1]
-                item_name = QtGui.QListWidgetItem(filename)
-                item_name.setIcon(build_icon(DVDICON))
-                item_name.setData(QtCore.Qt.UserRole, track)
-            item_name.setToolTip(track)
-            self.listView.addItem(item_name)
+                item_name = QtGui.QTreeWidgetItem(filename)
+                item_name.setText(0, filename)
+                item_name.setIcon(0, build_icon(DVDICON))
+                item_name.setData(0, QtCore.Qt.UserRole, track)
+            item_name.setToolTip(0, track)
+            self.listView.addTopLevelItem(item_name)
+        Settings().setValue(self.settingsSection + u'/media files', self.getFileList())
 
     def getList(self, type=MediaType.Audio):
         media = Settings().value(self.settingsSection + u'/media files')

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2013-01-23 20:29:43 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -121,7 +121,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.populateDisplayTypes()
 
     def populateDisplayTypes(self):
@@ -142,7 +142,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
@@ -164,11 +164,12 @@
                 continue
             filename = os.path.split(unicode(file))[1]
             if not os.path.exists(file):
-                item_name = QtGui.QListWidgetItem(filename)
-                item_name.setIcon(build_icon(ERROR))
-                item_name.setData(QtCore.Qt.UserRole, file)
-                item_name.setToolTip(file)
-                self.listView.addItem(item_name)
+                item_name = QtGui.QTreeWidgetItem(filename)
+                item_name.setText(0, filename)
+                item_name.setIcon(0, build_icon(ERROR))
+                item_name.setData(0, QtCore.Qt.UserRole, file)
+                item_name.setToolTip(0, file)
+                self.listView.addTopLevelItem(item_name)
             else:
                 if titles.count(filename) > 0:
                     if not initialLoad:
@@ -201,11 +202,13 @@
                         critical_error_message_box(UiStrings().UnsupportedFile,
                             translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.'))
                         continue
-                item_name = QtGui.QListWidgetItem(filename)
-                item_name.setData(QtCore.Qt.UserRole, file)
-                item_name.setIcon(icon)
-                item_name.setToolTip(file)
-                self.listView.addItem(item_name)
+                item_name = QtGui.QTreeWidgetItem(filename)
+                item_name.setText(0, filename)
+                item_name.setData(0, QtCore.Qt.UserRole, file)
+                item_name.setIcon(0, icon)
+                item_name.setToolTip(0, file)
+                self.listView.addTopLevelItem(item_name)
+        Settings().setValue(self.settingsSection + u'/presentations files', self.getFileList())
         Receiver.send_message(u'cursor_normal')
         if not initialLoad:
             self.main_window.finishedProgressBar()
@@ -231,7 +234,7 @@
             self.main_window.finishedProgressBar()
             Receiver.send_message(u'cursor_normal')
             for row in row_list:
-                self.listView.takeItem(row)
+                self.listView.takeTopLevelItem(row)
             Settings().setValue(self.settingsSection + u'/presentations files', self.getFileList())
 
     def generateSlideData(self, service_item, item=None, xmlVersion=False,

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2013-01-24 20:08:52 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2013-01-29 13:30:27 +0000
@@ -236,9 +236,10 @@
             author_list = [author.display_name for author in song.authors]
             song_title = unicode(song.title)
             song_detail = u'%s (%s)' % (song_title, create_separated_list(author_list))
-            song_name = QtGui.QListWidgetItem(song_detail)
-            song_name.setData(QtCore.Qt.UserRole, song.id)
-            self.listView.addItem(song_name)
+            song_name = QtGui.QTreeWidgetItem(song_detail)
+            song_name.setText(0, song_detail)
+            song_name.setData(0, QtCore.Qt.UserRole, song.id)
+            self.listView.addTopLevelItem(song_name)
             # Auto-select the item if name has been set
             if song.id == self.autoSelectId:
                 self.listView.setCurrentItem(song_name)
@@ -253,9 +254,10 @@
                 if song.temporary:
                     continue
                 song_detail = u'%s (%s)' % (author.display_name, song.title)
-                song_name = QtGui.QListWidgetItem(song_detail)
-                song_name.setData(QtCore.Qt.UserRole, song.id)
-                self.listView.addItem(song_name)
+                song_name = QtGui.QTreeWidgetItem(song_detail)
+                song_name.setText(0, song_detail)
+                song_name.setData(0, QtCore.Qt.UserRole, song.id)
+                self.listView.addTopLevelItem(song_name)
 
     def displayResultsBook(self, searchresults, song_number=False):
         log.debug(u'display results Book')
@@ -270,9 +272,10 @@
                 if song_number and not song_number in song.song_number:
                     continue
                 song_detail = u'%s - %s (%s)' % (book.name, song.song_number, song.title)
-                song_name = QtGui.QListWidgetItem(song_detail)
-                song_name.setData(QtCore.Qt.UserRole, song.id)
-                self.listView.addItem(song_name)
+                song_name = QtGui.QTreeWidgetItem(song_detail)
+                song_name.setText(0, song_detail)
+                song_name.setData(0, QtCore.Qt.UserRole, song.id)
+                self.listView.addTopLevelItem(song_name)
 
     def onClearTextButtonClick(self):
         """
@@ -351,7 +354,7 @@
         log.debug(u'onEditClick')
         if check_item_selected(self.listView, UiStrings().SelectEdit):
             self.editItem = self.listView.currentItem()
-            item_id = self.editItem.data(QtCore.Qt.UserRole)
+            item_id = self.editItem.data(0, QtCore.Qt.UserRole)
             self.editSongForm.loadSong(item_id, False)
             self.editSongForm.exec_()
             self.autoSelectId = -1
@@ -400,7 +403,7 @@
         log.debug(u'onCloneClick')
         if check_item_selected(self.listView, UiStrings().SelectEdit):
             self.editItem = self.listView.currentItem()
-            item_id = self.editItem.data(QtCore.Qt.UserRole)
+            item_id = self.editItem.data(0, QtCore.Qt.UserRole)
             old_song = self.plugin.manager.get_object(Song, item_id)
             song_xml = self.openLyrics.song_to_xml(old_song)
             new_song = self.openLyrics.xml_to_song(song_xml)

=== added file 'resources/forms/imagesaddgroupdialog.ui'
--- resources/forms/imagesaddgroupdialog.ui	1970-01-01 00:00:00 +0000
+++ resources/forms/imagesaddgroupdialog.ui	2013-01-29 13:30:27 +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-01-29 13:30:27 +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_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-01-29 13:30:27 +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-01-29 13:30:27 +0000
@@ -21,6 +21,9 @@
     <file>song_topic_edit.png</file>
     <file>song_book_edit.png</file>
   </qresource>
+  <qresource prefix="images">
+    <file>image_new_group.png</file>
+  </qresource>
   <qresource prefix="bibles">
     <file>bibles_search_text.png</file>
     <file>bibles_search_reference.png</file>


References