← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~suutari-olli/openlp/combined-bible-quick-search into lp:openlp

 

Azaziah has proposed merging lp:~suutari-olli/openlp/combined-bible-quick-search into lp:openlp.

Requested reviews:
  Raoul Snyman (raoul-snyman)
  Tim Bentley (trb143)
  Tomas Groth (tomasgroth)
Related bugs:
  Bug #1591480 in OpenLP: "Ending Bible Text search with "," breaks OpenLP"
  https://bugs.launchpad.net/openlp/+bug/1591480

For more details, see:
https://code.launchpad.net/~suutari-olli/openlp/combined-bible-quick-search/+merge/297699

This branch introduces following improvements to Quick Bible search:
- Combined Reference/Text search which first performs the Reference 
  (Self made icon)
  search and then moves to Text search if nothing is found.
- Added Search while typing functionality for Quick Bible search
- Possibility to use “.” when shortening Book names in Reference search.
  For an example Gen. 1 = Gen 1 = Genesis 1.
- New/Improved error messages
  (E.g. added actual example verses to Reference error)
- 3 New settings for controlling Quick search behavior
- Added a "Clear search results button" for clearing up the search results.
 (Icon:  http://www.1001freedownloads.com/free-clipart/tango-edit-clear (CC0 1.0)
- Various performance fixes for Quick search, such as:
   Prevent shorter than 3 characters long searches (not including spaces)

These currently possible bad search quarries result in LONG search times
and program instability/crashing.
---------------------------------------------------------------------------------

[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-01-Pull/1619/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-02-Functional-Tests/1530/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-03-Interface-Tests/1468/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1239/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/829/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-05a-Code_Analysis/897/
[←[1;32mSUCCESS←[1;m] https://ci.openlp.io/job/Branch-05b-Test_Coverage/765/
-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py	2016-05-14 04:24:46 +0000
+++ openlp/core/common/uistrings.py	2016-06-16 21:54:45 +0000
@@ -23,6 +23,7 @@
 The :mod:`uistrings` module provides standard strings for OpenLP.
 """
 import logging
+import itertools
 
 from openlp.core.common import translate
 
@@ -155,3 +156,30 @@
         self.View = translate('OpenLP.Ui', 'View')
         self.ViewMode = translate('OpenLP.Ui', 'View Mode')
         self.Video = translate('OpenLP.Ui', 'Video')
+        self.BibleShortSearchTitle = translate('OpenLP.Ui', 'Search is Empty or too Short')
+        self.BibleShortSearch = translate('OpenLP.Ui', '<strong>The search you have entered is empty or shorter '
+                                                       'than 3 characters long.</strong><br><br>Please try again with '
+                                                       'a longer search.')
+        self.BibleNoBiblesTitle = translate('OpenLP.Ui', 'No Bibles Available')
+        self.BibleNoBibles = translate('OpenLP.Ui', '<strong>There are no Bibles currently installed.</strong><br><br>'
+                                                    'Please use the Import Wizard to install one or more Bibles.')
+        book_chapter = translate('OpenLP.Ui', 'Book Chapter')
+        chapter = translate('OpenLP.Ui', 'Chapter')
+        verse = translate('OpenLP.Ui', 'Verse')
+        gap = ' | '
+        psalm = translate('OpenLP.Ui', 'Psalm')
+        may_shorten = translate('OpenLP.Ui', 'Book names may be shortened from full names, for an example Ps 23 = '
+                                             'Psalm 23')
+        bible_scripture_items = \
+            [book_chapter, gap, psalm, ' 23<br>',
+             book_chapter, '%(range)s', chapter, gap, psalm, ' 23%(range)s24<br>',
+             book_chapter, '%(verse)s', verse, '%(range)s', verse, gap, psalm, ' 23%(verse)s1%(range)s2<br>',
+             book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', verse, '%(range)s', verse, gap, psalm,
+             ' 23%(verse)s1%(range)s2%(list)s5%(range)s6<br>',
+             book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', chapter, '%(verse)s', verse, '%(range)s',
+             verse, gap, psalm, ' 23%(verse)s1%(range)s2%(list)s24%(verse)s1%(range)s3<br>',
+             book_chapter, '%(verse)s', verse, '%(range)s', chapter, '%(verse)s', verse, gap, psalm,
+             ' 23%(verse)s1%(range)s24%(verse)s1<br><br>', may_shorten]
+        itertools.chain.from_iterable(itertools.repeat(strings, 1) if isinstance(strings, str)
+                                      else strings for strings in bible_scripture_items)
+        self.BibleScriptureError = ''.join(str(joined) for joined in bible_scripture_items)

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2016-05-17 13:15:53 +0000
+++ openlp/core/lib/mediamanageritem.py	2016-06-16 21:54:45 +0000
@@ -651,6 +651,20 @@
         item.setFont(font)
         self.list_view.addItem(item)
 
+    def check_search_result_search_while_typing_short(self):
+        """
+        This is used in Bible "Search while typing" if the search is shorter than the min required len.
+        """
+        if self.list_view.count():
+            return
+        message = translate('OpenLP.MediaManagerItem', 'Search is too short to be used in: "Search while typing"')
+        item = QtWidgets.QListWidgetItem(message)
+        item.setFlags(QtCore.Qt.NoItemFlags)
+        font = QtGui.QFont()
+        font.setItalic(True)
+        item.setFont(font)
+        self.list_view.addItem(item)
+
     def _get_id_of_item_to_generate(self, item, remote_item):
         """
         Utility method to check items being submitted for slide generation.

=== modified file 'openlp/plugins/bibles/bibleplugin.py'
--- openlp/plugins/bibles/bibleplugin.py	2016-03-31 16:34:22 +0000
+++ openlp/plugins/bibles/bibleplugin.py	2016-06-16 21:54:45 +0000
@@ -41,7 +41,8 @@
     'bibles/db password': '',
     'bibles/db hostname': '',
     'bibles/db database': '',
-    'bibles/last search type': BibleSearch.Reference,
+    'bibles/last search type': BibleSearch.Combined,
+    'bibles/reset to combined quick search': True,
     'bibles/verse layout style': LayoutStyle.VersePerSlide,
     'bibles/book name language': LanguageSelection.Bible,
     'bibles/display brackets': DisplayStyle.NoBrackets,
@@ -59,7 +60,9 @@
     'bibles/range separator': '',
     'bibles/list separator': '',
     'bibles/end separator': '',
-    'bibles/last directory import': ''
+    'bibles/last directory import': '',
+    'bibles/hide combined quick error': False,
+    'bibles/is search while typing enabled': True
 }
 
 

=== modified file 'openlp/plugins/bibles/lib/biblestab.py'
--- openlp/plugins/bibles/lib/biblestab.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/bibles/lib/biblestab.py	2016-06-16 21:54:45 +0000
@@ -128,6 +128,20 @@
         self.language_selection_layout.addWidget(self.language_selection_label)
         self.language_selection_layout.addWidget(self.language_selection_combo_box)
         self.right_layout.addWidget(self.language_selection_group_box)
+        self.bible_quick_settings_group_box = QtWidgets.QGroupBox(self.right_column)
+        self.bible_quick_settings_group_box.setObjectName('bible_quick_settings_group_box')
+        self.right_layout.addWidget(self.bible_quick_settings_group_box)
+        self.search_settings_layout = QtWidgets.QFormLayout(self.bible_quick_settings_group_box)
+        self.search_settings_layout.setObjectName('search_settings_layout')
+        self.reset_to_combined_quick_search_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box)
+        self.reset_to_combined_quick_search_check_box.setObjectName('reset_to_combined_quick_search_check_box')
+        self.search_settings_layout.addRow(self.reset_to_combined_quick_search_check_box)
+        self.hide_combined_quick_error_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box)
+        self.hide_combined_quick_error_check_box.setObjectName('hide_combined_quick_error_check_box')
+        self.search_settings_layout.addRow(self.hide_combined_quick_error_check_box)
+        self.bible_search_while_typing_check_box = QtWidgets.QCheckBox(self.bible_quick_settings_group_box)
+        self.bible_search_while_typing_check_box.setObjectName('bible_search_while_typing_check_box')
+        self.search_settings_layout.addRow(self.bible_search_while_typing_check_box)
         self.left_layout.addStretch()
         self.right_layout.addStretch()
         # Signals and slots
@@ -151,6 +165,12 @@
         self.end_separator_line_edit.editingFinished.connect(self.on_end_separator_line_edit_finished)
         Registry().register_function('theme_update_list', self.update_theme_list)
         self.language_selection_combo_box.activated.connect(self.on_language_selection_combo_box_changed)
+        self.reset_to_combined_quick_search_check_box.stateChanged.connect(
+            self.on_reset_to_combined_quick_search_check_box_changed)
+        self.hide_combined_quick_error_check_box.stateChanged.connect(
+            self.on_hide_combined_quick_error_check_box_changed)
+        self.bible_search_while_typing_check_box.stateChanged.connect(
+            self.on_bible_search_while_typing_check_box_changed)
 
     def retranslateUi(self):
         self.verse_display_group_box.setTitle(translate('BiblesPlugin.BiblesTab', 'Verse Display'))
@@ -194,6 +214,17 @@
             LanguageSelection.Application, translate('BiblesPlugin.BiblesTab', 'Application Language'))
         self.language_selection_combo_box.setItemText(
             LanguageSelection.English, translate('BiblesPlugin.BiblesTab', 'English'))
+        self.bible_quick_settings_group_box.setTitle(translate('BiblesPlugin.BiblesTab', 'Quick Search Settings'))
+        self.reset_to_combined_quick_search_check_box.setText(translate('BiblesPlugin.BiblesTab',
+                                                                        'Reset search type to "Text or Scripture'
+                                                                        ' Reference" on startup'))
+        self.hide_combined_quick_error_check_box.setText(translate('BiblesPlugin.BiblesTab',
+                                                                   'Don\'t show error if nothing is found in "Text or '
+                                                                   'Scripture Reference"'))
+        self.bible_search_while_typing_check_box.setText(translate('BiblesPlugin.BiblesTab',
+                                                                   'Search automatically while typing (Text search must'
+                                                                   ' contain a\nminimum of {count} characters and a '
+                                                                   'space for performance reasons)').format(count='8'))
 
     def on_bible_theme_combo_box_changed(self):
         self.bible_theme = self.bible_theme_combo_box.currentText()
@@ -302,6 +333,24 @@
                 self.end_separator_line_edit.setText(get_reference_separator('sep_e_default'))
                 self.end_separator_line_edit.setPalette(self.get_grey_text_palette(True))
 
+    def on_reset_to_combined_quick_search_check_box_changed(self, check_state):
+        """
+        Event handler for the 'hide_combined_quick_error' check box
+        """
+        self.reset_to_combined_quick_search = (check_state == QtCore.Qt.Checked)
+
+    def on_hide_combined_quick_error_check_box_changed(self, check_state):
+        """
+        Event handler for the 'hide_combined_quick_error' check box
+        """
+        self.hide_combined_quick_error = (check_state == QtCore.Qt.Checked)
+
+    def on_bible_search_while_typing_check_box_changed(self, check_state):
+        """
+        Event handler for the 'hide_combined_quick_error' check box
+        """
+        self.bible_search_while_typing = (check_state == QtCore.Qt.Checked)
+
     def load(self):
         settings = Settings()
         settings.beginGroup(self.settings_section)
@@ -355,6 +404,12 @@
             self.end_separator_check_box.setChecked(True)
         self.language_selection = settings.value('book name language')
         self.language_selection_combo_box.setCurrentIndex(self.language_selection)
+        self.reset_to_combined_quick_search = settings.value('reset to combined quick search')
+        self.reset_to_combined_quick_search_check_box.setChecked(self.reset_to_combined_quick_search)
+        self.hide_combined_quick_error = settings.value('hide combined quick error')
+        self.hide_combined_quick_error_check_box.setChecked(self.hide_combined_quick_error)
+        self.bible_search_while_typing = settings.value('is search while typing enabled')
+        self.bible_search_while_typing_check_box.setChecked(self.bible_search_while_typing)
         settings.endGroup()
 
     def save(self):
@@ -386,6 +441,9 @@
         if self.language_selection != settings.value('book name language'):
             settings.setValue('book name language', self.language_selection)
             self.settings_form.register_post_process('bibles_load_list')
+        settings.setValue('reset to combined quick search', self.reset_to_combined_quick_search)
+        settings.setValue('hide combined quick error', self.hide_combined_quick_error)
+        settings.setValue('is search while typing enabled', self.bible_search_while_typing)
         settings.endGroup()
         if self.tab_visited:
             self.settings_form.register_post_process('bibles_config_updated')

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2016-05-21 08:31:24 +0000
+++ openlp/plugins/bibles/lib/manager.py	2016-06-16 21:54:45 +0000
@@ -23,8 +23,8 @@
 import logging
 import os
 
-from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file
-from openlp.plugins.bibles.lib import parse_reference, get_reference_separator, LanguageSelection
+from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file, UiStrings
+from openlp.plugins.bibles.lib import parse_reference, LanguageSelection
 from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
 from .csvbible import CSVBible
 from .http import HTTPBible
@@ -267,42 +267,21 @@
             For second bible this is necessary.
         :param show_error:
         """
+        # If no bibles are installed, message is given.
         log.debug('BibleManager.get_verses("{bible}", "{verse}")'.format(bible=bible, verse=verse_text))
         if not bible:
             if show_error:
                 self.main_window.information_message(
-                    translate('BiblesPlugin.BibleManager', 'No Bibles Available'),
-                    translate('BiblesPlugin.BibleManager', 'There are no Bibles currently installed. Please use the '
-                              'Import Wizard to install one or more Bibles.')
-                )
+                    UiStrings().BibleNoBiblesTitle,
+                    UiStrings().BibleNoBibles)
             return None
+        # Get the language for books.
         language_selection = self.get_language_selection(bible)
         ref_list = parse_reference(verse_text, self.db_cache[bible], language_selection, book_ref_id)
         if ref_list:
             return self.db_cache[bible].get_verses(ref_list, show_error)
+        # If nothing is found. Message is given if this is not combined search. (defined in mediaitem.py)
         else:
-            if show_error:
-                reference_separators = {
-                    'verse': get_reference_separator('sep_v_display'),
-                    'range': get_reference_separator('sep_r_display'),
-                    'list': get_reference_separator('sep_l_display')}
-                self.main_window.information_message(
-                    translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
-                    translate('BiblesPlugin.BibleManager', 'Your scripture reference is either not supported by '
-                              'OpenLP or is invalid. Please make sure your reference '
-                              'conforms to one of the following patterns or consult the manual:\n\n'
-                              'Book Chapter\n'
-                              'Book Chapter%(range)sChapter\n'
-                              'Book Chapter%(verse)sVerse%(range)sVerse\n'
-                              'Book Chapter%(verse)sVerse%(range)sVerse%(list)sVerse'
-                              '%(range)sVerse\n'
-                              'Book Chapter%(verse)sVerse%(range)sVerse%(list)sChapter'
-                              '%(verse)sVerse%(range)sVerse\n'
-                              'Book Chapter%(verse)sVerse%(range)sChapter%(verse)sVerse',
-                              'Please pay attention to the appended "s" of the wildcards '
-                              'and refrain from translating the words inside the names in the brackets.')
-                    % reference_separators
-                )
             return None
 
     def get_language_selection(self, bible):
@@ -334,34 +313,68 @@
         :param text: The text to search for (unicode).
         """
         log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
-        if not bible:
-            self.main_window.information_message(
-                translate('BiblesPlugin.BibleManager', 'No Bibles Available'),
-                translate('BiblesPlugin.BibleManager',
-                          'There are no Bibles currently installed. Please use the Import Wizard to install one or '
-                          'more Bibles.')
-            )
-            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:
-            self.main_window.information_message(
-                translate('BiblesPlugin.BibleManager', 'Web Bible cannot be used'),
-                translate('BiblesPlugin.BibleManager', 'Text Search is not available with Web Bibles.')
-            )
-            return None
-        if text:
-            return self.db_cache[bible].verse_search(text)
-        else:
-            self.main_window.information_message(
-                translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
-                translate('BiblesPlugin.BibleManager', 'You did not enter a search keyword.\nYou can separate '
-                          'different keywords by a space to search for all of your keywords and you can separate '
-                          'them by a comma to search for one of them.')
-            )
+        # If no bibles are installed, message is given.
+        if not bible:
+            self.main_window.information_message(
+                UiStrings().BibleNoBiblesTitle,
+                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')
+        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 message is given.
+            self.application.set_normal_cursor()
+            self.main_window.information_message(
+                translate('BiblesPlugin.BibleManager', 'Web Bible cannot be used in Text Search'),
+                translate('BiblesPlugin.BibleManager', 'Text Search is not available with Web Bibles.\n'
+                                                       'Please use the Scripture Reference Search instead.\n\n'
+                                                       'This means that the currently used Bible\nor Second Bible '
+                                                       'is installed as Web Bible.\n\n'
+                                                       'If you were trying to perform a Reference search\nin Combined '
+                                                       'Search, your reference is invalid.')
+            )
+            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.check_search_result()
+            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:
+            return self.db_cache[bible].verse_search(text)
+        else:
             return None
 
     def save_meta_data(self, bible, version, copyright, permissions, book_name_language=None):

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py'
--- openlp/plugins/bibles/lib/mediaitem.py	2016-05-31 16:10:31 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2016-06-16 21:54:45 +0000
@@ -35,6 +35,7 @@
 from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
     LanguageSelection, BibleStrings
 from openlp.plugins.bibles.lib.db import BiblesResourcesDB
+import re
 
 log = logging.getLogger(__name__)
 
@@ -45,6 +46,7 @@
     """
     Reference = 1
     Text = 2
+    Combined = 3
 
 
 class BibleMediaItem(MediaManagerItem):
@@ -56,6 +58,7 @@
     log.info('Bible Media Item loaded')
 
     def __init__(self, parent, plugin):
+        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')
         MediaManagerItem.__init__(self, parent, plugin)
@@ -157,10 +160,15 @@
         search_button_layout = QtWidgets.QHBoxLayout()
         search_button_layout.setObjectName(prefix + 'search_button_layout')
         search_button_layout.addStretch()
+        clear_button = QtWidgets.QPushButton(tab)
+        clear_button.setFixedSize(22, 22)
+        clear_button.setIcon(self.clear_icon)
+        clear_button.setObjectName(prefix + 'ClearButton')
         lock_button = QtWidgets.QToolButton(tab)
         lock_button.setIcon(self.unlock_icon)
         lock_button.setCheckable(True)
         lock_button.setObjectName(prefix + 'LockButton')
+        search_button_layout.addWidget(clear_button)
         search_button_layout.addWidget(lock_button)
         search_button = QtWidgets.QPushButton(tab)
         search_button.setObjectName(prefix + 'SearchButton')
@@ -176,6 +184,7 @@
         setattr(self, prefix + 'SecondComboBox', second_combo_box)
         setattr(self, prefix + 'StyleLabel', style_label)
         setattr(self, prefix + 'StyleComboBox', style_combo_box)
+        setattr(self, prefix + 'ClearButton', clear_button)
         setattr(self, prefix + 'LockButton', lock_button)
         setattr(self, prefix + 'SearchButtonLayout', search_button_layout)
         setattr(self, prefix + 'SearchButton', search_button)
@@ -245,11 +254,14 @@
         self.quickStyleComboBox.activated.connect(self.on_quick_style_combo_box_changed)
         self.advancedStyleComboBox.activated.connect(self.on_advanced_style_combo_box_changed)
         # Buttons
+        self.advancedClearButton.clicked.connect(self.on_clear_button)
+        self.quickClearButton.clicked.connect(self.on_clear_button)
         self.advancedSearchButton.clicked.connect(self.on_advanced_search_button)
         self.quickSearchButton.clicked.connect(self.on_quick_search_button)
         # Other stuff
         self.quick_search_edit.returnPressed.connect(self.on_quick_search_button)
         self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed)
+        self.quick_search_edit.textChanged.connect(self.on_search_text_edit_changed)
 
     def on_focus(self):
         if self.quickTab.isVisible():
@@ -286,6 +298,7 @@
         self.quickStyleComboBox.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
         self.quickStyleComboBox.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
         self.quickStyleComboBox.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
+        self.quickClearButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.'))
         self.quickLockButton.setToolTip(translate('BiblesPlugin.MediaItem',
                                                   'Toggle to keep or clear the previous results.'))
         self.quickSearchButton.setText(UiStrings().Search)
@@ -300,6 +313,7 @@
         self.advancedStyleComboBox.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
         self.advancedStyleComboBox.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
         self.advancedStyleComboBox.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
+        self.advancedClearButton.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.'))
         self.advancedLockButton.setToolTip(translate('BiblesPlugin.MediaItem',
                                                      'Toggle to keep or clear the previous results.'))
         self.advancedSearchButton.setText(UiStrings().Search)
@@ -309,6 +323,9 @@
         self.plugin.manager.media = self
         self.load_bibles()
         self.quick_search_edit.set_search_types([
+            (BibleSearch.Combined, ':/bibles/bibles_search_combined.png',
+                translate('BiblesPlugin.MediaItem', 'Text or Reference'),
+                translate('BiblesPlugin.MediaItem', 'Text or Reference...')),
             (BibleSearch.Reference, ':/bibles/bibles_search_reference.png',
                 translate('BiblesPlugin.MediaItem', 'Scripture Reference'),
                 translate('BiblesPlugin.MediaItem', 'Search Scripture Reference...')),
@@ -424,18 +441,24 @@
     def update_auto_completer(self):
         """
         This updates the bible book completion list for the search field. The completion depends on the bible. It is
-        only updated when we are doing a reference search, otherwise the auto completion list is removed.
+        only updated when we are doing reference or combined search, in text search the completion list is removed.
         """
         log.debug('update_auto_completer')
-        # Save the current search type to the configuration.
-        Settings().setValue('{section}/last search type'.format(section=self.settings_section),
-                            self.quick_search_edit.current_search_type())
+        # Save the current search type to the configuration. If setting for automatically resetting the search type to
+        # Combined is enabled, use that otherwise use the currently selected search type.
+        # Note: This setting requires a restart to take effect.
+        if Settings().value(self.settings_section + '/reset to combined quick search'):
+            Settings().setValue('{section}/last search type'.format(section=self.settings_section),
+                                BibleSearch.Combined)
+        else:
+            Settings().setValue('{section}/last search type'.format(section=self.settings_section),
+                                self.quick_search_edit.current_search_type())
         # Save the current bible to the configuration.
         Settings().setValue('{section}/quick bible'.format(section=self.settings_section),
                             self.quickVersionComboBox.currentText())
         books = []
-        # We have to do a 'Reference Search'.
-        if self.quick_search_edit.current_search_type() == BibleSearch.Reference:
+        # We have to do a 'Reference Search' (Or as part of Combined Search).
+        if self.quick_search_edit.current_search_type() is not BibleSearch.Text:
             bibles = self.plugin.manager.get_bibles()
             bible = self.quickVersionComboBox.currentText()
             if bible:
@@ -525,6 +548,11 @@
             self.advancedTab.setVisible(True)
             self.advanced_book_combo_box.setFocus()
 
+    def on_clear_button(self):
+        # Clear the list, then set the "No search Results" message.
+        self.list_view.clear()
+        self.check_search_result()
+
     def on_lock_button_toggled(self, checked):
         if checked:
             self.sender().setIcon(self.lock_icon)
@@ -652,10 +680,120 @@
         self.check_search_result()
         self.application.set_normal_cursor()
 
+    def on_quick_reference_search(self):
+        """
+        We are doing a 'Reference Search'.
+        This search is called on def on_quick_search_button by Quick Reference and Combined Searches.
+        """
+        # Set Bibles to use the text input from Quick search field.
+        bible = self.quickVersionComboBox.currentText()
+        second_bible = self.quickSecondComboBox.currentText()
+        """
+        Get input from field and replace 'A-Z + . ' with ''
+        This will check if field has any '.' after A-Z and removes them. Eg. Gen. 1 = Ge 1 = Genesis 1
+        If Book name has '.' after number. eg. 1. Genesis, the search fails without the dot, and vice versa.
+        A better solution would be to make '.' optional in the search results. Current solution was easier to code.
+        """
+        text = self.quick_search_edit.text()
+        text = re.sub('\D[.]\s', ' ', text)
+        # This is triggered on reference search, use the search from manager.py
+        if self.quick_search_edit.current_search_type() != BibleSearch.Text:
+            self.search_results = self.plugin.manager.get_verses(bible, text)
+        if second_bible and self.search_results:
+            self.second_search_results = \
+                self.plugin.manager.get_verses(second_bible, text, self.search_results[0].book.book_reference_id)
+
+    def on_quick_text_search(self):
+        """
+        We are doing a 'Text Search'.
+        This search is called on def on_quick_search_button by Quick Text and Combined Searches.
+        """
+        # Set Bibles to use the text input from Quick search field.
+        bible = self.quickVersionComboBox.currentText()
+        second_bible = self.quickSecondComboBox.currentText()
+        text = self.quick_search_edit.text()
+        # If Text search ends with "," OpenLP will crash, prevent this from happening by removing all ","s.
+        text = re.sub('[,]', '', text)
+        self.application.set_busy_cursor()
+        # Get Bibles list
+        bibles = self.plugin.manager.get_bibles()
+        # Add results to "search_results"
+        self.search_results = self.plugin.manager.verse_search(bible, second_bible, text)
+        if second_bible and self.search_results:
+            # new_search_results is needed to make sure 2nd bible contains all verses. (And counting them on error)
+            text = []
+            new_search_results = []
+            count = 0
+            passage_not_found = False
+            # Search second bible for results of search_results to make sure everythigns there.
+            # Count all the unfound passages.
+            for verse in self.search_results:
+                db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id)
+                if not db_book:
+                    log.debug('Passage "{name} {chapter:d}:{verse:d}" not found in '
+                              'Second Bible'.format(name=verse.book.name, chapter=verse.chapter, verse=verse.verse))
+                    passage_not_found = True
+                    count += 1
+                    continue
+                new_search_results.append(verse)
+                text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse))
+            if passage_not_found:
+                # This is for the 2nd Bible.
+                self.main_window.information_message(
+                    translate('BiblesPlugin.MediaItem', 'Information'),
+                    translate('BiblesPlugin.MediaItem', 'The second Bible does not contain all the verses '
+                                                        'that are in the main Bible.\nOnly verses found in both Bibles'
+                                                        ' will be shown.\n\n{count:d} verses have not been included '
+                                                        'in the results.').format(count=count))
+            # Join the searches so only verses that are found on both Bibles are shown.
+            self.search_results = new_search_results
+            self.second_search_results = bibles[second_bible].get_verses(text)
+
+    def on_quick_text_search_while_typing(self):
+        """
+        We are doing a 'Text Search' while typing
+        Call the verse_search_while_typing from manager.py
+        It does not show web bible errors while typing.
+        (It would result in the error popping every time a char is entered or removed)
+        """
+        # Set Bibles to use the text input from Quick search field.
+        bible = self.quickVersionComboBox.currentText()
+        second_bible = self.quickSecondComboBox.currentText()
+        text = self.quick_search_edit.text()
+        # If Text search ends with "," OpenLP will crash, prevent this from happening by removing all ","s.
+        text = re.sub('[,]', '', text)
+        self.application.set_busy_cursor()
+        # Get Bibles list
+        bibles = self.plugin.manager.get_bibles()
+        # Add results to "search_results"
+        self.search_results = self.plugin.manager.verse_search_while_typing(bible, second_bible, text)
+        if second_bible and self.search_results:
+            # new_search_results is needed to make sure 2nd bible contains all verses. (And counting them on error)
+            text = []
+            new_search_results = []
+            count = 0
+            passage_not_found = False
+            # Search second bible for results of search_results to make sure everythigns there.
+            # Count all the unfound passages. Even thou no error is shown, this needs to be done or
+            # the code breaks later on.
+            for verse in self.search_results:
+                db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id)
+                if not db_book:
+                    log.debug('Passage ("{versebookname}","{versechapter}","{verseverse}") not found in Second Bible'
+                              .format(versebookname=verse.book.name, versechapter='verse.chapter',
+                                      verseverse=verse.verse))
+                    count += 1
+                    continue
+                new_search_results.append(verse)
+                text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse))
+            # Join the searches so only verses that are found on both Bibles are shown.
+            self.search_results = new_search_results
+            self.second_search_results = bibles[second_bible].get_verses(text)
+
     def on_quick_search_button(self):
         """
-        Does a quick search and saves the search results. Quick search can either be "Reference Search" or
-        "Text Search".
+        This triggers the proper Quick search based on which search type is used.
+        "Eg. "Reference Search", "Text Search" or "Combined search".
         """
         log.debug('Quick Search Button clicked')
         self.quickSearchButton.setEnabled(False)
@@ -664,41 +802,68 @@
         second_bible = self.quickSecondComboBox.currentText()
         text = self.quick_search_edit.text()
         if self.quick_search_edit.current_search_type() == BibleSearch.Reference:
-            # We are doing a 'Reference Search'.
-            self.search_results = self.plugin.manager.get_verses(bible, text)
-            if second_bible and self.search_results:
-                self.second_search_results = \
-                    self.plugin.manager.get_verses(second_bible, text, self.search_results[0].book.book_reference_id)
-        else:
-            # We are doing a 'Text Search'.
-            self.application.set_busy_cursor()
-            bibles = self.plugin.manager.get_bibles()
-            self.search_results = self.plugin.manager.verse_search(bible, second_bible, text)
-            if second_bible and self.search_results:
-                text = []
-                new_search_results = []
-                count = 0
-                passage_not_found = False
-                for verse in self.search_results:
-                    db_book = bibles[second_bible].get_book_by_book_ref_id(verse.book.book_reference_id)
-                    if not db_book:
-                        log.debug('Passage "{name} {chapter:d}:{verse:d}" not found in '
-                                  'Second Bible'.format(name=verse.book.name, chapter=verse.chapter, verse=verse.verse))
-                        passage_not_found = True
-                        count += 1
-                        continue
-                    new_search_results.append(verse)
-                    text.append((verse.book.book_reference_id, verse.chapter, verse.verse, verse.verse))
-                if passage_not_found:
-                    QtWidgets.QMessageBox.information(
-                        self, translate('BiblesPlugin.MediaItem', 'Information'),
-                        translate('BiblesPlugin.MediaItem',
-                                  'The second Bible does not contain all the verses that are in the main Bible. '
-                                  'Only verses found in both Bibles will be shown. {count:d} verses have not been '
-                                  'included in the results.').format(count=count),
-                        QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
-                self.search_results = new_search_results
-                self.second_search_results = bibles[second_bible].get_verses(text)
+            # We are doing a 'Reference Search'. (Get script from def on_quick_reference_search)
+            self.on_quick_reference_search()
+            # Get reference separators from settings.
+            if not self.search_results:
+                reference_separators = {
+                    'verse': get_reference_separator('sep_v_display'),
+                    'range': get_reference_separator('sep_r_display'),
+                    'list': get_reference_separator('sep_l_display')}
+                self.main_window.information_message(
+                    translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
+                    translate('BiblesPlugin.BibleManager', '<strong>OpenLP couldn’t find anything '
+                                                           'with your search.<br><br>'
+                                                           'Please make sure that your reference follows '
+                                                           'one of these patterns:</strong><br><br>%s'
+                              % UiStrings().BibleScriptureError % reference_separators))
+        elif self.quick_search_edit.current_search_type() == BibleSearch.Text:
+            # We are doing a 'Text Search'. (Get script from def on_quick_text_search)
+            self.on_quick_text_search()
+            if not self.search_results and len(text) - text.count(' ') < 3 and bible:
+                self.main_window.information_message(
+                    UiStrings().BibleShortSearchTitle,
+                    UiStrings().BibleShortSearch)
+        elif self.quick_search_edit.current_search_type() == BibleSearch.Combined:
+            # We are doing a 'Combined search'. Starting with reference search.
+            # Perform only if text contains any numbers
+            if (char.isdigit() for char in text):
+                self.on_quick_reference_search()
+            """
+            If results are found, search will be finalized.
+            This check needs to be here in order to avoid duplicate errors.
+            If keyword is shorter than 3 (not including spaces), message is given. It's actually possible to find
+            verses with less than 3 chars (Eg. G1 = Genesis 1) thus this error is not shown if any results are found.
+            if no Bibles are installed, this message is not shown - "No bibles" message is shown instead.
+            """
+            if not self.search_results and len(text) - text.count(' ') < 3 and bible:
+                self.main_window.information_message(
+                    UiStrings().BibleShortSearchTitle,
+                    UiStrings().BibleShortSearch)
+            if not self.search_results and len(text) - text.count(' ') > 2 and bible:
+                # Text search starts here if no reference was found and keyword is longer than 2.
+                #  > 2 check is required in order to avoid duplicate error messages for short keywords.
+                self.on_quick_text_search()
+                if not self.search_results and not \
+                        Settings().value(self.settings_section + '/hide combined quick error'):
+                        self.application.set_normal_cursor()
+                        # Reference separators need to be defined both, in here and on reference search,
+                        # error won't work if they are left out from one.
+                        reference_separators = {
+                            'verse': get_reference_separator('sep_v_display'),
+                            'range': get_reference_separator('sep_r_display'),
+                            'list': get_reference_separator('sep_l_display')}
+                        self.main_window.information_message(translate('BiblesPlugin.BibleManager', 'Nothing found'),
+                                                             translate('BiblesPlugin.BibleManager',
+                                                                       '<strong>OpenLP couldn’t find anything with your'
+                                                                       ' search.</strong><br><br>If you tried to search'
+                                                                       ' with Scripture Reference, please make<br> sure'
+                                                                       ' that your reference follows one of these'
+                                                                       ' patterns: <br><br>%s'
+                                                                       % UiStrings().BibleScriptureError %
+                                                                       reference_separators))
+        # Finalizing the search
+        # List is cleared if not locked, results are listed, button is set available, cursor is set to normal.
         if not self.quickLockButton.isChecked():
             self.list_view.clear()
         if self.list_view.count() != 0 and self.search_results:
@@ -709,6 +874,99 @@
         self.check_search_result()
         self.application.set_normal_cursor()
 
+    def on_quick_search_while_typing(self):
+        """
+        This function is called when "Search as you type" is enabled for Bibles.
+        It is basically the same thing as "on_quick_search_search" but all the error messages are removed.
+        This also has increased min len for text search for performance reasons.
+        For commented version, please visit def on_quick_search_button.
+        """
+        bible = self.quickVersionComboBox.currentText()
+        second_bible = self.quickSecondComboBox.currentText()
+        text = self.quick_search_edit.text()
+        if self.quick_search_edit.current_search_type() == BibleSearch.Combined:
+            # If text has no numbers, auto search limit is min 8 characters for performance reasons.
+            # If you change this value, also change it in biblestab.py (Count) in enabling search while typing.
+            if (char.isdigit() for char in text) and len(text) > 2:
+                self.on_quick_reference_search()
+            if not self.search_results and len(text) > 7:
+                self.on_quick_text_search_while_typing()
+        elif self.quick_search_edit.current_search_type() == BibleSearch.Reference:
+            self.on_quick_reference_search()
+        elif self.quick_search_edit.current_search_type() == BibleSearch.Text:
+            if len(text) > 7:
+                self.on_quick_text_search_while_typing()
+        if not self.quickLockButton.isChecked():
+            self.list_view.clear()
+        if self.list_view.count() != 0 and self.search_results:
+            self.__check_second_bible(bible, second_bible)
+        elif self.search_results:
+            self.display_results(bible, second_bible)
+        self.check_search_result()
+        self.application.set_normal_cursor()
+
+    def on_search_text_edit_changed(self):
+        """
+        If search automatically while typing is enabled, perform the search and list results when conditions are met.
+        """
+        if Settings().value('bibles/is search while typing enabled'):
+            text = self.quick_search_edit.text()
+            """
+            Use Regex for finding space + number in reference search and space + 2 characters in text search.
+            Also search for two characters (Searches require at least two sets of two characters)
+            These are used to prevent bad search queries from starting. (Long/crashing queries)
+            """
+            space_and_digit_reference = re.compile(' \d')
+            two_chars_text = re.compile('\S\S')
+            space_and_two_chars_text = re.compile(' \S\S')
+            # Turn this into a format that may be used in if statement.
+            count_space_digit_reference = space_and_digit_reference.findall(text)
+            count_two_chars_text = two_chars_text.findall(text)
+            count_spaces_two_chars_text = space_and_two_chars_text.findall(text)
+            """
+            The Limit is required for setting the proper "No items found" message.
+            "Limit" is also hard coded to on_quick_search_while_typing, it must be there to avoid bad search
+            performance. Limit 8 = Text search, 3 = Reference search.
+            """
+            limit = 8
+            if self.quick_search_edit.current_search_type() == BibleSearch.Combined:
+                if len(count_space_digit_reference) != 0:
+                    limit = 3
+            elif self.quick_search_edit.current_search_type() == BibleSearch.Reference:
+                limit = 3
+            """
+            If text is empty, clear the list.
+            else: Start by checking if the search is suitable for "Search while typing"
+            """
+            if len(text) == 0:
+                if not self.quickLockButton.isChecked():
+                    self.list_view.clear()
+                self.check_search_result()
+            else:
+                if limit == 3 and (len(text) < limit or len(count_space_digit_reference) == 0):
+                    if not self.quickLockButton.isChecked():
+                        self.list_view.clear()
+                    self.check_search_result()
+                elif (limit == 8 and (len(text) < limit or len(count_spaces_two_chars_text) == 0 or
+                                      len(count_two_chars_text) < 2)):
+                    if not self.quickLockButton.isChecked():
+                        self.list_view.clear()
+                    self.check_search_result_search_while_typing_short()
+                else:
+                    """
+                    Start search if no chars are entered or deleted for 0.2 s
+                    If no Timer is set, Text search will break the search by sending repeative search Quaries on
+                    all chars. Use the self.on_quick_search_while_typing, this does not contain any error messages.
+                    """
+                    self.search_timer = ()
+                    if self.search_timer:
+                        self.search_timer.stop()
+                        self.search_timer.deleteLater()
+                    self.search_timer = QtCore.QTimer()
+                    self.search_timer.timeout.connect(self.on_quick_search_while_typing)
+                    self.search_timer.setSingleShot(True)
+                    self.search_timer.start(200)
+
     def display_results(self, bible, second_bible=''):
         """
         Displays the search results in the media manager. All data needed for further action is saved for/in each row.

=== added file 'resources/images/bibles_search_clear.png'
Binary files resources/images/bibles_search_clear.png	1970-01-01 00:00:00 +0000 and resources/images/bibles_search_clear.png	2016-06-16 21:54:45 +0000 differ
=== added file 'resources/images/bibles_search_combined.png'
Binary files resources/images/bibles_search_combined.png	1970-01-01 00:00:00 +0000 and resources/images/bibles_search_combined.png	2016-06-16 21:54:45 +0000 differ
=== modified file 'resources/images/openlp-2.qrc'
--- resources/images/openlp-2.qrc	2016-04-18 05:35:21 +0000
+++ resources/images/openlp-2.qrc	2016-06-16 21:54:45 +0000
@@ -30,9 +30,11 @@
     <file>image_new_group.png</file>
   </qresource>
   <qresource prefix="bibles">
+    <file>bibles_search_combined.png</file>
     <file>bibles_search_text.png</file>
     <file>bibles_search_reference.png</file>
     <file>bibles_upgrade_alert.png</file>
+    <file>bibles_search_clear.png</file>
     <file>bibles_search_unlock.png</file>
     <file>bibles_search_lock.png</file>
   </qresource>

=== modified file 'tests/functional/openlp_plugins/bibles/test_mediaitem.py'
--- tests/functional/openlp_plugins/bibles/test_mediaitem.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/bibles/test_mediaitem.py	2016-06-16 21:54:45 +0000
@@ -23,6 +23,7 @@
 This module contains tests for the lib submodule of the Presentations plugin.
 """
 from unittest import TestCase
+from openlp.core.common import Registry
 from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem
 from tests.functional import MagicMock, patch
 from tests.helpers.testmixin import TestMixin
@@ -41,6 +42,9 @@
                 patch('openlp.plugins.bibles.lib.mediaitem.BibleMediaItem.setup_item'):
             self.media_item = BibleMediaItem(None, MagicMock())
         self.setup_application()
+        self.mocked_main_window = MagicMock()
+        Registry.create()
+        Registry().register('main_window', self.mocked_main_window)
 
     def test_display_results_no_results(self):
         """
@@ -109,3 +113,40 @@
                 mocked_list_view.selectAll.assert_called_once_with()
                 self.assertEqual(self.media_item.search_results, {})
                 self.assertEqual(self.media_item.second_search_results, {})
+
+    def on_quick_search_button_general_test(self):
+        """
+        Test that general things, which should be called on all Quick searches are called.
+        """
+
+        # GIVEN: self.application as self.app, all the required functions
+        Registry.create()
+        Registry().register('application', self.app)
+        self.media_item.quickSearchButton = MagicMock()
+        self.app.process_events = MagicMock()
+        self.media_item.quickVersionComboBox = MagicMock()
+        self.media_item.quickVersionComboBox.currentText = MagicMock()
+        self.media_item.quickSecondComboBox = MagicMock()
+        self.media_item.quickSecondComboBox.currentText = MagicMock()
+        self.media_item.quick_search_edit = MagicMock()
+        self.media_item.quick_search_edit.text = MagicMock()
+        self.media_item.quickLockButton = MagicMock()
+        self.media_item.list_view = MagicMock()
+        self.media_item.search_results = MagicMock()
+        self.media_item.display_results = MagicMock()
+        self.media_item.check_search_result = MagicMock()
+        self.app.set_normal_cursor = MagicMock()
+
+        # WHEN: on_quick_search_button is called
+        self.media_item.on_quick_search_button()
+
+        # THEN: Search should had been started and finalized properly
+        self.assertEqual(1, self.app.process_events.call_count, 'Normal cursor should had been called once')
+        self.assertEqual(1, self.media_item.quickVersionComboBox.currentText.call_count, 'Should had been called once')
+        self.assertEqual(1, self.media_item.quickSecondComboBox.currentText.call_count, 'Should had been called once')
+        self.assertEqual(1, self.media_item.quick_search_edit.text.call_count, 'Text edit Should had been called once')
+        self.assertEqual(1, self.media_item.quickLockButton.isChecked.call_count, 'Lock Should had been called once')
+        self.assertEqual(1, self.media_item.display_results.call_count, 'Display results Should had been called once')
+        self.assertEqual(2, self.media_item.quickSearchButton.setEnabled.call_count, 'Disable and Enable the button')
+        self.assertEqual(1, self.media_item.check_search_result.call_count, 'Check results Should had been called once')
+        self.assertEqual(1, self.app.set_normal_cursor.call_count, 'Normal cursor should had been called once')


Follow ups