diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index dccc3bdb4..e947b1362 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -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 @@ class UiStrings(object): 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', 'The search you have entered is empty or shorter ' + 'than 3 characters long.

Please try again with ' + 'a longer search.') + self.BibleNoBiblesTitle = translate('OpenLP.Ui', 'No Bibles Available') + self.BibleNoBibles = translate('OpenLP.Ui', 'There are no Bibles currently installed.

' + '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
', + book_chapter, '%(range)s', chapter, gap, psalm, ' 23%(range)s24
', + book_chapter, '%(verse)s', verse, '%(range)s', verse, gap, psalm, ' 23%(verse)s1%(range)s2
', + 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
', + 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
', + book_chapter, '%(verse)s', verse, '%(range)s', chapter, '%(verse)s', verse, gap, psalm, + ' 23%(verse)s1%(range)s24%(verse)s1

', 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) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 358c82543..c7c84f912 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -651,6 +651,20 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): 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. diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index ccc61ba56..3d27effe4 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -41,7 +41,8 @@ __default_settings__ = { '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 @@ __default_settings__ = { '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 } diff --git a/openlp/plugins/bibles/lib/biblestab.py b/openlp/plugins/bibles/lib/biblestab.py index 5438d07f2..c087335ae 100644 --- a/openlp/plugins/bibles/lib/biblestab.py +++ b/openlp/plugins/bibles/lib/biblestab.py @@ -128,6 +128,20 @@ class BiblesTab(SettingsTab): 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 @@ class BiblesTab(SettingsTab): 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 @@ class BiblesTab(SettingsTab): 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 @@ class BiblesTab(SettingsTab): 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 @@ class BiblesTab(SettingsTab): 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 @@ class BiblesTab(SettingsTab): 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') diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 1c55222f2..9ea4c6612 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -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 @@ class BibleManager(RegistryProperties): 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,13 +313,11 @@ class BibleManager(RegistryProperties): :param text: The text to search for (unicode). """ log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) + # If no bibles are installed, message is given. 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.') - ) + 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') @@ -348,20 +325,56 @@ class BibleManager(RegistryProperties): 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'), - translate('BiblesPlugin.BibleManager', 'Text Search is not available with Web Bibles.') + 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 - if text: + # 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: - 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.') - ) return None def save_meta_data(self, bible, version, copyright, permissions, book_name_language=None): diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index d5304e867..debf786f7 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -35,6 +35,7 @@ from openlp.plugins.bibles.forms.editbibleform import EditBibleForm 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 @@ class BibleSearch(object): """ Reference = 1 Text = 2 + Combined = 3 class BibleMediaItem(MediaManagerItem): @@ -56,6 +58,7 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): search_button_layout = QtWidgets.QHBoxLayout() search_button_layout.setObjectName(prefix + 'search_button_layout') search_button_layout.addStretch() + # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon. + clear_button = QtWidgets.QToolButton(tab) + 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 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): 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 @@ class BibleMediaItem(MediaManagerItem): 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,7 +548,15 @@ class BibleMediaItem(MediaManagerItem): 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, then clear the text field and give it focus. + self.list_view.clear() + self.check_search_result() + self.quick_search_edit.clear() + self.quick_search_edit.setFocus() + def on_lock_button_toggled(self, checked): + self.quick_search_edit.setFocus() if checked: self.sender().setIcon(self.lock_icon) else: @@ -652,10 +683,120 @@ class BibleMediaItem(MediaManagerItem): 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 +805,68 @@ class BibleMediaItem(MediaManagerItem): 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', 'OpenLP couldn’t find anything ' + 'with your search.

' + 'Please make sure that your reference follows ' + 'one of these patterns:


%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', + 'OpenLP couldn’t find anything with your' + ' search.

If you tried to search' + ' with Scripture Reference, please make
sure' + ' that your reference follows one of these' + ' patterns:

%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 +877,99 @@ class BibleMediaItem(MediaManagerItem): 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. diff --git a/resources/images/bibles_search_clear.png b/resources/images/bibles_search_clear.png new file mode 100644 index 000000000..551ce7c96 Binary files /dev/null and b/resources/images/bibles_search_clear.png differ diff --git a/resources/images/bibles_search_combined.png b/resources/images/bibles_search_combined.png new file mode 100644 index 000000000..161b3fcd6 Binary files /dev/null and b/resources/images/bibles_search_combined.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index f2619b0c7..b45cc745d 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -30,9 +30,11 @@ image_new_group.png + bibles_search_combined.png bibles_search_text.png bibles_search_reference.png bibles_upgrade_alert.png + bibles_search_clear.png bibles_search_unlock.png bibles_search_lock.png diff --git a/tests/functional/openlp_plugins/bibles/test_mediaitem.py b/tests/functional/openlp_plugins/bibles/test_mediaitem.py index f820c7a64..05418f177 100644 --- a/tests/functional/openlp_plugins/bibles/test_mediaitem.py +++ b/tests/functional/openlp_plugins/bibles/test_mediaitem.py @@ -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 @@ class TestMediaItem(TestCase, TestMixin): 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 @@ class TestMediaItem(TestCase, TestMixin): 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')