Esempio n. 1
0
    def test_get_song(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that the get_song() method returns the correct song details
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_song_page = MagicMock()
        mocked_copyright = MagicMock()
        mocked_copyright.find_all.return_value = [
            MagicMock(string='Copyright 1'),
            MagicMock(string='Copyright 2')
        ]
        mocked_song_page.find.side_effect = [
            mocked_copyright,
            MagicMock(find=MagicMock(string='CCLI: 123456'))
        ]
        mocked_lyrics_page = MagicMock()
        mocked_find_all = MagicMock()
        mocked_find_all.side_effect = [[
            MagicMock(
                contents=
                'The Lord told Noah: there\'s gonna be a floody, floody'),
            MagicMock(
                contents='So, rise and shine, and give God the glory, glory'),
            MagicMock(contents='The Lord told Noah to build him an arky, arky')
        ],
                                       [
                                           MagicMock(string='Verse 1'),
                                           MagicMock(string='Chorus'),
                                           MagicMock(string='Verse 2')
                                       ]]
        mocked_lyrics_page.find.return_value = MagicMock(
            find_all=mocked_find_all)
        MockedBeautifulSoup.side_effect = [
            mocked_song_page, mocked_lyrics_page
        ]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)
        fake_song = {
            'title': 'Title',
            'authors': ['Author 1', 'Author 2'],
            'link': 'url'
        }

        # WHEN: get_song is called
        result = importer.get_song(fake_song, callback=mocked_callback)

        # THEN: The callback should have been called three times and the song should be returned
        assert 3 == mocked_callback.call_count, 'The callback should have been called twice'
        assert result is not None, 'The get_song() method should have returned a song dictionary'
        assert 2 == mocked_lyrics_page.find.call_count, 'The find() method should have been called twice'
        assert 2 == mocked_find_all.call_count, 'The find_all() method should have been called twice'
        assert [call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')] == \
            mocked_lyrics_page.find.call_args_list, 'The find() method should have been called with the right arguments'
        assert [call('p'), call('h3')] == mocked_find_all.call_args_list, \
            'The find_all() method should have been called with the right arguments'
        assert 'copyright' in result, 'The returned song should have a copyright'
        assert 'ccli_number' in result, 'The returned song should have a CCLI number'
        assert 'verses' in result, 'The returned song should have verses'
        assert 3 == len(
            result['verses']), 'Three verses should have been returned'
Esempio n. 2
0
    def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called twice and None should be returned
        self.assertEqual(2, mocked_callback.call_count, 'The callback should have been called twice')
        self.assertIsNone(result, 'The get_song() method should have returned None')
Esempio n. 3
0
    def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        song_page = MagicMock(return_value={'href': '/lyricpage'})
        MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called twice and None should be returned
        assert 2 == mocked_callback.call_count, 'The callback should have been called twice'
        assert result is None, 'The get_song() method should have returned None'
Esempio n. 4
0
    def test_get_song_page_raises_exception(self, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad song page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known')
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called once and None should be returned
        mocked_callback.assert_called_with()
        assert result is None, 'The get_song() method should have returned None'
Esempio n. 5
0
    def test_get_song_page_raises_exception(self, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad song page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known')
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called once and None should be returned
        mocked_callback.assert_called_with()
        self.assertIsNone(result, 'The get_song() method should have returned None')
Esempio n. 6
0
    def test_get_song(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that the get_song() method returns the correct song details
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_song_page = MagicMock()
        mocked_copyright = MagicMock()
        mocked_copyright.find_all.return_value = [MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2')]
        mocked_song_page.find.side_effect = [
            mocked_copyright,
            MagicMock(find=MagicMock(string='CCLI: 123456'))
        ]
        mocked_lyrics_page = MagicMock()
        mocked_find_all = MagicMock()
        mocked_find_all.side_effect = [
            [
                MagicMock(contents='The Lord told Noah: there\'s gonna be a floody, floody'),
                MagicMock(contents='So, rise and shine, and give God the glory, glory'),
                MagicMock(contents='The Lord told Noah to build him an arky, arky')
            ],
            [MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2')]
        ]
        mocked_lyrics_page.find.return_value = MagicMock(find_all=mocked_find_all)
        MockedBeautifulSoup.side_effect = [mocked_song_page, mocked_lyrics_page]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)
        fake_song = {'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url'}

        # WHEN: get_song is called
        result = importer.get_song(fake_song, callback=mocked_callback)

        # THEN: The callback should have been called three times and the song should be returned
        self.assertEqual(3, mocked_callback.call_count, 'The callback should have been called twice')
        self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary')
        self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice')
        self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice')
        self.assertEqual([call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')],
                         mocked_lyrics_page.find.call_args_list,
                         'The find() method should have been called with the right arguments')
        self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list,
                         'The find_all() method should have been called with the right arguments')
        self.assertIn('copyright', result, 'The returned song should have a copyright')
        self.assertIn('ccli_number', result, 'The returned song should have a CCLI number')
        self.assertIn('verses', result, 'The returned song should have verses')
        self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')
Esempio n. 7
0
    def get_song_lyrics_raise_exception_test(self, MockedBeautifulSoup,
                                             mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called twice and None should be returned
        self.assertEqual(2, mocked_callback.call_count,
                         'The callback should have been called twice')
        self.assertIsNone(result,
                          'The get_song() method should have returned None')
Esempio n. 8
0
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog,
                     RegistryProperties):
    """
    The :class:`SongSelectForm` class is the SongSelect dialog.
    """
    def __init__(self, parent=None, plugin=None, db_manager=None):
        QtWidgets.QDialog.__init__(
            self, parent, QtCore.Qt.WindowSystemMenuHint
            | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
        self.plugin = plugin
        self.db_manager = db_manager
        self.setup_ui(self)

    def initialise(self):
        """
        Initialise the SongSelectForm
        """
        self.song_count = 0
        self.song = None
        self.set_progress_visible(False)
        self.song_select_importer = SongSelectImport(self.db_manager)
        self.save_password_checkbox.toggled.connect(
            self.on_save_password_checkbox_toggled)
        self.login_button.clicked.connect(self.on_login_button_clicked)
        self.search_button.clicked.connect(self.on_search_button_clicked)
        self.search_combobox.returnPressed.connect(
            self.on_search_button_clicked)
        self.stop_button.clicked.connect(self.on_stop_button_clicked)
        self.logout_button.clicked.connect(self.done)
        self.search_results_widget.itemDoubleClicked.connect(
            self.on_search_results_widget_double_clicked)
        self.search_results_widget.itemSelectionChanged.connect(
            self.on_search_results_widget_selection_changed)
        self.view_button.clicked.connect(self.on_view_button_clicked)
        self.back_button.clicked.connect(self.on_back_button_clicked)
        self.import_button.clicked.connect(self.on_import_button_clicked)

    def exec(self):
        """
        Execute the dialog. This method sets everything back to its initial
        values.
        """
        self.stacked_widget.setCurrentIndex(0)
        self.username_edit.setEnabled(True)
        self.password_edit.setEnabled(True)
        self.save_password_checkbox.setEnabled(True)
        self.search_combobox.clearEditText()
        self.search_combobox.clear()
        self.search_results_widget.clear()
        self.view_button.setEnabled(False)
        if self.settings.contains(self.plugin.settings_section +
                                  '/songselect password'):
            self.username_edit.setText(
                self.settings.value(self.plugin.settings_section +
                                    '/songselect username'))
            self.password_edit.setText(
                self.settings.value(self.plugin.settings_section +
                                    '/songselect password'))
            self.save_password_checkbox.setChecked(True)
        if self.settings.contains(self.plugin.settings_section +
                                  '/songselect searches'):
            self.search_combobox.addItems(
                self.settings.value(self.plugin.settings_section +
                                    '/songselect searches').split('|'))
        self.username_edit.setFocus()
        return QtWidgets.QDialog.exec(self)

    def done(self, result_code):
        """
        Log out of SongSelect.

        :param result_code: The result of the dialog.
        """
        log.debug('Closing SongSelectForm')
        if self.stacked_widget.currentIndex() > 0:
            progress_dialog = QtWidgets.QProgressDialog(
                translate('SongsPlugin.SongSelectForm', 'Logging out...'), '',
                0, 2, self)
            progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
            progress_dialog.setCancelButton(None)
            progress_dialog.setValue(1)
            progress_dialog.show()
            progress_dialog.setFocus()
            self.application.process_events()
            sleep(0.5)
            self.application.process_events()
            self.song_select_importer.logout()
            self.application.process_events()
            progress_dialog.setValue(2)
        return QtWidgets.QDialog.done(self, result_code)

    def _update_login_progress(self):
        """
        Update the progress bar as the user logs in.
        """
        self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
        self.application.process_events()

    def _update_song_progress(self):
        """
        Update the progress bar as the song is being downloaded.
        """
        self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
        self.application.process_events()

    def _view_song(self, current_item):
        """
        Load a song into the song view.
        """
        if not current_item:
            return
        else:
            current_item = current_item.data(QtCore.Qt.UserRole)
        # Stop the current search, if it's running
        self.song_select_importer.stop()
        # Clear up the UI
        self.song_progress_bar.setVisible(True)
        self.import_button.setEnabled(False)
        self.back_button.setEnabled(False)
        self.title_edit.setText('')
        self.title_edit.setEnabled(False)
        self.copyright_edit.setText('')
        self.copyright_edit.setEnabled(False)
        self.ccli_edit.setText('')
        self.ccli_edit.setEnabled(False)
        self.author_list_widget.clear()
        self.author_list_widget.setEnabled(False)
        self.lyrics_table_widget.clear()
        self.lyrics_table_widget.setRowCount(0)
        self.lyrics_table_widget.setEnabled(False)
        self.stacked_widget.setCurrentIndex(2)
        song = {}
        for key, value in current_item.items():
            song[key] = value
        self.song_progress_bar.setValue(0)
        self.application.process_events()
        # Get the full song
        song = self.song_select_importer.get_song(song,
                                                  self._update_song_progress)
        if not song:
            QtWidgets.QMessageBox.critical(
                self, translate('SongsPlugin.SongSelectForm',
                                'Incomplete song'),
                translate(
                    'SongsPlugin.SongSelectForm',
                    'This song is missing some information, like the lyrics, '
                    'and cannot be imported.'),
                QtWidgets.QMessageBox.StandardButtons(
                    QtWidgets.QMessageBox.Ok), QtWidgets.QMessageBox.Ok)
            self.stacked_widget.setCurrentIndex(1)
            return
        # Update the UI
        self.title_edit.setText(song['title'])
        self.copyright_edit.setText(song['copyright'])
        self.ccli_edit.setText(song['ccli_number'])
        for author in song['authors']:
            QtWidgets.QListWidgetItem(author, self.author_list_widget)
        for counter, verse in enumerate(song['verses']):
            self.lyrics_table_widget.setRowCount(
                self.lyrics_table_widget.rowCount() + 1)
            item = QtWidgets.QTableWidgetItem(verse['lyrics'])
            item.setData(QtCore.Qt.UserRole, verse['label'])
            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
            self.lyrics_table_widget.setItem(counter, 0, item)
        self.lyrics_table_widget.setVerticalHeaderLabels(
            [verse['label'] for verse in song['verses']])
        self.lyrics_table_widget.resizeRowsToContents()
        self.title_edit.setEnabled(True)
        self.copyright_edit.setEnabled(True)
        self.ccli_edit.setEnabled(True)
        self.author_list_widget.setEnabled(True)
        self.lyrics_table_widget.setEnabled(True)
        self.lyrics_table_widget.repaint()
        self.import_button.setEnabled(True)
        self.back_button.setEnabled(True)
        self.song_progress_bar.setVisible(False)
        self.song_progress_bar.setValue(0)
        self.song = song
        self.application.process_events()

    def on_save_password_checkbox_toggled(self, checked):
        """
        Show a warning dialog when the user toggles the save checkbox on or off.

        :param checked: If the combobox is checked or not
        """
        if checked and self.login_page.isVisible():
            answer = QtWidgets.QMessageBox.question(
                self,
                translate('SongsPlugin.SongSelectForm',
                          'Save Username and Password'),
                translate(
                    'SongsPlugin.SongSelectForm',
                    'WARNING: Saving your username and password is INSECURE, your '
                    'password is stored in PLAIN TEXT. Click Yes to save your '
                    'password or No to cancel this.'),
                defaultButton=QtWidgets.QMessageBox.No)
            if answer == QtWidgets.QMessageBox.No:
                self.save_password_checkbox.setChecked(False)

    def on_login_button_clicked(self):
        """
        Log the user in to SongSelect.
        """
        self.username_edit.setEnabled(False)
        self.password_edit.setEnabled(False)
        self.save_password_checkbox.setEnabled(False)
        self.login_button.setEnabled(False)
        self.login_spacer.setVisible(False)
        self.login_progress_bar.setValue(0)
        self.login_progress_bar.setVisible(True)
        self.application.process_events()
        # Log the user in
        subscription_level = self.song_select_importer.login(
            self.username_edit.text(), self.password_edit.text(),
            self._update_login_progress)
        if not subscription_level:
            QtWidgets.QMessageBox.critical(
                self,
                translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
                translate(
                    'SongsPlugin.SongSelectForm',
                    'There was a problem logging in, perhaps your username or password is incorrect?'
                ))
        else:
            if subscription_level == 'Free':
                QtWidgets.QMessageBox.information(
                    self, translate('SongsPlugin.SongSelectForm', 'Free user'),
                    translate(
                        'SongsPlugin.SongSelectForm',
                        'You logged in with a free account, '
                        'the search will be limited to songs '
                        'in the public domain.'))
            if self.save_password_checkbox.isChecked():
                self.settings.setValue(
                    self.plugin.settings_section + '/songselect username',
                    self.username_edit.text())
                self.settings.setValue(
                    self.plugin.settings_section + '/songselect password',
                    self.password_edit.text())
            else:
                self.settings.remove(self.plugin.settings_section +
                                     '/songselect username')
                self.settings.remove(self.plugin.settings_section +
                                     '/songselect password')
            self.stacked_widget.setCurrentIndex(1)
        self.login_progress_bar.setVisible(False)
        self.login_progress_bar.setValue(0)
        self.login_spacer.setVisible(True)
        self.login_button.setEnabled(True)
        self.username_edit.setEnabled(True)
        self.password_edit.setEnabled(True)
        self.save_password_checkbox.setEnabled(True)
        self.search_combobox.setFocus()
        self.application.process_events()

    def on_search_button_clicked(self):
        """
        Run a search on SongSelect.
        """
        # Set up UI components
        self.view_button.setEnabled(False)
        self.search_button.setEnabled(False)
        self.search_combobox.setEnabled(False)
        self.search_progress_bar.setMinimum(0)
        self.search_progress_bar.setMaximum(0)
        self.search_progress_bar.setValue(0)
        self.set_progress_visible(True)
        self.search_results_widget.clear()
        self.result_count_label.setText(
            translate('SongsPlugin.SongSelectForm',
                      'Found {count:d} song(s)').format(count=self.song_count))
        self.application.process_events()
        self.song_count = 0
        search_history = self.search_combobox.getItems()
        self.settings.setValue(
            self.plugin.settings_section + '/songselect searches',
            '|'.join(search_history))
        # Create thread and run search
        worker = SearchWorker(self.song_select_importer,
                              self.search_combobox.currentText())
        worker.show_info.connect(self.on_search_show_info)
        worker.found_song.connect(self.on_search_found_song)
        worker.finished.connect(self.on_search_finished)
        run_thread(worker, 'songselect')

    def on_stop_button_clicked(self):
        """
        Stop the search when the stop button is clicked.
        """
        self.song_select_importer.stop()

    def on_search_show_info(self, title, message):
        """
        Show an informational message from the search thread
        :param title:
        :param message:
        """
        QtWidgets.QMessageBox.information(self, title, message)

    def on_search_found_song(self, song):
        """
        Add a song to the list when one is found.
        :param song:
        """
        self.song_count += 1
        self.result_count_label.setText(
            translate('SongsPlugin.SongSelectForm',
                      'Found {count:d} song(s)').format(count=self.song_count))
        item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
        song_item = QtWidgets.QListWidgetItem(item_title,
                                              self.search_results_widget)
        song_item.setData(QtCore.Qt.UserRole, song)

    def on_search_finished(self):
        """
        Slot which is called when the search is completed.
        """
        self.application.process_events()
        self.set_progress_visible(False)
        self.search_button.setEnabled(True)
        self.search_combobox.setEnabled(True)
        self.application.process_events()

    def on_search_results_widget_selection_changed(self):
        """
        Enable or disable the view button when the selection changes.
        """
        self.view_button.setEnabled(
            len(self.search_results_widget.selectedItems()) > 0)

    def on_view_button_clicked(self):
        """
        View a song from SongSelect.
        """
        self._view_song(self.search_results_widget.currentItem())

    def on_search_results_widget_double_clicked(self, current_item):
        """
        View a song from SongSelect

        :param current_item:
        """
        self._view_song(current_item)

    def on_back_button_clicked(self):
        """
        Go back to the search page.
        """
        self.stacked_widget.setCurrentIndex(1)
        self.search_combobox.setFocus()

    def on_import_button_clicked(self):
        """
        Import a song from SongSelect.
        """
        self.song_select_importer.save_song(self.song)
        self.song = None
        if QtWidgets.QMessageBox.question(
                self,
                translate('SongsPlugin.SongSelectForm', 'Song Imported'),
                translate(
                    'SongsPlugin.SongSelectForm',
                    'Your song has been imported, would you '
                    'like to import more songs?'),
                defaultButton=QtWidgets.QMessageBox.Yes
        ) == QtWidgets.QMessageBox.Yes:
            self.on_back_button_clicked()
        else:
            self.application.process_events()
            self.done(QtWidgets.QDialog.Accepted)

    def set_progress_visible(self, is_visible):
        """
        Show or hide the search progress, including the stop button.
        """
        self.search_progress_bar.setVisible(is_visible)
        self.stop_button.setVisible(is_visible)
Esempio n. 9
0
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
    """
    The :class:`SongSelectForm` class is the SongSelect dialog.
    """

    def __init__(self, parent=None, plugin=None, db_manager=None):
        QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
        self.plugin = plugin
        self.db_manager = db_manager
        self.setup_ui(self)

    def initialise(self):
        """
        Initialise the SongSelectForm
        """
        self.thread = None
        self.worker = None
        self.song_count = 0
        self.song = None
        self.set_progress_visible(False)
        self.song_select_importer = SongSelectImport(self.db_manager)
        self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
        self.login_button.clicked.connect(self.on_login_button_clicked)
        self.search_button.clicked.connect(self.on_search_button_clicked)
        self.search_combobox.returnPressed.connect(self.on_search_button_clicked)
        self.stop_button.clicked.connect(self.on_stop_button_clicked)
        self.logout_button.clicked.connect(self.done)
        self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked)
        self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed)
        self.view_button.clicked.connect(self.on_view_button_clicked)
        self.back_button.clicked.connect(self.on_back_button_clicked)
        self.import_button.clicked.connect(self.on_import_button_clicked)

    def exec(self):
        """
        Execute the dialog. This method sets everything back to its initial
        values.
        """
        self.stacked_widget.setCurrentIndex(0)
        self.username_edit.setEnabled(True)
        self.password_edit.setEnabled(True)
        self.save_password_checkbox.setEnabled(True)
        self.search_combobox.clearEditText()
        self.search_combobox.clear()
        self.search_results_widget.clear()
        self.view_button.setEnabled(False)
        if Settings().contains(self.plugin.settings_section + '/songselect password'):
            self.username_edit.setText(Settings().value(self.plugin.settings_section + '/songselect username'))
            self.password_edit.setText(Settings().value(self.plugin.settings_section + '/songselect password'))
            self.save_password_checkbox.setChecked(True)
        if Settings().contains(self.plugin.settings_section + '/songselect searches'):
            self.search_combobox.addItems(
                Settings().value(self.plugin.settings_section + '/songselect searches').split('|'))
        self.username_edit.setFocus()
        return QtWidgets.QDialog.exec(self)

    def done(self, r):
        """
        Log out of SongSelect.

        :param r: The result of the dialog.
        """
        log.debug('Closing SongSelectForm')
        if self.stacked_widget.currentIndex() > 0:
            progress_dialog = QtWidgets.QProgressDialog(
                translate('SongsPlugin.SongSelectForm', 'Logging out...'), '', 0, 2, self)
            progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
            progress_dialog.setCancelButton(None)
            progress_dialog.setValue(1)
            progress_dialog.show()
            progress_dialog.setFocus()
            self.application.process_events()
            sleep(0.5)
            self.application.process_events()
            self.song_select_importer.logout()
            self.application.process_events()
            progress_dialog.setValue(2)
        return QtWidgets.QDialog.done(self, r)

    def _update_login_progress(self):
        """
        Update the progress bar as the user logs in.
        """
        self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
        self.application.process_events()

    def _update_song_progress(self):
        """
        Update the progress bar as the song is being downloaded.
        """
        self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
        self.application.process_events()

    def _view_song(self, current_item):
        """
        Load a song into the song view.
        """
        if not current_item:
            return
        else:
            current_item = current_item.data(QtCore.Qt.UserRole)
        # Stop the current search, if it's running
        self.song_select_importer.stop()
        # Clear up the UI
        self.song_progress_bar.setVisible(True)
        self.import_button.setEnabled(False)
        self.back_button.setEnabled(False)
        self.title_edit.setText('')
        self.title_edit.setEnabled(False)
        self.copyright_edit.setText('')
        self.copyright_edit.setEnabled(False)
        self.ccli_edit.setText('')
        self.ccli_edit.setEnabled(False)
        self.author_list_widget.clear()
        self.author_list_widget.setEnabled(False)
        self.lyrics_table_widget.clear()
        self.lyrics_table_widget.setRowCount(0)
        self.lyrics_table_widget.setEnabled(False)
        self.stacked_widget.setCurrentIndex(2)
        song = {}
        for key, value in current_item.items():
            song[key] = value
        self.song_progress_bar.setValue(0)
        self.application.process_events()
        # Get the full song
        song = self.song_select_importer.get_song(song, self._update_song_progress)
        if not song:
            QtWidgets.QMessageBox.critical(
                self, translate('SongsPlugin.SongSelectForm', 'Incomplete song'),
                translate('SongsPlugin.SongSelectForm', 'This song is missing some information, like the lyrics, '
                                                        'and cannot be imported.'),
                QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok), QtWidgets.QMessageBox.Ok)
            self.stacked_widget.setCurrentIndex(1)
            return
        # Update the UI
        self.title_edit.setText(song['title'])
        self.copyright_edit.setText(song['copyright'])
        self.ccli_edit.setText(song['ccli_number'])
        for author in song['authors']:
            QtWidgets.QListWidgetItem(author, self.author_list_widget)
        for counter, verse in enumerate(song['verses']):
            self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
            item = QtWidgets.QTableWidgetItem(verse['lyrics'])
            item.setData(QtCore.Qt.UserRole, verse['label'])
            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
            self.lyrics_table_widget.setItem(counter, 0, item)
        self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in song['verses']])
        self.lyrics_table_widget.resizeRowsToContents()
        self.title_edit.setEnabled(True)
        self.copyright_edit.setEnabled(True)
        self.ccli_edit.setEnabled(True)
        self.author_list_widget.setEnabled(True)
        self.lyrics_table_widget.setEnabled(True)
        self.lyrics_table_widget.repaint()
        self.import_button.setEnabled(True)
        self.back_button.setEnabled(True)
        self.song_progress_bar.setVisible(False)
        self.song_progress_bar.setValue(0)
        self.song = song
        self.application.process_events()

    def on_save_password_checkbox_toggled(self, checked):
        """
        Show a warning dialog when the user toggles the save checkbox on or off.

        :param checked: If the combobox is checked or not
        """
        if checked and self.login_page.isVisible():
            answer = QtWidgets.QMessageBox.question(
                self, translate('SongsPlugin.SongSelectForm', 'Save Username and Password'),
                translate('SongsPlugin.SongSelectForm', 'WARNING: Saving your username and password is INSECURE, your '
                                                        'password is stored in PLAIN TEXT. Click Yes to save your '
                                                        'password or No to cancel this.'),
                QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
                QtWidgets.QMessageBox.No)
            if answer == QtWidgets.QMessageBox.No:
                self.save_password_checkbox.setChecked(False)

    def on_login_button_clicked(self):
        """
        Log the user in to SongSelect.
        """
        self.username_edit.setEnabled(False)
        self.password_edit.setEnabled(False)
        self.save_password_checkbox.setEnabled(False)
        self.login_button.setEnabled(False)
        self.login_spacer.setVisible(False)
        self.login_progress_bar.setValue(0)
        self.login_progress_bar.setVisible(True)
        self.application.process_events()
        # Log the user in
        if not self.song_select_importer.login(
                self.username_edit.text(), self.password_edit.text(), self._update_login_progress):
            QtWidgets.QMessageBox.critical(
                self,
                translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
                translate('SongsPlugin.SongSelectForm',
                          'There was a problem logging in, perhaps your username or password is incorrect?')
            )
        else:
            if self.save_password_checkbox.isChecked():
                Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text())
                Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text())
            else:
                Settings().remove(self.plugin.settings_section + '/songselect username')
                Settings().remove(self.plugin.settings_section + '/songselect password')
            self.stacked_widget.setCurrentIndex(1)
        self.login_progress_bar.setVisible(False)
        self.login_progress_bar.setValue(0)
        self.login_spacer.setVisible(True)
        self.login_button.setEnabled(True)
        self.username_edit.setEnabled(True)
        self.password_edit.setEnabled(True)
        self.save_password_checkbox.setEnabled(True)
        self.search_combobox.setFocus()
        self.application.process_events()

    def on_search_button_clicked(self):
        """
        Run a search on SongSelect.
        """
        # Set up UI components
        self.view_button.setEnabled(False)
        self.search_button.setEnabled(False)
        self.search_combobox.setEnabled(False)
        self.search_progress_bar.setMinimum(0)
        self.search_progress_bar.setMaximum(0)
        self.search_progress_bar.setValue(0)
        self.set_progress_visible(True)
        self.search_results_widget.clear()
        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
                                                  'Found {count:d} song(s)').format(count=self.song_count))
        self.application.process_events()
        self.song_count = 0
        search_history = self.search_combobox.getItems()
        Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))
        # Create thread and run search
        self.thread = QtCore.QThread()
        self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.start)
        self.worker.show_info.connect(self.on_search_show_info)
        self.worker.found_song.connect(self.on_search_found_song)
        self.worker.finished.connect(self.on_search_finished)
        self.worker.quit.connect(self.thread.quit)
        self.worker.quit.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.thread.start()

    def on_stop_button_clicked(self):
        """
        Stop the search when the stop button is clicked.
        """
        self.song_select_importer.stop()

    def on_search_show_info(self, title, message):
        """
        Show an informational message from the search thread
        :param title:
        :param message:
        """
        QtWidgets.QMessageBox.information(self, title, message)

    def on_search_found_song(self, song):
        """
        Add a song to the list when one is found.
        :param song:
        """
        self.song_count += 1
        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
                                                  'Found {count:d} song(s)').format(count=self.song_count))
        item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
        song_item = QtWidgets.QListWidgetItem(item_title, self.search_results_widget)
        song_item.setData(QtCore.Qt.UserRole, song)

    def on_search_finished(self):
        """
        Slot which is called when the search is completed.
        """
        self.application.process_events()
        self.set_progress_visible(False)
        self.search_button.setEnabled(True)
        self.search_combobox.setEnabled(True)
        self.application.process_events()

    def on_search_results_widget_selection_changed(self):
        """
        Enable or disable the view button when the selection changes.
        """
        self.view_button.setEnabled(len(self.search_results_widget.selectedItems()) > 0)

    def on_view_button_clicked(self):
        """
        View a song from SongSelect.
        """
        self._view_song(self.search_results_widget.currentItem())

    def on_search_results_widget_double_clicked(self, current_item):
        """
        View a song from SongSelect

        :param current_item:
        """
        self._view_song(current_item)

    def on_back_button_clicked(self):
        """
        Go back to the search page.
        """
        self.stacked_widget.setCurrentIndex(1)
        self.search_combobox.setFocus()

    def on_import_button_clicked(self):
        """
        Import a song from SongSelect.
        """
        self.song_select_importer.save_song(self.song)
        self.song = None
        if QtWidgets.QMessageBox.question(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
                                          translate('SongsPlugin.SongSelectForm',
                                                    'Your song has been imported, would you '
                                                    'like to import more songs?'),
                                          QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                                          QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
            self.on_back_button_clicked()
        else:
            self.application.process_events()
            self.done(QtWidgets.QDialog.Accepted)

    def set_progress_visible(self, is_visible):
        """
        Show or hide the search progress, including the stop button.
        """
        self.search_progress_bar.setVisible(is_visible)
        self.stop_button.setVisible(is_visible)

    @property
    def application(self):
        """
        Adds the openlp to the class dynamically.
        Windows needs to access the application in a dynamic manner.
        """
        if is_win():
            return Registry().get('application')
        else:
            if not hasattr(self, '_application'):
                self._application = Registry().get('application')
            return self._application