← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~sfindlay/openlp/refactor-song-import into lp:openlp

 

Samuel Findlay has proposed merging lp:~sfindlay/openlp/refactor-song-import into lp:openlp.

Requested reviews:
  Raoul Snyman (raoul-snyman)

For more details, see:
https://code.launchpad.net/~sfindlay/openlp/refactor-song-import/+merge/108164

This is a refactor of the architecture that the song importers plug into. The significant changes are in SongImportForm (963 ->  523 lines) and songs.lib.importer (172 -> 369 lines). The import classes themselves are not affected.

Reasoning: 
While writing a couple of song importers I've noticed a lot of repetitive code in SongImportForm, due to much the same code being repeated for each importer. This not only seems to go against the Don't Repeat Yourself principle, but also makes it more time consuming to add new song importers (It can take almost as long to add all the various elements to SongImportForm as it does to write the new import class itself).

Solution:
SongImportForm is now generalised, so it should not usually need to be modified when a new importer is added. Rather, a dict of format 'attributes' is added to SongFormatAttr in songs.lib.importer for each importer.
(Also improved the validation of the wizard source page).
-- 
https://code.launchpad.net/~sfindlay/openlp/refactor-song-import/+merge/108164
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/ui.py'
--- openlp/core/lib/ui.py	2012-05-07 11:24:46 +0000
+++ openlp/core/lib/ui.py	2012-05-31 14:10:23 +0000
@@ -80,6 +80,10 @@
         self.Help = translate('OpenLP.Ui', 'Help')
         self.Hours = translate('OpenLP.Ui', 'h',
             'The abbreviated unit for hours')
+        self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected',
+            'Singular')
+        self.IFSs = translate('OpenLP.Ui', 'Invalid File Selected', 'Singular')
+        self.IFSp = translate('OpenLP.Ui', 'Invalid Files Selected', 'Plural')
         self.Image = translate('OpenLP.Ui', 'Image')
         self.Import = translate('OpenLP.Ui', 'Import')
         self.LayoutStyle = translate('OpenLP.Ui', 'Layout style:')

=== modified file 'openlp/core/ui/wizard.py'
--- openlp/core/ui/wizard.py	2012-05-07 13:38:02 +0000
+++ openlp/core/ui/wizard.py	2012-05-31 14:10:23 +0000
@@ -44,20 +44,9 @@
     # Applications/Formats we import from or export to. These get used in
     # multiple places but do not need translating unless you find evidence of
     # the writers translating their own product name.
-    CCLI = u'CCLI/SongSelect'
     CSV = u'CSV'
-    DB = u'DreamBeam'
-    EW = u'EasyWorship'
-    ES = u'EasySlides'
-    FP = u'Foilpresenter'
-    OL = u'OpenLyrics'
     OS = u'OpenSong'
     OSIS = u'OSIS'
-    PS = u'PowerSong 1.0'
-    SB = u'SongBeamer'
-    SoF = u'Songs of Fellowship'
-    SSP = u'SongShow Plus'
-    WoW = u'Words of Worship'
     # These strings should need a good reason to be retranslated elsewhere.
     FinishedImport = translate('OpenLP.Ui', 'Finished import.')
     FormatLabel = translate('OpenLP.Ui', 'Format:')
@@ -76,10 +65,12 @@
     PercentSymbolFormat = unicode(translate('OpenLP.Ui', '%p%'))
     Ready = translate('OpenLP.Ui', 'Ready.')
     StartingImport = translate('OpenLP.Ui', 'Starting import...')
-    YouSpecifyFile = unicode(translate('OpenLP.Ui', 'You need to specify at '
+    YouSpecifyFile = unicode(translate('OpenLP.Ui', 'You need to specify one '
+        '%s file to import from.', 'A file type e.g. OpenSong'))
+    YouSpecifyFiles = unicode(translate('OpenLP.Ui', 'You need to specify at '
         'least one %s file to import from.', 'A file type e.g. OpenSong'))
-    YouSpecifyFolder = unicode(translate('OpenLP.Ui', 'You need to specify a '
-        '%s folder to import from.', 'A file type e.g. OpenSong'))
+    YouSpecifyFolder = unicode(translate('OpenLP.Ui', 'You need to specify one '
+        '%s folder to import from.', 'A song format e.g. PowerSong'))
 
 
 class OpenLPWizard(QtGui.QWizard):

=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py	2012-05-20 01:42:07 +0000
+++ openlp/plugins/songs/forms/songimportform.py	2012-05-31 14:10:23 +0000
@@ -36,7 +36,8 @@
 from openlp.core.lib import Receiver, SettingsManager, translate
 from openlp.core.lib.ui import UiStrings, critical_error_message_box
 from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
-from openlp.plugins.songs.lib.importer import SongFormat
+from openlp.plugins.songs.lib.importer import SongFormat, SongFormatAttr, \
+    SongFormatSelect
 
 log = logging.getLogger(__name__)
 
@@ -65,122 +66,59 @@
         """
         Set up the song wizard UI.
         """
+        self.formatWidgets = dict([(format, {}) for format in
+            SongFormat.get_format_list()])
         OpenLPWizard.setupUi(self, image)
-        self.formatStack.setCurrentIndex(0)
+        self.currentFormat = SongFormat.OpenLyrics
+        self.formatStack.setCurrentIndex(self.currentFormat)
         QtCore.QObject.connect(self.formatComboBox,
             QtCore.SIGNAL(u'currentIndexChanged(int)'),
             self.onCurrentIndexChanged)
 
     def onCurrentIndexChanged(self, index):
         """
-        Called when the format combo box's index changed. We have to check if
-        the import is available and accordingly to disable or enable the next
-        button.
+        Called when the format combo box's index changed.
         """
+        self.currentFormat = index
         self.formatStack.setCurrentIndex(index)
-        next_button = self.button(QtGui.QWizard.NextButton)
-        next_button.setEnabled(SongFormat.get_availability(index))
+        self.sourcePage.emit(QtCore.SIGNAL(u'completeChanged()'))
 
     def customInit(self):
         """
         Song wizard specific initialisation.
         """
-        if not SongFormat.get_availability(SongFormat.OpenLP1):
-            self.openLP1DisabledWidget.setVisible(True)
-            self.openLP1ImportWidget.setVisible(False)
-        if not SongFormat.get_availability(SongFormat.SongsOfFellowship):
-            self.songsOfFellowshipDisabledWidget.setVisible(True)
-            self.songsOfFellowshipImportWidget.setVisible(False)
-        if not SongFormat.get_availability(SongFormat.Generic):
-            self.genericDisabledWidget.setVisible(True)
-            self.genericImportWidget.setVisible(False)
+        for format in SongFormat.get_format_list():
+            if not SongFormatAttr.get(format, SongFormatAttr.availability):
+                self.formatWidgets[format][u'disabledWidget'].setVisible(True)
+                self.formatWidgets[format][u'importWidget'].setVisible(False)
 
     def customSignals(self):
         """
         Song wizard specific signals.
         """
-        QtCore.QObject.connect(self.openLP2BrowseButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenLP2BrowseButtonClicked)
-        QtCore.QObject.connect(self.openLP1BrowseButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenLP1BrowseButtonClicked)
-        QtCore.QObject.connect(self.powerSongBrowseButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onPowerSongBrowseButtonClicked)
-        QtCore.QObject.connect(self.openLyricsAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenLyricsAddButtonClicked)
-        QtCore.QObject.connect(self.openLyricsRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenLyricsRemoveButtonClicked)
-        QtCore.QObject.connect(self.openSongAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenSongAddButtonClicked)
-        QtCore.QObject.connect(self.openSongRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onOpenSongRemoveButtonClicked)
-        QtCore.QObject.connect(self.wordsOfWorshipAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onWordsOfWorshipAddButtonClicked)
-        QtCore.QObject.connect(self.wordsOfWorshipRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onWordsOfWorshipRemoveButtonClicked)
-        QtCore.QObject.connect(self.ccliAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onCCLIAddButtonClicked)
-        QtCore.QObject.connect(self.ccliRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onCCLIRemoveButtonClicked)
-        QtCore.QObject.connect(self.dreamBeamAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onDreamBeamAddButtonClicked)
-        QtCore.QObject.connect(self.dreamBeamRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onDreamBeamRemoveButtonClicked)
-        QtCore.QObject.connect(self.songsOfFellowshipAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongsOfFellowshipAddButtonClicked)
-        QtCore.QObject.connect(self.songsOfFellowshipRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongsOfFellowshipRemoveButtonClicked)
-        QtCore.QObject.connect(self.genericAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onGenericAddButtonClicked)
-        QtCore.QObject.connect(self.genericRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onGenericRemoveButtonClicked)
-        QtCore.QObject.connect(self.easySlidesBrowseButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onEasySlidesBrowseButtonClicked)
-        QtCore.QObject.connect(self.ewBrowseButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onEWBrowseButtonClicked)
-        QtCore.QObject.connect(self.songBeamerAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongBeamerAddButtonClicked)
-        QtCore.QObject.connect(self.songBeamerRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongBeamerRemoveButtonClicked)
-        QtCore.QObject.connect(self.songShowPlusAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongShowPlusAddButtonClicked)
-        QtCore.QObject.connect(self.songShowPlusRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onSongShowPlusRemoveButtonClicked)
-        QtCore.QObject.connect(self.foilPresenterAddButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onFoilPresenterAddButtonClicked)
-        QtCore.QObject.connect(self.foilPresenterRemoveButton,
-            QtCore.SIGNAL(u'clicked()'),
-            self.onFoilPresenterRemoveButtonClicked)
+        for format in SongFormat.get_format_list():
+            select_mode = SongFormatAttr.get(format, SongFormatAttr.select_mode)
+            if select_mode == SongFormatSelect.MultipleFiles:
+                QtCore.QObject.connect(self.formatWidgets[format][u'addButton'],
+                    QtCore.SIGNAL(u'clicked()'), self.onAddButtonClicked)
+                QtCore.QObject.connect(
+                    self.formatWidgets[format][u'removeButton'],
+                    QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked)
+            else:
+                QtCore.QObject.connect(
+                    self.formatWidgets[format][u'browseButton'],
+                    QtCore.SIGNAL(u'clicked()'), self.onBrowseButtonClicked)
+                QtCore.QObject.connect(
+                    self.formatWidgets[format][u'filepathEdit'],
+                    QtCore.SIGNAL(u'textChanged (const QString&)'),
+                    self.onFilepathEditTextChanged)
 
     def addCustomPages(self):
         """
         Add song wizard specific pages.
         """
         # Source Page
-        self.sourcePage = QtGui.QWizardPage()
+        self.sourcePage = SongImportSourcePage()
         self.sourcePage.setObjectName(u'SourcePage')
         self.sourceLayout = QtGui.QVBoxLayout(self.sourcePage)
         self.sourceLayout.setObjectName(u'SourceLayout')
@@ -200,38 +138,9 @@
             QtGui.QSizePolicy.Expanding)
         self.formatStack = QtGui.QStackedLayout()
         self.formatStack.setObjectName(u'FormatStack')
-        # OpenLyrics
-        self.addFileSelectItem(u'openLyrics', u'OpenLyrics', True)
-        # OpenLP 2.0
-        self.addFileSelectItem(u'openLP2', single_select=True)
-        # openlp.org 1.x
-        self.addFileSelectItem(u'openLP1', None, True, True)
-        # Generic Document/Presentation import
-        self.addFileSelectItem(u'generic', None, True)
-        # CCLI File import
-        self.addFileSelectItem(u'ccli')
-        # DreamBeam
-        self.addFileSelectItem(u'dreamBeam')
-        # EasySlides
-        self.addFileSelectItem(u'easySlides', single_select=True)
-        # EasyWorship
-        self.addFileSelectItem(u'ew', single_select=True)
-        # Foilpresenter
-        self.addFileSelectItem(u'foilPresenter')
-        # Open Song
-        self.addFileSelectItem(u'openSong', u'OpenSong')
-        # PowerSong
-        self.addFileSelectItem(u'powerSong', single_select=True)
-        # SongBeamer
-        self.addFileSelectItem(u'songBeamer')
-        # Song Show Plus
-        self.addFileSelectItem(u'songShowPlus')
-        # Songs of Fellowship
-        self.addFileSelectItem(u'songsOfFellowship', None, True)
-        # Words of Worship
-        self.addFileSelectItem(u'wordsOfWorship')
-#        Commented out for future use.
-#        self.addFileSelectItem(u'csv', u'CSV', single_select=True)
+        self.disablableFormats = []
+        for self.currentFormat in SongFormat.get_format_list():
+            self.addFileSelectItem()
         self.sourceLayout.addLayout(self.formatStack)
         self.addPage(self.sourcePage)
 
@@ -245,113 +154,35 @@
             translate('OpenLP.Ui', 'Welcome to the Song Import Wizard'))
         self.informationLabel.setText(
             translate('SongsPlugin.ImportWizardForm',
-                'This wizard will help you to import songs from a variety of '
-                'formats. Click the next button below to start the process by '
-                'selecting a format to import from.'))
+            'This wizard will help you to import songs from a variety of '
+            'formats. Click the next button below to start the process by '
+            'selecting a format to import from.'))
         self.sourcePage.setTitle(WizardStrings.ImportSelect)
         self.sourcePage.setSubTitle(WizardStrings.ImportSelectLong)
         self.formatLabel.setText(WizardStrings.FormatLabel)
-        self.formatComboBox.setItemText(SongFormat.OpenLyrics,
-            translate('SongsPlugin.ImportWizardForm',
-            'OpenLyrics or OpenLP 2.0 Exported Song'))
-        self.formatComboBox.setItemText(SongFormat.OpenLP2, UiStrings().OLPV2)
-        self.formatComboBox.setItemText(SongFormat.OpenLP1, UiStrings().OLPV1)
-        self.formatComboBox.setItemText(SongFormat.Generic,
-            translate('SongsPlugin.ImportWizardForm',
-            'Generic Document/Presentation'))
-        self.formatComboBox.setItemText(SongFormat.CCLI, WizardStrings.CCLI)
-        self.formatComboBox.setItemText(
-            SongFormat.DreamBeam, WizardStrings.DB)
-        self.formatComboBox.setItemText(
-            SongFormat.EasySlides, WizardStrings.ES)
-        self.formatComboBox.setItemText(
-            SongFormat.EasyWorship, WizardStrings.EW)
-        self.formatComboBox.setItemText(
-            SongFormat.FoilPresenter, WizardStrings.FP)
-        self.formatComboBox.setItemText(SongFormat.OpenSong, WizardStrings.OS)
-        self.formatComboBox.setItemText(
-            SongFormat.PowerSong, WizardStrings.PS)
-        self.formatComboBox.setItemText(
-            SongFormat.SongBeamer, WizardStrings.SB)
-        self.formatComboBox.setItemText(
-            SongFormat.SongShowPlus, WizardStrings.SSP)
-        self.formatComboBox.setItemText(
-            SongFormat.SongsOfFellowship, WizardStrings.SoF)
-        self.formatComboBox.setItemText(
-            SongFormat.WordsOfWorship, WizardStrings.WoW)
-#        self.formatComboBox.setItemText(SongFormat.CSV, WizardStrings.CSV)
-        self.openLP2FilenameLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Filename:'))
-        self.openLP2BrowseButton.setText(UiStrings().Browse)
-        self.openLP1FilenameLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Filename:'))
-        self.openLP1BrowseButton.setText(UiStrings().Browse)
-        self.openLP1DisabledLabel.setText(WizardStrings.NoSqlite)
-        self.powerSongFilenameLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Folder:'))
-        self.powerSongBrowseButton.setText(UiStrings().Browse)
-        self.openLyricsAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.openLyricsRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.openLyricsDisabledLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'The OpenLyrics '
-            'importer has not yet been developed, but as you can see, we are '
-            'still intending to do so. Hopefully it will be in the next '
-            'release.'))
-        self.openSongAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.openSongRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.wordsOfWorshipAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.wordsOfWorshipRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.ccliAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.ccliRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.dreamBeamAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.dreamBeamRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.songsOfFellowshipAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.songsOfFellowshipRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.songsOfFellowshipDisabledLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'The Songs of '
-            'Fellowship importer has been disabled because OpenLP cannot '
-            'access OpenOffice or LibreOffice.'))
-        self.genericAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.genericRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.genericDisabledLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'The generic document/'
-            'presentation importer has been disabled because OpenLP cannot '
-            'access OpenOffice or LibreOffice.'))
-        self.easySlidesFilenameLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Filename:'))
-        self.easySlidesBrowseButton.setText(UiStrings().Browse)
-        self.ewFilenameLabel.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Filename:'))
-        self.ewBrowseButton.setText(UiStrings().Browse)
-        self.songBeamerAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.songBeamerRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.songShowPlusAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.songShowPlusRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-        self.foilPresenterAddButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        self.foilPresenterRemoveButton.setText(
-            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
-#        self.csvFilenameLabel.setText(
-#            translate('SongsPlugin.ImportWizardForm', 'Filename:'))
-#        self.csvBrowseButton.setText(UiStrings().Browse)
+        for format in SongFormat.get_format_list():
+            format_name, custom_combo_text, select_mode = SongFormatAttr.get(
+                format, SongFormatAttr.name, SongFormatAttr.combo_box_text,
+                SongFormatAttr.select_mode)
+            combo_box_text = custom_combo_text if custom_combo_text \
+                else format_name
+            self.formatComboBox.setItemText(format, combo_box_text)
+            if select_mode == SongFormatSelect.MultipleFiles:
+                self.formatWidgets[format][u'addButton'].setText(
+                    translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
+                self.formatWidgets[format][u'removeButton'].setText(
+                    translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
+            else:
+                self.formatWidgets[format][u'browseButton'].setText(
+                    UiStrings().Browse)
+                f_label = 'Filename:'
+                if select_mode == SongFormatSelect.SingleFolder:
+                    f_label = 'Folder:'
+                self.formatWidgets[format][u'filepathLabel'].setText(
+                        translate('SongsPlugin.ImportWizardForm', f_label))
+        for format in self.disablableFormats:
+            self.formatWidgets[format][u'disabledLabel'].setText(
+                SongFormatAttr.get(format, SongFormatAttr.disabled_label_text))
         self.progressPage.setTitle(WizardStrings.Importing)
         self.progressPage.setSubTitle(
             translate('SongsPlugin.ImportWizardForm',
@@ -363,10 +194,19 @@
         self.errorSaveToButton.setText(translate('SongsPlugin.ImportWizardForm',
             'Save to File'))
         # Align all QFormLayouts towards each other.
-        width = max(self.formatLabel.minimumSizeHint().width(),
-            self.openLP2FilenameLabel.minimumSizeHint().width())
-        self.formatSpacer.changeSize(width, 0, QtGui.QSizePolicy.Fixed,
-            QtGui.QSizePolicy.Fixed)
+        formats = filter(lambda f: u'filepathLabel' in
+            self.formatWidgets[f], SongFormat.get_format_list())
+        labels = [self.formatWidgets[f][u'filepathLabel'] for f in formats]
+        # Get max width of all labels
+        max_label_width = max(self.formatLabel.minimumSizeHint().width(),
+            max([label.minimumSizeHint().width() for label in labels]))
+        self.formatSpacer.changeSize(max_label_width, 0,
+            QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+        spacers = [self.formatWidgets[f][u'filepathSpacer'] for f in formats]
+        for index, spacer in enumerate(spacers):
+            spacer.changeSize(
+                max_label_width - labels[index].minimumSizeHint().width(), 0,
+                QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
 
     def customPageChanged(self, pageId):
         """
@@ -378,108 +218,34 @@
     def validateCurrentPage(self):
         """
         Validate the current page before moving on to the next page.
+
+        Provides each song format class with a chance to validate its input by
+        overriding isValidSource().
         """
         if self.currentPage() == self.welcomePage:
             return True
         elif self.currentPage() == self.sourcePage:
-            source_format = self.formatComboBox.currentIndex()
+            format = self.currentFormat
             QtCore.QSettings().setValue(u'songs/last import type',
-                source_format)
-            import_class = SongFormat.get_class(source_format)
-            if source_format == SongFormat.OpenLP2:
-                if self.openLP2FilenameEdit.text().isEmpty():
-                    critical_error_message_box(UiStrings().NFSs,
-                        WizardStrings.YouSpecifyFile % UiStrings().OLPV2)
-                    self.openLP2BrowseButton.setFocus()
-                    return False
-            elif source_format == SongFormat.OpenLP1:
-                if self.openLP1FilenameEdit.text().isEmpty():
-                    critical_error_message_box(UiStrings().NFSs,
-                        WizardStrings.YouSpecifyFile % UiStrings().OLPV1)
-                    self.openLP1BrowseButton.setFocus()
-                    return False
-            elif source_format == SongFormat.PowerSong:
-                if self.powerSongFilenameEdit.text().isEmpty() or \
-                    not import_class.isValidSource(
-                    folder=self.powerSongFilenameEdit.text()):
-                    critical_error_message_box(UiStrings().NFdSs,
-                        WizardStrings.YouSpecifyFolder % WizardStrings.PS)
-                    self.powerSongBrowseButton.setFocus()
-                    return False
-            elif source_format == SongFormat.OpenLyrics:
-                if self.openLyricsFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.OL)
-                    self.openLyricsAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.OpenSong:
-                if self.openSongFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.OS)
-                    self.openSongAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.WordsOfWorship:
-                if self.wordsOfWorshipFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.WoW)
-                    self.wordsOfWorshipAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.CCLI:
-                if self.ccliFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.CCLI)
-                    self.ccliAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.DreamBeam:
-                if self.dreamBeamFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.DB)
-                    self.dreamBeamAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.SongsOfFellowship:
-                if self.songsOfFellowshipFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.SoF)
-                    self.songsOfFellowshipAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.Generic:
-                if self.genericFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        translate('SongsPlugin.ImportWizardForm',
-                        'You need to specify at least one document or '
-                        'presentation file to import from.'))
-                    self.genericAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.EasySlides:
-                if self.easySlidesFilenameEdit.text().isEmpty():
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.ES)
-                    self.easySlidesBrowseButton.setFocus()
-                    return False
-            elif source_format == SongFormat.EasyWorship:
-                if self.ewFilenameEdit.text().isEmpty():
-                    critical_error_message_box(UiStrings().NFSs,
-                        WizardStrings.YouSpecifyFile % WizardStrings.EW)
-                    self.ewBrowseButton.setFocus()
-                    return False
-            elif source_format == SongFormat.SongBeamer:
-                if self.songBeamerFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.SB)
-                    self.songBeamerAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.SongShowPlus:
-                if self.songShowPlusFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.SSP)
-                    self.songShowPlusAddButton.setFocus()
-                    return False
-            elif source_format == SongFormat.FoilPresenter:
-                if self.foilPresenterFileListWidget.count() == 0:
-                    critical_error_message_box(UiStrings().NFSp,
-                        WizardStrings.YouSpecifyFile % WizardStrings.FP)
-                    self.foilPresenterAddButton.setFocus()
-                    return False
+                format)
+            select_mode, class_, error_msg = \
+                SongFormatAttr.get(format, SongFormatAttr.select_mode,
+                SongFormatAttr.class_, SongFormatAttr.invalid_source_msg)
+            if select_mode == SongFormatSelect.MultipleFiles:
+                import_source = self.getListOfFiles(
+                    self.formatWidgets[format][u'fileListWidget'])
+                error_title = UiStrings().IFSp
+                focus_button = self.formatWidgets[format][u'addButton']
+            else:
+                import_source = \
+                    self.formatWidgets[format][u'filepathEdit'].text()
+                error_title = UiStrings().IFSs if select_mode == \
+                    SongFormatSelect.SingleFile else UiStrings().IFdSs
+                focus_button = self.formatWidgets[format][u'browseButton']
+            if not class_.isValidSource(import_source):
+                critical_error_message_box(error_title, error_msg)
+                focus_button.setFocus()
+                return False
             return True
         elif self.currentPage() == self.progressPage:
             return True
@@ -525,203 +291,41 @@
             item = listbox.takeItem(listbox.row(item))
             del item
 
-    def onOpenLP2BrowseButtonClicked(self):
-        """
-        Get OpenLP v2 song database file
-        """
-        self.getFileName(WizardStrings.OpenTypeFile % UiStrings().OLPV2,
-            self.openLP2FilenameEdit, u'%s (*.sqlite)'
-            % (translate('SongsPlugin.ImportWizardForm',
-            'OpenLP 2.0 Databases'))
-        )
-
-    def onOpenLP1BrowseButtonClicked(self):
-        """
-        Get OpenLP v1 song database file
-        """
-        self.getFileName(WizardStrings.OpenTypeFile % UiStrings().OLPV1,
-            self.openLP1FilenameEdit, u'%s (*.olp)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'openlp.org v1.x Databases')
-        )
-
-    def onPowerSongBrowseButtonClicked(self):
-        """
-        Get PowerSong song database folder
-        """
-        self.getFolder(WizardStrings.OpenTypeFolder % WizardStrings.PS,
-            self.powerSongFilenameEdit)
-
-    def onOpenLyricsAddButtonClicked(self):
-        """
-        Get OpenLyrics song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.OL,
-            self.openLyricsFileListWidget, u'%s (*.xml)' %
-            translate('SongsPlugin.ImportWizardForm', 'OpenLyrics Files'))
-
-    def onOpenLyricsRemoveButtonClicked(self):
-        """
-        Remove selected OpenLyrics files from the import list
-        """
-        self.removeSelectedItems(self.openLyricsFileListWidget)
-
-    def onOpenSongAddButtonClicked(self):
-        """
-        Get OpenSong song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.OS,
-            self.openSongFileListWidget)
-
-    def onOpenSongRemoveButtonClicked(self):
-        """
-        Remove selected OpenSong files from the import list
-        """
-        self.removeSelectedItems(self.openSongFileListWidget)
-
-    def onWordsOfWorshipAddButtonClicked(self):
-        """
-        Get Words of Worship song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.WoW,
-            self.wordsOfWorshipFileListWidget, u'%s (*.wsg *.wow-song)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'Words Of Worship Song Files')
-        )
-
-    def onWordsOfWorshipRemoveButtonClicked(self):
-        """
-        Remove selected Words of Worship files from the import list
-        """
-        self.removeSelectedItems(self.wordsOfWorshipFileListWidget)
-
-    def onCCLIAddButtonClicked(self):
-        """
-        Get CCLI song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.CCLI,
-            self.ccliFileListWidget,  u'%s (*.usr *.txt)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'CCLI SongSelect Files'))
-
-    def onCCLIRemoveButtonClicked(self):
-        """
-        Remove selected CCLI files from the import list
-        """
-        self.removeSelectedItems(self.ccliFileListWidget)
-
-    def onDreamBeamAddButtonClicked(self):
-        """
-        Get DreamBeam song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.DB,
-            self.dreamBeamFileListWidget, u'%s (*.xml)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'DreamBeam Song Files')
-        )
-
-    def onDreamBeamRemoveButtonClicked(self):
-        """
-        Remove selected DreamBeam files from the import list
-        """
-        self.removeSelectedItems(self.dreamBeamFileListWidget)
-
-    def onSongsOfFellowshipAddButtonClicked(self):
-        """
-        Get Songs of Fellowship song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.SoF,
-            self.songsOfFellowshipFileListWidget, u'%s (*.rtf)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'Songs Of Fellowship Song Files')
-        )
-
-    def onSongsOfFellowshipRemoveButtonClicked(self):
-        """
-        Remove selected Songs of Fellowship files from the import list
-        """
-        self.removeSelectedItems(self.songsOfFellowshipFileListWidget)
-
-    def onGenericAddButtonClicked(self):
-        """
-        Get song database files
-        """
-        self.getFiles(
-            translate('SongsPlugin.ImportWizardForm',
-            'Select Document/Presentation Files'),
-            self.genericFileListWidget
-        )
-
-    def onGenericRemoveButtonClicked(self):
-        """
-        Remove selected files from the import list
-        """
-        self.removeSelectedItems(self.genericFileListWidget)
-
-    def onEasySlidesBrowseButtonClicked(self):
-        """
-        Get EasySlides song database file
-        """
-        self.getFileName(WizardStrings.OpenTypeFile % WizardStrings.ES,
-            self.easySlidesFilenameEdit, u'%s (*.xml)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'EasySlides XML File'))
-
-    def onEWBrowseButtonClicked(self):
-        """
-        Get EasyWorship song database files
-        """
-        self.getFileName(WizardStrings.OpenTypeFile % WizardStrings.EW,
-            self.ewFilenameEdit, u'%s (*.db)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'EasyWorship Song Database'))
-
-    def onSongBeamerAddButtonClicked(self):
-        """
-        Get SongBeamer song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.SB,
-            self.songBeamerFileListWidget, u'%s (*.sng)' %
-            translate('SongsPlugin.ImportWizardForm', 'SongBeamer Files')
-        )
-
-    def onSongBeamerRemoveButtonClicked(self):
-        """
-        Remove selected SongBeamer files from the import list
-        """
-        self.removeSelectedItems(self.songBeamerFileListWidget)
-
-    def onSongShowPlusAddButtonClicked(self):
-        """
-        Get SongShow Plus song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.SSP,
-            self.songShowPlusFileListWidget, u'%s (*.sbsong)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'SongShow Plus Song Files')
-        )
-
-    def onSongShowPlusRemoveButtonClicked(self):
-        """
-        Remove selected SongShow Plus files from the import list
-        """
-        self.removeSelectedItems(self.songShowPlusFileListWidget)
-
-    def onFoilPresenterAddButtonClicked(self):
-        """
-        Get FoilPresenter song database files
-        """
-        self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.FP,
-            self.foilPresenterFileListWidget, u'%s (*.foil)'
-            % translate('SongsPlugin.ImportWizardForm',
-            'Foilpresenter Song Files')
-        )
-
-    def onFoilPresenterRemoveButtonClicked(self):
-        """
-        Remove selected FoilPresenter files from the import list
-        """
-        self.removeSelectedItems(self.foilPresenterFileListWidget)
+    def onBrowseButtonClicked(self):
+        format = self.currentFormat
+        select_mode, format_name, filter = SongFormatAttr.get(format,
+            SongFormatAttr.select_mode, SongFormatAttr.name,
+            SongFormatAttr.filter)
+        filepathEdit = self.formatWidgets[format][u'filepathEdit']
+        if select_mode == SongFormatSelect.SingleFile:
+            self.getFileName(WizardStrings.OpenTypeFile % format_name,
+                filepathEdit, filter)
+        elif select_mode == SongFormatSelect.SingleFolder:
+            self.getFolder(WizardStrings.OpenTypeFolder % format_name,
+                filepathEdit)
+
+    def onAddButtonClicked(self):
+        format = self.currentFormat
+        select_mode, format_name, filter, custom_title = SongFormatAttr.get(
+            format, SongFormatAttr.select_mode, SongFormatAttr.name,
+            SongFormatAttr.filter, SongFormatAttr.get_files_title)
+        title = custom_title if custom_title \
+            else WizardStrings.OpenTypeFile % format_name
+        if select_mode == SongFormatSelect.MultipleFiles:
+            self.getFiles(title, self.formatWidgets[format][u'fileListWidget'],
+                filter)
+            self.sourcePage.emit(QtCore.SIGNAL(u'completeChanged()'))
+
+    def onRemoveButtonClicked(self):
+        self.removeSelectedItems(
+            self.formatWidgets[self.currentFormat][u'fileListWidget'])
+        self.sourcePage.emit(QtCore.SIGNAL(u'completeChanged()'))
+
+    def onFilepathEditTextChanged(self):
+        """
+        Called when the content of the Filename/Folder edit box changes.
+        """
+        self.sourcePage.emit(QtCore.SIGNAL(u'completeChanged()'))
 
     def setDefaults(self):
         """
@@ -736,22 +340,12 @@
             last_import_type >= self.formatComboBox.count():
             last_import_type = 0
         self.formatComboBox.setCurrentIndex(last_import_type)
-        self.openLP2FilenameEdit.setText(u'')
-        self.openLP1FilenameEdit.setText(u'')
-        self.powerSongFilenameEdit.setText(u'')
-        self.openLyricsFileListWidget.clear()
-        self.openSongFileListWidget.clear()
-        self.wordsOfWorshipFileListWidget.clear()
-        self.ccliFileListWidget.clear()
-        self.dreamBeamFileListWidget.clear()
-        self.songsOfFellowshipFileListWidget.clear()
-        self.genericFileListWidget.clear()
-        self.easySlidesFilenameEdit.setText(u'')
-        self.ewFilenameEdit.setText(u'')
-        self.songBeamerFileListWidget.clear()
-        self.songShowPlusFileListWidget.clear()
-        self.foilPresenterFileListWidget.clear()
-        #self.csvFilenameEdit.setText(u'')
+        for format in SongFormat.get_format_list():
+            select_mode = SongFormatAttr.get(format, SongFormatAttr.select_mode)
+            if select_mode == SongFormatSelect.MultipleFiles:
+                self.formatWidgets[format][u'fileListWidget'].clear()
+            else:
+                self.formatWidgets[format][u'filepathEdit'].setText(u'')
         self.errorReportTextEdit.clear()
         self.errorReportTextEdit.setHidden(True)
         self.errorCopyToButton.setHidden(True)
@@ -771,87 +365,19 @@
         class, and then runs the ``doImport`` method of the importer to do
         the actual importing.
         """
-        source_format = self.formatComboBox.currentIndex()
-        importer = None
-        if source_format == SongFormat.OpenLP2:
-            # Import an OpenLP 2.0 database
-            importer = self.plugin.importSongs(SongFormat.OpenLP2,
-                filename=unicode(self.openLP2FilenameEdit.text())
-            )
-        elif source_format == SongFormat.OpenLP1:
-            # Import an openlp.org database
-            importer = self.plugin.importSongs(SongFormat.OpenLP1,
-                filename=unicode(self.openLP1FilenameEdit.text()),
-                plugin=self.plugin
-            )
-        elif source_format == SongFormat.PowerSong:
-            # Import PowerSong folder
-            importer = self.plugin.importSongs(SongFormat.PowerSong,
-                folder=unicode(self.powerSongFilenameEdit.text())
-            )
-        elif source_format == SongFormat.OpenLyrics:
-            # Import OpenLyrics songs
-            importer = self.plugin.importSongs(SongFormat.OpenLyrics,
-                filenames=self.getListOfFiles(self.openLyricsFileListWidget)
-            )
-        elif source_format == SongFormat.OpenSong:
-            # Import OpenSong songs
-            importer = self.plugin.importSongs(SongFormat.OpenSong,
-                filenames=self.getListOfFiles(self.openSongFileListWidget)
-            )
-        elif source_format == SongFormat.WordsOfWorship:
-            # Import Words Of Worship songs
-            importer = self.plugin.importSongs(SongFormat.WordsOfWorship,
-                filenames=self.getListOfFiles(
-                    self.wordsOfWorshipFileListWidget)
-            )
-        elif source_format == SongFormat.CCLI:
-            # Import Words Of Worship songs
-            importer = self.plugin.importSongs(SongFormat.CCLI,
-                filenames=self.getListOfFiles(self.ccliFileListWidget)
-            )
-        elif source_format == SongFormat.DreamBeam:
-            # Import DreamBeam songs
-            importer = self.plugin.importSongs(SongFormat.DreamBeam,
-                filenames=self.getListOfFiles(
-                    self.dreamBeamFileListWidget)
-            )
-        elif source_format == SongFormat.SongsOfFellowship:
-            # Import a Songs of Fellowship RTF file
-            importer = self.plugin.importSongs(SongFormat.SongsOfFellowship,
-                filenames=self.getListOfFiles(
-                    self.songsOfFellowshipFileListWidget)
-            )
-        elif source_format == SongFormat.Generic:
-            # Import a generic document or presentation
-            importer = self.plugin.importSongs(SongFormat.Generic,
-                filenames=self.getListOfFiles(self.genericFileListWidget)
-            )
-        elif source_format == SongFormat.EasySlides:
-            # Import an EasySlides export file
-            importer = self.plugin.importSongs(SongFormat.EasySlides,
-                filename=unicode(self.easySlidesFilenameEdit.text())
-            )
-        elif source_format == SongFormat.EasyWorship:
-            # Import an EasyWorship database
-            importer = self.plugin.importSongs(SongFormat.EasyWorship,
-                filename=unicode(self.ewFilenameEdit.text())
-            )
-        elif source_format == SongFormat.SongBeamer:
-            # Import SongBeamer songs
-            importer = self.plugin.importSongs(SongFormat.SongBeamer,
-                filenames=self.getListOfFiles(self.songBeamerFileListWidget)
-            )
-        elif source_format == SongFormat.SongShowPlus:
-            # Import ShongShow Plus songs
-            importer = self.plugin.importSongs(SongFormat.SongShowPlus,
-                filenames=self.getListOfFiles(self.songShowPlusFileListWidget)
-            )
-        elif source_format == SongFormat.FoilPresenter:
-            # Import Foilpresenter songs
-            importer = self.plugin.importSongs(SongFormat.FoilPresenter,
-                filenames=self.getListOfFiles(self.foilPresenterFileListWidget)
-            )
+        source_format = self.currentFormat
+        select_mode = SongFormatAttr.get(source_format,
+            SongFormatAttr.select_mode)
+        if select_mode == SongFormatSelect.SingleFile:
+            importer = self.plugin.importSongs(source_format, filename=unicode(
+                self.formatWidgets[source_format][u'filepathEdit'].text()))
+        elif select_mode == SongFormatSelect.SingleFolder:
+            importer = self.plugin.importSongs(source_format, folder=unicode(
+                self.formatWidgets[source_format][u'filepathEdit'].text()))
+        else:
+            importer = self.plugin.importSongs(source_format,
+                filenames=self.getListOfFiles(
+                self.formatWidgets[source_format][u'fileListWidget']))
         importer.doImport()
         self.progressLabel.setText(WizardStrings.FinishedImport)
 
@@ -873,35 +399,47 @@
         report_file.write(self.errorReportTextEdit.toPlainText())
         report_file.close()
 
-    def addFileSelectItem(self, prefix, obj_prefix=None, can_disable=False,
-        single_select=False):
+    def addFileSelectItem(self):
+        format = self.currentFormat
+        prefix, obj_prefix, can_disable, select_mode = SongFormatAttr.get(
+            format, SongFormatAttr.prefix, SongFormatAttr.obj_prefix,
+            SongFormatAttr.can_disable, SongFormatAttr.select_mode)
         if not obj_prefix:
             obj_prefix = prefix
         page = QtGui.QWidget()
         page.setObjectName(obj_prefix + u'Page')
         if can_disable:
-            importWidget = self.disablableWidget(page, prefix, obj_prefix)
+            importWidget = self.disablableWidget(page, obj_prefix)
         else:
             importWidget = page
         importLayout = QtGui.QVBoxLayout(importWidget)
         importLayout.setMargin(0)
         importLayout.setObjectName(obj_prefix + u'ImportLayout')
-        if single_select:
-            fileLayout = QtGui.QHBoxLayout()
-            fileLayout.setObjectName(obj_prefix + u'FileLayout')
-            filenameLabel = QtGui.QLabel(importWidget)
-            filenameLabel.setObjectName(obj_prefix + u'FilenameLabel')
-            fileLayout.addWidget(filenameLabel)
-            filenameEdit = QtGui.QLineEdit(importWidget)
-            filenameEdit.setObjectName(obj_prefix + u'FilenameEdit')
-            fileLayout.addWidget(filenameEdit)
+        if select_mode == SongFormatSelect.SingleFile or \
+            select_mode == SongFormatSelect.SingleFolder:
+            filepathLayout = QtGui.QHBoxLayout()
+            filepathLayout.setObjectName(obj_prefix + u'FilepathLayout')
+            filepathLabel = QtGui.QLabel(importWidget)
+            filepathLabel.setObjectName(obj_prefix + u'FilepathLabel')
+            filepathLayout.addWidget(filepathLabel)
+            filepathSpacer = QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Fixed,
+                QtGui.QSizePolicy.Fixed)
+            filepathLayout.addSpacerItem(filepathSpacer)
+            filepathEdit = QtGui.QLineEdit(importWidget)
+            filepathEdit.setObjectName(obj_prefix + u'FilepathEdit')
+            filepathLayout.addWidget(filepathEdit)
             browseButton = QtGui.QToolButton(importWidget)
             browseButton.setIcon(self.openIcon)
             browseButton.setObjectName(obj_prefix + u'BrowseButton')
-            fileLayout.addWidget(browseButton)
-            importLayout.addLayout(fileLayout)
+            filepathLayout.addWidget(browseButton)
+            importLayout.addLayout(filepathLayout)
             importLayout.addSpacerItem(self.stackSpacer)
-        else:
+            self.formatWidgets[format][u'filepathLabel'] = filepathLabel
+            self.formatWidgets[format][u'filepathSpacer'] = filepathSpacer
+            self.formatWidgets[format][u'filepathLayout'] = filepathLayout
+            self.formatWidgets[format][u'filepathEdit'] = filepathEdit
+            self.formatWidgets[format][u'browseButton'] = browseButton
+        elif select_mode == SongFormatSelect.MultipleFiles:
             fileListWidget = QtGui.QListWidget(importWidget)
             fileListWidget.setSelectionMode(
                 QtGui.QAbstractItemView.ExtendedSelection)
@@ -919,22 +457,18 @@
             removeButton.setObjectName(obj_prefix + u'RemoveButton')
             buttonLayout.addWidget(removeButton)
             importLayout.addLayout(buttonLayout)
+            self.formatWidgets[format][u'fileListWidget'] = fileListWidget
+            self.formatWidgets[format][u'buttonLayout'] = buttonLayout
+            self.formatWidgets[format][u'addButton'] = addButton
+            self.formatWidgets[format][u'removeButton'] = removeButton
         self.formatStack.addWidget(page)
-        setattr(self, prefix + u'Page', page)
-        if single_select:
-            setattr(self, prefix + u'FilenameLabel', filenameLabel)
-            setattr(self, prefix + u'FileLayout', fileLayout)
-            setattr(self, prefix + u'FilenameEdit', filenameEdit)
-            setattr(self, prefix + u'BrowseButton', browseButton)
-        else:
-            setattr(self, prefix + u'FileListWidget', fileListWidget)
-            setattr(self, prefix + u'ButtonLayout', buttonLayout)
-            setattr(self, prefix + u'AddButton', addButton)
-            setattr(self, prefix + u'RemoveButton', removeButton)
-        setattr(self, prefix + u'ImportLayout', importLayout)
+        self.formatWidgets[format][u'page'] = page
+        self.formatWidgets[format][u'importLayout'] = importLayout
         self.formatComboBox.addItem(u'')
 
-    def disablableWidget(self, page, prefix, obj_prefix):
+    def disablableWidget(self, page, obj_prefix):
+        format = self.currentFormat
+        self.disablableFormats.append(format)
         layout = QtGui.QVBoxLayout(page)
         layout.setMargin(0)
         layout.setSpacing(0)
@@ -954,9 +488,35 @@
         importWidget = QtGui.QWidget(page)
         importWidget.setObjectName(obj_prefix + u'ImportWidget')
         layout.addWidget(importWidget)
-        setattr(self, prefix + u'Layout', layout)
-        setattr(self, prefix + u'DisabledWidget', disabledWidget)
-        setattr(self, prefix + u'DisabledLayout', disabledLayout)
-        setattr(self, prefix + u'DisabledLabel', disabledLabel)
-        setattr(self, prefix + u'ImportWidget', importWidget)
+        self.formatWidgets[format][u'layout'] = layout
+        self.formatWidgets[format][u'disabledWidget'] = disabledWidget
+        self.formatWidgets[format][u'disabledLayout'] = disabledLayout
+        self.formatWidgets[format][u'disabledLabel'] = disabledLabel
+        self.formatWidgets[format][u'importWidget'] = importWidget
         return importWidget
+
+class SongImportSourcePage(QtGui.QWizardPage):
+    """
+    Subclass QtGui.QWizardPage in order to reimplement isComplete().
+    """
+
+    def isComplete(self):
+        """
+        Returns True if an available format is selected, and the
+        file/folder/files widget is not empty.
+
+        When this method returns True, the wizard's Next button is enabled.
+        """
+        wizard = self.wizard()
+        format = wizard.currentFormat
+        select_mode, format_available = SongFormatAttr.get(format,
+            SongFormatAttr.select_mode, SongFormatAttr.availability)
+        if format_available:
+            if select_mode == SongFormatSelect.MultipleFiles:
+                if wizard.formatWidgets[format][u'fileListWidget'].count() > 0:
+                    return True
+            else:
+                filepathEdit = wizard.formatWidgets[format][u'filepathEdit']
+                if not filepathEdit.text().isEmpty():
+                    return True
+        return False

=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py	2012-04-30 12:19:36 +0000
+++ openlp/plugins/songs/lib/importer.py	2012-05-31 14:10:23 +0000
@@ -29,6 +29,9 @@
 """
 import logging
 
+from openlp.core.lib import translate
+from openlp.core.lib.ui import UiStrings
+from openlp.core.ui.wizard import WizardStrings
 from opensongimport import OpenSongImport
 from easyslidesimport import EasySlidesImport
 from olpimport import OpenLPSongImport
@@ -64,11 +67,9 @@
 
 class SongFormat(object):
     """
-    This is a special enumeration class that holds the various types of songs,
-    plus a few helper functions to facilitate generic handling of song types
-    for importing.
+    This is a special enumeration class that holds the various types of song
+    importers and some helper functions to facilitate access.
     """
-    _format_availability = {}
     Unknown = -1
     OpenLyrics = 0
     OpenLP2 = 1
@@ -88,47 +89,7 @@
     #CSV = 15
 
     @staticmethod
-    def get_class(format):
-        """
-        Return the appropriate implementation class.
-
-        ``format``
-            The song format.
-        """
-        if format == SongFormat.OpenLP2:
-            return OpenLPSongImport
-        elif format == SongFormat.OpenLP1:
-            return OpenLP1SongImport
-        elif format == SongFormat.OpenLyrics:
-            return OpenLyricsImport
-        elif format == SongFormat.OpenSong:
-            return OpenSongImport
-        elif format == SongFormat.SongsOfFellowship:
-            return SofImport
-        elif format == SongFormat.WordsOfWorship:
-            return WowImport
-        elif format == SongFormat.Generic:
-            return OooImport
-        elif format == SongFormat.CCLI:
-            return CCLIFileImport
-        elif format == SongFormat.DreamBeam:
-            return DreamBeamImport
-        elif format == SongFormat.PowerSong:
-            return PowerSongImport
-        elif format == SongFormat.EasySlides:
-            return EasySlidesImport
-        elif format == SongFormat.EasyWorship:
-            return EasyWorshipSongImport
-        elif format == SongFormat.SongBeamer:
-            return SongBeamerImport
-        elif format == SongFormat.SongShowPlus:
-            return SongShowPlusImport
-        elif format == SongFormat.FoilPresenter:
-            return FoilPresenterImport
-        return None
-
-    @staticmethod
-    def get_formats_list():
+    def get_format_list():
         """
         Return a list of the supported song formats.
         """
@@ -138,7 +99,7 @@
             SongFormat.OpenLP1,
             SongFormat.Generic,
             SongFormat.CCLI,
-            SongFormat.DreamBeam, 
+            SongFormat.DreamBeam,
             SongFormat.EasySlides,
             SongFormat.EasyWorship,
             SongFormat.FoilPresenter,
@@ -150,23 +111,258 @@
             SongFormat.WordsOfWorship
         ]
 
-    @staticmethod
-    def set_availability(format, available):
-        """
-        Set the availability for a given song format.
-        """
-        SongFormat._format_availability[format] = available
-
-    @staticmethod
-    def get_availability(format):
-        """
-        Return the availability of a given song format.
-        """
-        return SongFormat._format_availability.get(format, True)
-
-SongFormat.set_availability(SongFormat.OpenLP1, HAS_OPENLP1)
-SongFormat.set_availability(SongFormat.SongsOfFellowship, HAS_SOF)
-SongFormat.set_availability(SongFormat.Generic, HAS_OOO)
-
-__all__ = [u'SongFormat']
-
+class SongFormatSelect(object):
+    """
+    This is a special enumeration class listing available file selection modes.
+    """
+    SingleFile = 0
+    MultipleFiles = 1
+    SingleFolder = 2
+
+class SongFormatAttr(object):
+    """
+    This is a special static class that holds the attributes of each song format
+    to aid the importing of each song type.
+
+    The various definable attributes are enumerated as follows:
+
+    Required attributes for each song format:
+        * ``class_`` Import class, e.g. OpenLyricsImport
+        * ``name`` Name of this format, e.g. u'OpenLyrics'
+        * ``prefix`` Prefix for objects. Use camelCase, e.g. u'openLyrics'
+          See ``SongImportForm.addFileSelectItem()``.
+
+    Optional attributes for each song format:
+        * ``obj_prefix`` Alternate prefix for objects.
+          See ``SongImportForm.addFileSelectItem()``.
+        * ``can_disable`` Whether song format is disablable.
+        * ``availability`` Whether song format is available.
+        * ``select_mode`` Whether format accepts single file, multiple files, or
+          single folder.
+        * ``filter`` File extension filter for QFileDialog.
+
+    Optional/custom text values for SongImportForm widgets:
+       * ``combo_box_text`` Combo box selector (default is format name).
+       * ``disabled_label_text`` Required for disablable song formats.
+       * ``get_files_title`` Title for QFileDialog (default includes format
+         name).
+       * ``invalid_source_msg`` Message shown when source does not validate with
+         class_.isValidSource().
+    """
+    # Required attributes
+    class_ = 0
+    name = 1
+    prefix = 2
+    # Optional attributes
+    obj_prefix = 10
+    can_disable = 11
+    availability = 12
+    select_mode = 13
+    filter = 14
+    # Optional/custom text values
+    combo_box_text = 20
+    disabled_label_text = 21
+    get_files_title = 22
+    invalid_source_msg = 23
+
+    # Set optional attribute defaults
+    _defaults = {
+        obj_prefix: None,
+        can_disable: False,
+        availability: True,
+        select_mode: SongFormatSelect.MultipleFiles,
+        filter: u'',
+        combo_box_text: None,
+        disabled_label_text: u'',
+        get_files_title: None,
+        invalid_source_msg: None
+    }
+
+    # Set attribute values
+    _attributes = {
+        SongFormat.OpenLyrics: {
+            class_: OpenLyricsImport,
+            name: u'OpenLyrics',
+            prefix: u'openLyrics',
+            obj_prefix: u'OpenLyrics',
+            can_disable: True,
+            filter: u'%s (*.xml)' % translate('SongsPlugin.ImportWizardForm',
+                'OpenLyrics Files'),
+            combo_box_text: translate('SongsPlugin.ImportWizardForm',
+                'OpenLyrics or OpenLP 2.0 Exported Song'),
+            disabled_label_text: translate('SongsPlugin.ImportWizardForm',
+                'The OpenLyrics importer has not yet been developed, but as '
+                'you can see, we are still intending to do so. Hopefully it '
+                'will be in the next release.')
+        },
+        SongFormat.OpenLP2: {
+            class_: OpenLPSongImport,
+            name: UiStrings().OLPV2,
+            prefix: u'openLP2',
+            select_mode: SongFormatSelect.SingleFile,
+            filter: u'%s (*.sqlite)' % (translate(
+                'SongsPlugin.ImportWizardForm', 'OpenLP 2.0 Databases'))
+        },
+        SongFormat.OpenLP1: {
+            name: UiStrings().OLPV1,
+            prefix: u'openLP1',
+            can_disable: True,
+            select_mode: SongFormatSelect.SingleFile,
+            filter: u'%s (*.olp)' % translate('SongsPlugin.ImportWizardForm',
+                'openlp.org v1.x Databases'),
+            disabled_label_text: WizardStrings.NoSqlite
+        },
+        SongFormat.Generic: {
+            name: translate('SongsPlugin.ImportWizardForm',
+                'Generic Document/Presentation'),
+            prefix: u'generic',
+            can_disable: True,
+            disabled_label_text: translate('SongsPlugin.ImportWizardForm',
+                'The generic document/presentation importer has been disabled '
+                'because OpenLP cannot access OpenOffice or LibreOffice.'),
+            get_files_title: translate('SongsPlugin.ImportWizardForm',
+                'Select Document/Presentation Files')
+        },
+        SongFormat.CCLI: {
+            class_: CCLIFileImport,
+            name: u'CCLI/SongSelect',
+            prefix: u'ccli',
+            filter: u'%s (*.usr *.txt)' % translate(
+                'SongsPlugin.ImportWizardForm', 'CCLI SongSelect Files')
+        },
+        SongFormat.DreamBeam: {
+            class_: DreamBeamImport,
+            name: u'DreamBeam',
+            prefix: u'dreamBeam',
+            filter: u'%s (*.xml)' % translate('SongsPlugin.ImportWizardForm',
+                'DreamBeam Song Files')
+        },
+        SongFormat.EasySlides: {
+            class_: EasySlidesImport,
+            name: u'EasySlides',
+            prefix: u'easySlides',
+            select_mode: SongFormatSelect.SingleFile,
+            filter: u'%s (*.xml)' % translate('SongsPlugin.ImportWizardForm',
+                'EasySlides XML File')
+        },
+        SongFormat.EasyWorship: {
+            class_: EasyWorshipSongImport,
+            name: u'EasyWorship',
+            prefix: u'ew',
+            select_mode: SongFormatSelect.SingleFile,
+            filter: u'%s (*.db)' % translate('SongsPlugin.ImportWizardForm',
+                'EasyWorship Song Database')
+        },
+        SongFormat.FoilPresenter: {
+            class_: FoilPresenterImport,
+            name: u'Foilpresenter',
+            prefix: u'foilPresenter',
+            filter: u'%s (*.foil)' % translate('SongsPlugin.ImportWizardForm',
+                'Foilpresenter Song Files')
+        },
+        SongFormat.OpenSong: {
+            class_: OpenSongImport,
+            name: WizardStrings.OS,
+            prefix: u'openSong',
+            obj_prefix: u'OpenSong'
+        },
+        SongFormat.PowerSong: {
+            class_: PowerSongImport,
+            name: u'PowerSong 1.0',
+            prefix: u'powerSong',
+            select_mode: SongFormatSelect.SingleFolder,
+            invalid_source_msg: translate('SongsPlugin.ImportWizardForm',
+                'You need to specify a valid PowerSong 1.0 database folder.')
+        },
+        SongFormat.SongBeamer: {
+            class_: SongBeamerImport,
+            name: u'SongBeamer',
+            prefix: u'songBeamer',
+            filter: u'%s (*.sng)' % translate('SongsPlugin.ImportWizardForm',
+                'SongBeamer Files')
+        },
+        SongFormat.SongShowPlus: {
+            class_: SongShowPlusImport,
+            name: u'SongShow Plus',
+            prefix: u'songShowPlus',
+            filter: u'%s (*.sbsong)' % translate('SongsPlugin.ImportWizardForm',
+                'SongShow Plus Song Files')
+        },
+        SongFormat.SongsOfFellowship: {
+            name: u'Songs of Fellowship',
+            prefix: u'songsOfFellowship',
+            can_disable: True,
+            filter: u'%s (*.rtf)' % translate('SongsPlugin.ImportWizardForm',
+                'Songs Of Fellowship Song Files'),
+            disabled_label_text: translate('SongsPlugin.ImportWizardForm',
+                'The Songs of Fellowship importer has been disabled because '
+                'OpenLP cannot access OpenOffice or LibreOffice.')
+        },
+        SongFormat.WordsOfWorship: {
+            class_: WowImport,
+            name: u'Words of Worship',
+            prefix: u'wordsOfWorship',
+            filter: u'%s (*.wsg *.wow-song)' % translate(
+                'SongsPlugin.ImportWizardForm', 'Words Of Worship Song Files')
+#        },
+#        SongFormat.CSV: {
+#            class_: CSVImport,
+#            name: WizardStrings.CSV,
+#            prefix: u'csv',
+#            obj_prefix: u'CSV',
+#            select_mode: SongFormatSelect.SingleFile
+        }
+    }
+
+    @staticmethod
+    def get(format, *attributes):
+        """
+        Return requested song format attribute(s).
+
+        ``format``
+            A song format from SongFormat.
+
+        ``*attributes``
+            Zero or more song format attributes from SongFormatAttr.
+
+        Return type depends on number of supplied attributes:
+            * 0 : Return dict containing all defined attributes for the format.
+            * 1 : Return the attribute value.
+            * >1 : Return tuple of requested attribute values.
+        """
+        if not attributes:
+            return SongFormatAttr._attributes.get(format)
+        elif len(attributes) == 1:
+            default = SongFormatAttr._defaults.get(attributes[0])
+            return SongFormatAttr._attributes[format].get(attributes[0],
+                default)
+        else:
+            values = []
+            for attr in attributes:
+                default = SongFormatAttr._defaults.get(attr)
+                values.append(SongFormatAttr._attributes[format].get(attr,
+                    default))
+            return tuple(values)
+
+    @staticmethod
+    def set(format, attribute, value):
+        """
+        Set specified song format attribute to the supplied value.
+        """
+        SongFormatAttr._attributes[format][attribute] = value
+
+SongFormatAttr.set(SongFormat.OpenLP1, SongFormatAttr.availability, HAS_OPENLP1)
+if HAS_OPENLP1:
+    SongFormatAttr.set(SongFormat.OpenLP1, SongFormatAttr.class_,
+        OpenLP1SongImport)
+SongFormatAttr.set(SongFormat.SongsOfFellowship, SongFormatAttr.availability,
+    HAS_SOF)
+if HAS_SOF:
+    SongFormatAttr.set(SongFormat.SongsOfFellowship, SongFormatAttr.class_,
+        SofImport)
+SongFormatAttr.set(SongFormat.Generic, SongFormatAttr.availability, HAS_OOO)
+if HAS_OOO:
+    SongFormatAttr.set(SongFormat.Generic, SongFormatAttr.class_,
+        OooImport)
+
+__all__ = [u'SongFormat', u'SongFormatSelect', u'SongFormatAttr']

=== modified file 'openlp/plugins/songs/lib/powersongimport.py'
--- openlp/plugins/songs/lib/powersongimport.py	2012-05-19 10:43:19 +0000
+++ openlp/plugins/songs/lib/powersongimport.py	2012-05-31 14:10:23 +0000
@@ -33,7 +33,6 @@
 import os
 
 from openlp.core.lib import translate
-from openlp.core.ui.wizard import WizardStrings
 from openlp.plugins.songs.lib.songimport import SongImport
 
 log = logging.getLogger(__name__)
@@ -71,26 +70,26 @@
 
         * .song
     """
-
     @staticmethod
-    def isValidSource(**kwargs):
+    def isValidSource(import_source):
         """
         Checks if source is a PowerSong 1.0 folder:
             * is a directory
             * contains at least one *.song file
         """
-        if u'folder' in kwargs:
-            dir = kwargs[u'folder']
-            if os.path.isdir(dir):
-                for file in os.listdir(dir):
-                    if fnmatch.fnmatch(file, u'*.song'):
-                        return True
+        if os.path.isdir(import_source):
+            for file in os.listdir(import_source):
+                if fnmatch.fnmatch(file, u'*.song'):
+                    return True
         return False
 
     def doImport(self):
         """
         Receive either a list of files or a folder (unicode) to import.
         """
+        from importer import SongFormat, SongFormatAttr
+        PS_string = SongFormatAttr.get(SongFormat.PowerSong,
+            SongFormatAttr.name)
         if isinstance(self.importSource, unicode):
             if os.path.isdir(self.importSource):
                 dir = self.importSource
@@ -104,7 +103,7 @@
             self.logError(unicode(translate('SongsPlugin.PowerSongImport',
                 'No songs to import.')),
                 unicode(translate('SongsPlugin.PowerSongImport',
-                'No %s files found.' % WizardStrings.PS)))
+                'No %s files found.' % PS_string)))
             return
         self.importWizard.progressBar.setMaximum(len(self.importSource))
         for file in self.importSource:
@@ -124,7 +123,7 @@
                         self.logError(os.path.basename(file), unicode(
                             translate('SongsPlugin.PowerSongImport',
                             'Invalid %s file. Unexpected byte value.'
-                            % WizardStrings.PS)))
+                            % PS_string)))
                         break
                     else:
                         if label == u'TITLE':
@@ -142,15 +141,14 @@
             if not self.title:
                 self.logError(os.path.basename(file), unicode(
                     translate('SongsPlugin.PowerSongImport',
-                    'Invalid %s file. Missing "TITLE" header.'
-                    % WizardStrings.PS)))
+                    'Invalid %s file. Missing "TITLE" header.' % PS_string)))
                 continue
             # Check that file had COPYRIGHTLINE label
             if not found_copyright:
                 self.logError(self.title, unicode(
                     translate('SongsPlugin.PowerSongImport',
                     'Invalid %s file. Missing "COPYRIGHTLINE" '
-                    'header.' % WizardStrings.PS)))
+                    'header.' % PS_string)))
                 continue
             # Check that file had at least one verse
             if not self.verses:

=== modified file 'openlp/plugins/songs/lib/songimport.py'
--- openlp/plugins/songs/lib/songimport.py	2012-05-19 10:43:19 +0000
+++ openlp/plugins/songs/lib/songimport.py	2012-05-31 14:10:23 +0000
@@ -51,11 +51,11 @@
     as necessary
     """
     @staticmethod
-    def isValidSource(**kwargs):
+    def isValidSource(import_source):
         """
         Override this method to validate the source prior to import.
         """
-        pass
+        return True
 
     def __init__(self, manager, **kwargs):
         """

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2012-04-29 15:31:56 +0000
+++ openlp/plugins/songs/songsplugin.py	2012-05-31 14:10:23 +0000
@@ -39,7 +39,7 @@
 from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \
     SongsTab
 from openlp.plugins.songs.lib.db import init_schema, Song
-from openlp.plugins.songs.lib.importer import SongFormat
+from openlp.plugins.songs.lib.importer import SongFormatAttr
 from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
 
 log = logging.getLogger(__name__)
@@ -194,7 +194,7 @@
             self.manager.save_object(song)
 
     def importSongs(self, format, **kwargs):
-        class_ = SongFormat.get_class(format)
+        class_ = SongFormatAttr.get(format, SongFormatAttr.class_)
         importer = class_(self.manager, **kwargs)
         importer.register(self.mediaItem.importWizard)
         return importer