← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~phill-ridout/openlp/saved_bible_verses into lp:openlp

 

Phill has proposed merging lp:~phill-ridout/openlp/saved_bible_verses into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)

For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/saved_bible_verses/+merge/324674

Merged in trunk

Adds more flexibility to 'locking' bible verses.
fixes #1625681 "If Bible search results are locked, duplicated results are listed"


lp:~phill-ridout/openlp/saved_bible_verses (revision 2746)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2030/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1940/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1871/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1251/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1105/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/234/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/81/
Stopping after failure
-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/lib/listwidgetwithdnd.py' (properties changed: -x to +x)
--- openlp/core/ui/lib/listwidgetwithdnd.py	2017-02-18 07:23:15 +0000
+++ openlp/core/ui/lib/listwidgetwithdnd.py	2017-05-26 13:55:01 +0000
@@ -44,7 +44,6 @@
         self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
         self.setAlternatingRowColors(True)
         self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
-        self.locked = False
 
     def activateDnD(self):
         """
@@ -54,15 +53,13 @@
         self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
         Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
 
-    def clear(self, search_while_typing=False, override_lock=False):
+    def clear(self, search_while_typing=False):
         """
         Re-implement clear, so that we can customise feedback when using 'Search as you type'
 
         :param search_while_typing: True if we want to display the customised message
         :return: None
         """
-        if self.locked and not override_lock:
-            return
         if search_while_typing:
             self.no_results_text = UiStrings().ShortResults
         else:
@@ -128,6 +125,15 @@
         else:
             event.ignore()
 
+    def allItems(self):
+        """
+        An generator to list all the items in the widget
+
+        :return: a generator
+        """
+        for row in range(self.count()):
+            yield self.item(row)
+
     def paintEvent(self, event):
         """
         Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.

=== modified file 'openlp/core/ui/lib/pathedit.py'
--- openlp/core/ui/lib/pathedit.py	2017-05-22 18:22:43 +0000
+++ openlp/core/ui/lib/pathedit.py	2017-05-26 13:55:01 +0000
@@ -46,16 +46,16 @@
 
         :param parent: The parent of the widget. This is just passed to the super method.
         :type parent: QWidget or None
-        
+
         :param dialog_caption: Used to customise the caption in the QFileDialog.
         :param dialog_caption: str
-        
+
         :param default_path: The default path. This is set as the path when the revert button is clicked
         :type default_path: str
 
         :param show_revert: Used to determin if the 'revert button' should be visible.
         :type show_revert: bool
-        
+
         :return: None
         :rtype: None
         """
@@ -72,7 +72,7 @@
         Set up the widget
         :param show_revert: Show or hide the revert button
         :type show_revert: bool
-        
+
         :return: None
         :rtype: None
         """

=== modified file 'openlp/plugins/bibles/lib/__init__.py'
--- openlp/plugins/bibles/lib/__init__.py	2017-02-18 07:23:15 +0000
+++ openlp/plugins/bibles/lib/__init__.py	2017-05-26 13:55:01 +0000
@@ -341,10 +341,10 @@
         if not book_ref_id:
             book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
         elif not bible.get_book_by_book_ref_id(book_ref_id):
-            return False
+            return []
         # We have not found the book so do not continue
         if not book_ref_id:
-            return False
+            return []
         ranges = match.group('ranges')
         range_list = get_reference_match('range_separator').split(ranges)
         ref_list = []
@@ -403,7 +403,7 @@
         return ref_list
     else:
         log.debug('Invalid reference: {text}'.format(text=reference))
-        return None
+        return []
 
 
 class SearchResults(object):

=== modified file 'openlp/plugins/bibles/lib/db.py'
--- openlp/plugins/bibles/lib/db.py	2017-02-18 07:23:15 +0000
+++ openlp/plugins/bibles/lib/db.py	2017-05-26 13:55:01 +0000
@@ -158,6 +158,7 @@
                 self.get_name()
         if 'path' in kwargs:
             self.path = kwargs['path']
+        self._is_web_bible = None
 
     def get_name(self):
         """
@@ -426,6 +427,18 @@
             return 0
         return count
 
+    @property
+    def is_web_bible(self):
+        """
+        A read only property indicating if the bible is a 'web bible'
+
+        :return: If the bible is a web bible.
+        :rtype: bool
+        """
+        if self._is_web_bible is None:
+            self._is_web_bible = bool(self.get_object(BibleMeta, 'download_source'))
+        return self._is_web_bible
+
     def dump_bible(self):
         """
         Utility debugging method to dump the contents of a bible.

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2017-03-15 19:51:10 +0000
+++ openlp/plugins/bibles/lib/manager.py	2017-05-26 13:55:01 +0000
@@ -142,8 +142,8 @@
             log.debug('Bible Name: "{name}"'.format(name=name))
             self.db_cache[name] = bible
             # Look to see if lazy load bible exists and get create getter.
-            source = self.db_cache[name].get_object(BibleMeta, 'download_source')
-            if source:
+            if self.db_cache[name].is_web_bible:
+                source = self.db_cache[name].get_object(BibleMeta, 'download_source')
                 download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value
                 meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server')
                 web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value,
@@ -278,7 +278,7 @@
         :param show_error:
         """
         if not bible or not ref_list:
-            return None
+            return []
         return self.db_cache[bible].get_verses(ref_list, show_error)
 
     def get_language_selection(self, bible):
@@ -305,11 +305,17 @@
         """
         Does a verse search for the given bible and text.
 
-        :param bible: The bible to search in (unicode).
-        :param second_bible: The second bible (unicode). We do not search in this bible.
-        :param text: The text to search for (unicode).
+        :param bible: The bible to search
+        :type bible: str
+        :param text: The text to search for
+        :type text: str
+
+        :return: The search results if valid, or None if the search is invalid.
+        :rtype: None, list
         """
         log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
+        if not text:
+            return None
         # If no bibles are installed, message is given.
         if not bible:
             self.main_window.information_message(
@@ -317,8 +323,7 @@
                 UiStrings().BibleNoBibles)
             return None
         # Check if the bible or second_bible is a web bible.
-        web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
-        if web_bible:
+        if self.db_cache[bible].is_web_bible:
             # If either Bible is Web, cursor is reset to normal and message is given.
             self.application.set_normal_cursor()
             self.main_window.information_message(
@@ -328,41 +333,8 @@
                                                        'This means that the currently selected Bible is a Web Bible.')
             )
             return None
-        # Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked.
-        if len(text) - text.count(' ') < 3:
-            return None
-        # Fetch the results from db. If no results are found, return None, no message is given for this.
-        elif text:
-            return self.db_cache[bible].verse_search(text)
-        else:
-            return None
-
-    def verse_search_while_typing(self, bible, second_bible, text):
-        """
-        Does a verse search for the given bible and text.
-        This is used during "Search while typing"
-        It's the same thing as the normal text search, but it does not show the web Bible error.
-        (It would result in the error popping every time a char is entered or removed)
-        It also does not have a minimum text len, this is set in mediaitem.py
-
-        :param bible: The bible to search in (unicode).
-        :param second_bible: The second bible (unicode). We do not search in this bible.
-        :param text: The text to search for (unicode).
-        """
-        # If no bibles are installed, message is given.
-        if not bible:
-            return None
-        # Check if the bible or second_bible is a web bible.
-        web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
-        second_web_bible = ''
-        if second_bible:
-            second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source')
-        if web_bible or second_web_bible:
-            # If either Bible is Web, cursor is reset to normal and search ends w/o any message.
-            self.application.set_normal_cursor()
-            return None
-        # Fetch the results from db. If no results are found, return None, no message is given for this.
-        elif text:
+        # Fetch the results from db. If no results are found, return None, no message is given for this.
+        if text:
             return self.db_cache[bible].verse_search(text)
         else:
             return None

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py' (properties changed: -x to +x)
--- openlp/plugins/bibles/lib/mediaitem.py	2017-02-18 20:22:47 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2017-05-26 13:55:01 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import re
+from enum import IntEnum, unique
 
 from PyQt5 import QtCore, QtWidgets
 
@@ -48,15 +49,45 @@
             'list': get_reference_separator('sep_l_display')}
 
 
-class BibleSearch(object):
+@unique
+class BibleSearch(IntEnum):
     """
-    Enumeration class for the different search methods for the "Search" tab.
+    Enumeration class for the different search types for the "Search" tab.
     """
     Reference = 1
     Text = 2
     Combined = 3
 
 
+@unique
+class ResultsTab(IntEnum):
+    """
+    Enumeration class for the different tabs for the results list.
+    """
+    Saved = 0
+    Search = 1
+
+
+@unique
+class SearchStatus(IntEnum):
+    """
+    Enumeration class for the different search methods.
+    """
+    SearchButton = 0
+    SearchAsYouType = 1
+    NotEnoughText = 2
+
+
+@unique
+class SearchTabs(IntEnum):
+    """
+    Enumeration class for the tabs on the media item.
+    """
+    Search = 0
+    Select = 1
+    Options = 2
+
+
 class BibleMediaItem(MediaManagerItem):
     """
     This is the custom media manager item for Bibles.
@@ -73,11 +104,13 @@
         :param kwargs: Keyword arguments to pass to the super method. (dict)
         """
         self.clear_icon = build_icon(':/bibles/bibles_search_clear.png')
-        self.lock_icon = build_icon(':/bibles/bibles_search_lock.png')
-        self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png')
+        self.save_results_icon = build_icon(':/bibles/bibles_save_results.png')
         self.sort_icon = build_icon(':/bibles/bibles_book_sort.png')
         self.bible = None
         self.second_bible = None
+        self.saved_results = []
+        self.current_results = []
+        self.search_status = SearchStatus.SearchButton
         # TODO: Make more central and clean up after!
         self.search_timer = QtCore.QTimer()
         self.search_timer.setInterval(200)
@@ -162,8 +195,10 @@
         self.select_tab.setVisible(False)
         self.page_layout.addWidget(self.select_tab)
         # General Search Opions
-        self.options_widget = QtWidgets.QGroupBox(translate('BiblesPlugin.MediaItem', 'Options'), self)
-        self.general_bible_layout = QtWidgets.QFormLayout(self.options_widget)
+        self.options_tab = QtWidgets.QWidget()
+        self.options_tab.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+        self.search_tab_bar.addTab(translate('BiblesPlugin.MediaItem', 'Options'))
+        self.general_bible_layout = QtWidgets.QFormLayout(self.options_tab)
         self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box')
         self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box)
         self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box')
@@ -171,20 +206,28 @@
         self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box')
         self.style_combo_box.addItems(['', '', ''])
         self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box)
-        self.search_button_layout = QtWidgets.QHBoxLayout()
+        self.options_tab.setVisible(False)
+        self.page_layout.addWidget(self.options_tab)
+        # This widget is the easier way to reset the spacing of search_button_layout. (Because page_layout has had its
+        # spacing set to 0)
+        self.search_button_widget = QtWidgets.QWidget()
+        self.search_button_layout = QtWidgets.QHBoxLayout(self.search_button_widget)
         self.search_button_layout.addStretch()
         # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon.
-        self.clear_button = QtWidgets.QToolButton(self)
+        self.clear_button = QtWidgets.QPushButton()
         self.clear_button.setIcon(self.clear_icon)
-        self.lock_button = QtWidgets.QToolButton(self)
-        self.lock_button.setIcon(self.unlock_icon)
-        self.lock_button.setCheckable(True)
+        self.save_results_button = QtWidgets.QPushButton()
+        self.save_results_button.setIcon(self.save_results_icon)
         self.search_button_layout.addWidget(self.clear_button)
-        self.search_button_layout.addWidget(self.lock_button)
+        self.search_button_layout.addWidget(self.save_results_button)
         self.search_button = QtWidgets.QPushButton(self)
         self.search_button_layout.addWidget(self.search_button)
-        self.general_bible_layout.addRow(self.search_button_layout)
-        self.page_layout.addWidget(self.options_widget)
+        self.page_layout.addWidget(self.search_button_widget)
+        self.results_view_tab = QtWidgets.QTabBar(self)
+        self.results_view_tab.addTab('')
+        self.results_view_tab.addTab('')
+        self.results_view_tab.setCurrentIndex(ResultsTab.Search)
+        self.page_layout.addWidget(self.results_view_tab)
 
     def setupUi(self):
         super().setupUi()
@@ -211,12 +254,15 @@
         # Buttons
         self.book_order_button.toggled.connect(self.on_book_order_button_toggled)
         self.clear_button.clicked.connect(self.on_clear_button_clicked)
-        self.lock_button.toggled.connect(self.on_lock_button_toggled)
+        self.save_results_button.clicked.connect(self.on_save_results_button_clicked)
         self.search_button.clicked.connect(self.on_search_button_clicked)
         # Other stuff
         self.search_edit.returnPressed.connect(self.on_search_button_clicked)
         self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed)
+        self.results_view_tab.currentChanged.connect(self.on_results_view_tab_current_changed)
         self.search_edit.textChanged.connect(self.on_search_edit_text_changed)
+        self.on_results_view_tab_total_update(ResultsTab.Saved)
+        self.on_results_view_tab_total_update(ResultsTab.Search)
 
     def retranslateUi(self):
         log.debug('retranslateUi')
@@ -225,9 +271,9 @@
         self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
         self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
         self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
-        self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.'))
-        self.lock_button.setToolTip(
-            translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.'))
+        self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the results on the current tab.'))
+        self.save_results_button.setToolTip(
+            translate('BiblesPlugin.MediaItem', 'Add the search results to the saved list.'))
         self.search_button.setText(UiStrings().Search)
 
     def on_focus(self):
@@ -241,8 +287,10 @@
         if self.search_tab.isVisible():
             self.search_edit.setFocus()
             self.search_edit.selectAll()
-        else:
+        if self.select_tab.isVisible():
             self.select_book_combo_box.setFocus()
+        if self.options_tab.isVisible():
+            self.version_combo_box.setFocus()
 
     def config_update(self):
         """
@@ -415,14 +463,48 @@
         """
         Show the selected tab and set focus to it
 
-        :param index: The tab selected (int)
+        :param index: The tab selected
+        :type index: int
         :return: None
         """
-        search_tab = index == 0
-        self.search_tab.setVisible(search_tab)
-        self.select_tab.setVisible(not search_tab)
+        if index == SearchTabs.Search or index == SearchTabs.Select:
+            self.search_button.setEnabled(True)
+        else:
+            self.search_button.setEnabled(False)
+        self.search_tab.setVisible(index == SearchTabs.Search)
+        self.select_tab.setVisible(index == SearchTabs.Select)
+        self.options_tab.setVisible(index == SearchTabs.Options)
         self.on_focus()
 
+    def on_results_view_tab_current_changed(self, index):
+        """
+        Update list_widget with the contents of the selected list
+
+        :param index: The index of the tab that has been changed to. (int)
+        :return: None
+        """
+        if index == ResultsTab.Saved:
+            self.add_built_results_to_list_widget(self.saved_results)
+        elif index == ResultsTab.Search:
+            self.add_built_results_to_list_widget(self.current_results)
+
+    def on_results_view_tab_total_update(self, index):
+        """
+        Update the result total count on the tab with the given index.
+
+        :param index: Index of the tab to update (int)
+        :return: None
+        """
+        string = ''
+        count = 0
+        if index == ResultsTab.Saved:
+            string = translate('BiblesPlugin.MediaItem', 'Saved ({result_count})')
+            count = len(self.saved_results)
+        elif index == ResultsTab.Search:
+            string = translate('BiblesPlugin.MediaItem', 'Results ({result_count})')
+            count = len(self.current_results)
+        self.results_view_tab.setTabText(index, string.format(result_count=count))
+
     def on_book_order_button_toggled(self, checked):
         """
         Change the sort order of the book names
@@ -442,22 +524,25 @@
 
         :return: None
         """
-        self.list_view.clear()
-        self.search_edit.clear()
-        self.on_focus()
+        current_index = self.results_view_tab.currentIndex()
+        for item in self.list_view.selectedItems():
+            self.list_view.takeItem(self.list_view.row(item))
+        results = [item.data(QtCore.Qt.UserRole) for item in self.list_view.allItems()]
+        if current_index == ResultsTab.Saved:
+            self.saved_results = results
+        elif current_index == ResultsTab.Search:
+            self.current_results = results
+        self.on_results_view_tab_total_update(current_index)
 
-    def on_lock_button_toggled(self, checked):
+    def on_save_results_button_clicked(self):
         """
-        Toggle the lock button, if Search tab is used, set focus to search field.
+        Add the selected verses to the saved_results list.
 
-        :param checked: The state of the toggle button. (bool)
         :return: None
         """
-        self.list_view.locked = checked
-        if checked:
-            self.sender().setIcon(self.lock_icon)
-        else:
-            self.sender().setIcon(self.unlock_icon)
+        for verse in self.list_view.selectedItems():
+            self.saved_results.append(verse.data(QtCore.Qt.UserRole))
+        self.on_results_view_tab_total_update(ResultsTab.Saved)
 
     def on_style_combo_box_index_changed(self, index):
         """
@@ -490,16 +575,17 @@
         :return: None
         """
         new_selection = self.second_combo_box.currentData()
-        if self.list_view.count():
+        if self.saved_results:
             # Exclusive or (^) the new and previous selections to detect if the user has switched between single and
             # dual bible mode
             if (new_selection is None) ^ (self.second_bible is None):
                 if critical_error_message_box(
                     message=translate('BiblesPlugin.MediaItem',
                                       'OpenLP cannot combine single and dual Bible verse search results. '
-                                      'Do you want to clear your search results and start a new search?'),
+                                      'Do you want to clear your saved results?'),
                         parent=self, question=True) == QtWidgets.QMessageBox.Yes:
-                    self.list_view.clear(override_lock=True)
+                    self.saved_results = []
+                    self.on_results_view_tab_total_update(ResultsTab.Saved)
                 else:
                     self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible))
                     return
@@ -525,7 +611,8 @@
             log.warning('Not enough chapters in %s', book_ref_id)
             critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.'))
         else:
-            self.search_button.setEnabled(True)
+            if self.select_tab.isVisible():
+                self.search_button.setEnabled(True)
             self.adjust_combo_box(1, self.chapter_count, self.from_chapter)
             self.adjust_combo_box(1, self.chapter_count, self.to_chapter)
             self.adjust_combo_box(1, verse_count, self.from_verse)
@@ -602,6 +689,8 @@
 
         :return: None
         """
+        self.search_timer.stop()
+        self.search_status = SearchStatus.SearchButton
         if not self.bible:
             self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles)
             return
@@ -613,6 +702,7 @@
         elif self.select_tab.isVisible():
             self.select_search()
         self.search_button.setEnabled(True)
+        self.results_view_tab.setCurrentIndex(ResultsTab.Search)
         self.application.set_normal_cursor()
 
     def select_search(self):
@@ -636,18 +726,21 @@
 
         :return: None
         """
+        self.search_results = []
         verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text)
         self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True)
         if self.second_bible and self.search_results:
             self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True)
         self.display_results()
 
-    def on_text_search(self, text, search_while_type=False):
+    def on_text_search(self, text):
         """
         We are doing a 'Text Search'.
         This search is called on def text_search by 'Search' Text and Combined Searches.
         """
         self.search_results = self.plugin.manager.verse_search(self.bible.name, text)
+        if self.search_results is None:
+            return
         if self.second_bible and self.search_results:
             filtered_search_results = []
             not_found_count = 0
@@ -663,7 +756,7 @@
                         verse=verse.verse, bible_name=self.second_bible.name))
                     not_found_count += 1
             self.search_results = filtered_search_results
-            if not_found_count != 0 and not search_while_type:
+            if not_found_count != 0 and self.search_status == SearchStatus.SearchButton:
                 self.main_window.information_message(
                     translate('BiblesPlugin.MediaItem', 'Verses not found'),
                     translate('BiblesPlugin.MediaItem',
@@ -673,22 +766,23 @@
                               ).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count))
         self.display_results()
 
-    def text_search(self, search_while_type=False):
+    def text_search(self):
         """
         This triggers the proper 'Search' search based on which search type is used.
         "Eg. "Reference Search", "Text Search" or "Combined search".
         """
+        self.search_results = []
         log.debug('text_search called')
         text = self.search_edit.text()
         if text == '':
-            self.list_view.clear()
+            self.display_results()
             return
-        self.list_view.clear(search_while_typing=search_while_type)
+        self.on_results_view_tab_total_update(ResultsTab.Search)
         if self.search_edit.current_search_type() == BibleSearch.Reference:
             if get_reference_match('full').match(text):
                 # Valid reference found. Do reference search.
                 self.text_reference_search(text)
-            elif not search_while_type:
+            elif self.search_status == SearchStatus.SearchButton:
                 self.main_window.information_message(
                     translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
                     translate('BiblesPlugin.BibleManager',
@@ -700,10 +794,12 @@
                 self.text_reference_search(text)
         else:
             # It can only be a 'Combined' search without a valid reference, or a 'Text' search
-            if search_while_type:
-                if len(text) > 8 and VALID_TEXT_SEARCH.search(text):
-                    self.on_text_search(text, True)
-            elif VALID_TEXT_SEARCH.search(text):
+            if self.search_status == SearchStatus.SearchAsYouType:
+                if len(text) <= 8:
+                    self.search_status = SearchStatus.NotEnoughText
+                    self.display_results()
+                    return
+            if VALID_TEXT_SEARCH.search(text):
                 self.on_text_search(text)
 
     def on_search_edit_text_changed(self):
@@ -713,9 +809,12 @@
 
         :return: None
         """
-        if Settings().value('bibles/is search while typing enabled'):
-            if not self.search_timer.isActive():
-                self.search_timer.start()
+        if not Settings().value('bibles/is search while typing enabled') or \
+                not self.bible or self.bible.is_web_bible or \
+                (self.second_bible and self.bible.is_web_bible):
+            return
+        if not self.search_timer.isActive():
+            self.search_timer.start()
 
     def on_search_timer_timeout(self):
         """
@@ -724,7 +823,9 @@
 
         :return: None
         """
-        self.text_search(True)
+        self.search_status = SearchStatus.SearchAsYouType
+        self.text_search()
+        self.results_view_tab.setCurrentIndex(ResultsTab.Search)
 
     def display_results(self):
         """
@@ -732,14 +833,16 @@
 
         :return: None
         """
-        self.list_view.clear()
-        if self.search_results:
-            items = self.build_display_results(self.bible, self.second_bible, self.search_results)
-            for item in items:
-                self.list_view.addItem(item)
-            self.list_view.selectAll()
+        self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
         self.search_results = []
-        self.second_search_results = []
+        self.add_built_results_to_list_widget(self.current_results)
+
+    def add_built_results_to_list_widget(self, results):
+        self.list_view.clear(self.search_status == SearchStatus.NotEnoughText)
+        for item in self.build_list_widget_items(results):
+            self.list_view.addItem(item)
+        self.list_view.selectAll()
+        self.on_results_view_tab_total_update(ResultsTab.Search)
 
     def build_display_results(self, bible, second_bible, search_results):
         """
@@ -789,10 +892,17 @@
                 bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})'
             else:
                 bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})'
-            bible_verse = QtWidgets.QListWidgetItem(bible_text.format(sep=verse_separator, **data))
+            data['item_title'] = bible_text.format(sep=verse_separator, **data)
+            items.append(data)
+        return items
+
+    def build_list_widget_items(self, items):
+        list_widget_items = []
+        for data in items:
+            bible_verse = QtWidgets.QListWidgetItem(data['item_title'])
             bible_verse.setData(QtCore.Qt.UserRole, data)
-            items.append(bible_verse)
-        return items
+            list_widget_items.append(bible_verse)
+        return list_widget_items
 
     def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
                             context=ServiceItemContext.Service):
@@ -897,6 +1007,8 @@
         """
         Search for some Bible verses (by reference).
         """
+        if self.bible is None:
+            return []
         reference = self.plugin.manager.parse_ref(self.bible.name, string)
         search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError)
         if search_results:
@@ -908,6 +1020,9 @@
         """
         Create a media item from an item id.
         """
+        if self.bible is None:
+            return []
         reference = self.plugin.manager.parse_ref(self.bible.name, item_id)
         search_results = self.plugin.manager.get_verses(self.bible.name, reference, False)
-        return self.build_display_results(self.bible, None, search_results)
+        items = self.build_display_results(self.bible, None, search_results)
+        return self.build_list_widget_items(items)

=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2017-05-22 18:27:40 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2017-05-26 13:55:01 +0000
@@ -1,4 +1,4 @@
-    # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
 
 ###############################################################################

=== modified file 'openlp/plugins/songusage/forms/songusagedetaildialog.py'
--- openlp/plugins/songusage/forms/songusagedetaildialog.py	2017-05-22 18:22:43 +0000
+++ openlp/plugins/songusage/forms/songusagedetaildialog.py	2017-05-26 13:55:01 +0000
@@ -69,7 +69,7 @@
         self.file_horizontal_layout.setSpacing(8)
         self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
         self.file_horizontal_layout.setObjectName('file_horizontal_layout')
-        self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
+        self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False)
         self.file_horizontal_layout.addWidget(self.report_path_edit)
         self.vertical_layout.addWidget(self.file_group_box)
         self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])

=== added file 'resources/images/bibles_save_results.png'
Binary files resources/images/bibles_save_results.png	1970-01-01 00:00:00 +0000 and resources/images/bibles_save_results.png	2017-05-26 13:55:01 +0000 differ
=== removed file 'resources/images/bibles_search_lock.png'
Binary files resources/images/bibles_search_lock.png	2011-05-11 23:59:37 +0000 and resources/images/bibles_search_lock.png	1970-01-01 00:00:00 +0000 differ
=== removed file 'resources/images/bibles_search_unlock.png'
Binary files resources/images/bibles_search_unlock.png	2011-05-11 23:59:37 +0000 and resources/images/bibles_search_unlock.png	1970-01-01 00:00:00 +0000 differ
=== removed file 'resources/images/network_ssl.png'
Binary files resources/images/network_ssl.png	2014-04-14 18:09:47 +0000 and resources/images/network_ssl.png	1970-01-01 00:00:00 +0000 differ
=== modified file 'resources/images/openlp-2.qrc'
--- resources/images/openlp-2.qrc	2016-12-18 14:11:31 +0000
+++ resources/images/openlp-2.qrc	2017-05-26 13:55:01 +0000
@@ -34,8 +34,7 @@
     <file>bibles_search_text.png</file>
     <file>bibles_search_reference.png</file>
     <file>bibles_search_clear.png</file>
-    <file>bibles_search_unlock.png</file>
-    <file>bibles_search_lock.png</file>
+    <file>bibles_save_results.png</file>
   </qresource>
   <qresource prefix="plugins">
     <file>plugin_alerts.png</file>
@@ -144,7 +143,6 @@
     </qresource>
   <qresource prefix="remote">
     <file>network_server.png</file>
-    <file>network_ssl.png</file>
     <file>network_auth.png</file>
   </qresource>
   <qresource prefix="songusage">
@@ -188,4 +186,4 @@
     <file>android_app_qr.png</file>
     <file>ios_app_qr.png</file>
   </qresource>
-</RCC>
\ No newline at end of file
+</RCC>

=== modified file 'tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py' (properties changed: -x to +x)
--- tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py	2017-02-18 07:23:15 +0000
+++ tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py	2017-05-26 13:55:01 +0000
@@ -23,6 +23,7 @@
 This module contains tests for the openlp.core.lib.listwidgetwithdnd module
 """
 from unittest import TestCase
+from types import GeneratorType
 
 from openlp.core.common.uistrings import UiStrings
 from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
@@ -33,37 +34,6 @@
     """
     Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class
     """
-    def test_clear_locked(self):
-        """
-        Test the clear method the list is 'locked'
-        """
-        with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
-            # GIVEN: An instance of ListWidgetWithDnD
-            widget = ListWidgetWithDnD()
-
-            # WHEN: The list is 'locked' and clear has been called
-            widget.locked = True
-            widget.clear()
-
-            # THEN: The super method should not have been called (i.e. The list not cleared)
-            self.assertFalse(mocked_clear_super_method.called)
-
-    def test_clear_overide_locked(self):
-        """
-        Test the clear method the list is 'locked', but clear is called with 'override_lock' set to True
-        """
-        with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
-            # GIVEN: An instance of ListWidgetWithDnD
-            widget = ListWidgetWithDnD()
-
-            # WHEN: The list is 'locked' and clear has been called with override_lock se to True
-            widget.locked = True
-            widget.clear(override_lock=True)
-
-            # THEN: The super method should have been called (i.e. The list is cleared regardless whether it is locked
-            #       or not)
-            mocked_clear_super_method.assert_called_once_with()
-
     def test_clear(self):
         """
         Test the clear method when called without any arguments.
@@ -90,6 +60,38 @@
         # THEN: The results text should be the 'short results' text.
         self.assertEqual(widget.no_results_text, UiStrings().ShortResults)
 
+    def test_all_items_no_list_items(self):
+        """
+        Test allItems when there are no items in the list widget
+        """
+        # GIVEN: An instance of ListWidgetWithDnD
+        widget = ListWidgetWithDnD()
+        with patch.object(widget, 'count', return_value=0), \
+                patch.object(widget, 'item', side_effect=lambda x: [][x]):
+
+            # WHEN: Calling allItems
+            result = widget.allItems()
+
+            # THEN: An instance of a Generator object should be returned. The generator should not yeild any results
+            self.assertIsInstance(result, GeneratorType)
+            self.assertEqual(list(result), [])
+
+    def test_all_items_list_items(self):
+        """
+        Test allItems when the list widget contains some items.
+        """
+        # GIVEN: An instance of ListWidgetWithDnD
+        widget = ListWidgetWithDnD()
+        with patch.object(widget, 'count', return_value=2), \
+                patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]):
+
+            # WHEN: Calling allItems
+            result = widget.allItems()
+
+            # THEN: An instance of a Generator object should be returned. The generator should not yeild any results
+            self.assertIsInstance(result, GeneratorType)
+            self.assertEqual(list(result), [5, 3])
+
     def test_paint_event(self):
         """
         Test the paintEvent method when the list is not empty

=== modified file 'tests/functional/openlp_plugins/bibles/test_mediaitem.py' (properties changed: -x to +x)
--- tests/functional/openlp_plugins/bibles/test_mediaitem.py	2017-02-18 07:23:15 +0000
+++ tests/functional/openlp_plugins/bibles/test_mediaitem.py	2017-05-26 13:55:01 +0000
@@ -31,7 +31,8 @@
 
 from openlp.core.common import Registry
 from openlp.core.lib import MediaManagerItem
-from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, get_reference_separators, VALID_TEXT_SEARCH
+from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, ResultsTab, SearchStatus, \
+    get_reference_separators, VALID_TEXT_SEARCH
 
 
 class TestBibleMediaItemModulefunctions(TestCase):
@@ -143,6 +144,7 @@
             self.media_item = BibleMediaItem(None, self.mocked_plugin)
 
         self.media_item.settings_section = 'bibles'
+        self.media_item.results_view_tab = MagicMock()
 
         self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1})
         self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2})
@@ -658,56 +660,65 @@
         # THEN: The select_book_combo_box model sort should have been reset
         self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1)
 
-    def test_on_clear_button_clicked(self):
-        """
-        Test on_clear_button_clicked
+    def test_on_clear_button_clicked_saved_tab(self):
+        """
+        Test on_clear_button_clicked when the saved tab is selected
+        """
+        # GIVEN: An instance of :class:`MediaManagerItem` and mocked out saved_tab and select_tab and a mocked out
+        #        list_view and search_edit
+        self.media_item.list_view = MagicMock()
+        self.media_item.search_edit = MagicMock()
+        self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Saved})
+        self.media_item.saved_results = ['Some', 'Results']
+        with patch.object(self.media_item, 'on_focus'):
+
+            # WHEN: Calling on_clear_button_clicked
+            self.media_item.on_clear_button_clicked()
+
+            # THEN: The list_view should be cleared
+            self.assertEqual(self.media_item.saved_results, [])
+            self.media_item.list_view.clear.assert_called_once_with()
+
+    def test_on_clear_button_clicked_search_tab(self):
+        """
+        Test on_clear_button_clicked when the search tab is selected
         """
         # GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out
         #        list_view and search_edit
         self.media_item.list_view = MagicMock()
         self.media_item.search_edit = MagicMock()
+        self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Search})
+        self.media_item.current_results = ['Some', 'Results']
         with patch.object(self.media_item, 'on_focus'):
 
             # WHEN: Calling on_clear_button_clicked
             self.media_item.on_clear_button_clicked()
 
             # THEN: The list_view and the search_edit should be cleared
+            self.assertEqual(self.media_item.current_results, [])
             self.media_item.list_view.clear.assert_called_once_with()
             self.media_item.search_edit.clear.assert_called_once_with()
 
-    def test_on_lock_button_toggled_search_tab_lock_icon(self):
-        """
-        Test that "on_lock_button_toggled" toggles the lock properly.
-        """
-        # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
-        self.media_item.list_view = MagicMock()
-        self.media_item.lock_icon = 'lock icon'
-        mocked_sender_instance = MagicMock()
-        with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
-
-            # WHEN: When the lock_button is checked
-            self.media_item.on_lock_button_toggled(True)
-
-            # THEN: list_view should be 'locked' and the lock icon set
-            self.assertTrue(self.media_item.list_view.locked)
-            mocked_sender_instance.setIcon.assert_called_once_with('lock icon')
-
-    def test_on_lock_button_toggled_unlock_icon(self):
-        """
-        Test that "on_lock_button_toggled" toggles the lock properly.
-        """
-        # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
-        self.media_item.list_view = MagicMock()
-        self.media_item.unlock_icon = 'unlock icon'
-        mocked_sender_instance = MagicMock()
-        with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
-
-            # WHEN: When the lock_button is unchecked
-            self.media_item.on_lock_button_toggled(False)
-
-            # THEN: list_view should be 'unlocked' and the unlock icon set
-            self.assertFalse(self.media_item.list_view.locked)
-            mocked_sender_instance.setIcon.assert_called_once_with('unlock icon')
+    def test_on_save_results_button_clicked(self):
+        """
+        Test that "on_save_results_button_clicked" saves the results.
+        """
+        # GIVEN: An instance of :class:`MediaManagerItem` and a mocked list_view
+        result_1 = MagicMock(**{'data.return_value': 'R1'})
+        result_2 = MagicMock(**{'data.return_value': 'R2'})
+        result_3 = MagicMock(**{'data.return_value': 'R3'})
+        self.media_item.list_view = MagicMock(**{'selectedItems.return_value': [result_1, result_2, result_3]})
+
+        with patch.object(self.media_item, 'on_results_view_tab_total_update') as \
+                mocked_on_results_view_tab_total_update:
+
+            # WHEN: When the save_results_button is clicked
+            self.media_item.on_save_results_button_clicked()
+
+            # THEN: The selected results in the list_view should be added to the 'saved_results' list. And the saved_tab
+            #       total should be updated.
+            self.assertEqual(self.media_item.saved_results, ['R1', 'R2', 'R3'])
+            mocked_on_results_view_tab_total_update.assert_called_once_with(ResultsTab.Saved)
 
     def test_on_style_combo_box_changed(self):
         """
@@ -815,7 +826,9 @@
         self.media_item.list_view = MagicMock(**{'count.return_value': 5})
         self.media_item.style_combo_box = MagicMock()
         self.media_item.select_book_combo_box = MagicMock()
+        self.media_item.search_results = ['list', 'of', 'results']
         with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
+                patch.object(self.media_item, 'display_results'), \
                 patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
                       return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
 
@@ -825,9 +838,8 @@
             self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1})
             self.media_item.on_second_combo_box_index_changed(5)
 
-            # THEN: The list_view should be cleared and the selected bible should be set as the current bible
+            # THEN: The selected bible should be set as the current bible
             self.assertTrue(mocked_critical_error_message_box.called)
-            self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
             self.media_item.style_combo_box.setEnabled.assert_called_once_with(False)
             self.assertTrue(mocked_initialise_advanced_bible.called)
             self.assertEqual(self.media_item.second_bible, self.mocked_bible_1)
@@ -841,7 +853,9 @@
         self.media_item.list_view = MagicMock(**{'count.return_value': 5})
         self.media_item.style_combo_box = MagicMock()
         self.media_item.select_book_combo_box = MagicMock()
+        self.media_item.search_results = ['list', 'of', 'results']
         with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
+                patch.object(self.media_item, 'display_results'), \
                 patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
                       return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
             # WHEN: The previously is a bible new selection is None and the user selects yes
@@ -850,9 +864,8 @@
             self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None})
             self.media_item.on_second_combo_box_index_changed(0)
 
-            # THEN: The list_view should be cleared and the selected bible should be set as the current bible
+            # THEN: The selected bible should be set as the current bible
             self.assertTrue(mocked_critical_error_message_box.called)
-            self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
             self.media_item.style_combo_box.setEnabled.assert_called_once_with(True)
             self.assertFalse(mocked_initialise_advanced_bible.called)
             self.assertEqual(self.media_item.second_bible, None)
@@ -1388,8 +1401,9 @@
             # WHEN: Calling on_search_timer_timeout
             self.media_item.on_search_timer_timeout()
 
-            # THEN: The text_search method should have been called with True
-            mocked_text_search.assert_called_once_with(True)
+            # THEN: The search_status should be set to SearchAsYouType and text_search should have been called
+            self.assertEqual(self.media_item.search_status, SearchStatus().SearchAsYouType)
+            mocked_text_search.assert_called_once_with()
 
     def test_display_results_no_results(self):
         """
@@ -1407,7 +1421,6 @@
             self.media_item.display_results()
 
             # THEN: No items should be added to the list
-            self.media_item.list_view.clear.assert_called_once_with()
             self.assertFalse(self.media_item.list_view.addItem.called)
 
     def test_display_results_results(self):
@@ -1415,7 +1428,10 @@
         Test the display_results method when there are items to display
         """
         # GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results
-        with patch.object(self.media_item, 'build_display_results', return_value=['list', 'items']):
+        with patch.object(self.media_item, 'build_display_results', return_value=[
+            {'item_title': 'Title 1'}, {'item_title': 'Title 2'}]), \
+            patch.object(self.media_item, 'add_built_results_to_list_widget') as \
+                mocked_add_built_results_to_list_widget:
             self.media_item.search_results = ['results']
             self.media_item.list_view = MagicMock()
 
@@ -1423,5 +1439,5 @@
             self.media_item.display_results()
 
             # THEN: addItem should have been with the display items
-            self.media_item.list_view.clear.assert_called_once_with()
-            self.assertEqual(self.media_item.list_view.addItem.call_args_list, [call('list'), call('items')])
+            mocked_add_built_results_to_list_widget.assert_called_once_with(
+                [{'item_title': 'Title 1'}, {'item_title': 'Title 2'}])

=== modified file 'tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py'
--- tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py	2017-04-24 05:17:55 +0000
+++ tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py	2017-05-26 13:55:01 +0000
@@ -108,7 +108,7 @@
         # WHEN asking to parse the bible reference
         results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock())
         # THEN a verse array should be returned
-        self.assertEqual(False, results, "The bible Search should return False")
+        self.assertEqual([], results, "The bible Search should return an empty list")
 
     def test_parse_reference_five(self):
         """


Follow ups