diff --git a/openlp/core/ui/lib/listwidgetwithdnd.py b/openlp/core/ui/lib/listwidgetwithdnd.py old mode 100644 new mode 100755 index 662e99ded..1758d46fb --- a/openlp/core/ui/lib/listwidgetwithdnd.py +++ b/openlp/core/ui/lib/listwidgetwithdnd.py @@ -44,7 +44,6 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setAlternatingRowColors(True) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.locked = False def activateDnD(self): """ @@ -54,15 +53,13 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) - def clear(self, search_while_typing=False, override_lock=False): + def clear(self, search_while_typing=False): """ Re-implement clear, so that we can customise feedback when using 'Search as you type' :param search_while_typing: True if we want to display the customised message :return: None """ - if self.locked and not override_lock: - return if search_while_typing: self.no_results_text = UiStrings().ShortResults else: @@ -128,6 +125,15 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): else: event.ignore() + def allItems(self): + """ + An generator to list all the items in the widget + + :return: a generator + """ + for row in range(self.count()): + yield self.item(row) + def paintEvent(self, event): """ Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. diff --git a/openlp/core/ui/lib/pathedit.py b/openlp/core/ui/lib/pathedit.py index 238bcb00d..c489daa33 100755 --- a/openlp/core/ui/lib/pathedit.py +++ b/openlp/core/ui/lib/pathedit.py @@ -46,16 +46,16 @@ class PathEdit(QtWidgets.QWidget): :param parent: The parent of the widget. This is just passed to the super method. :type parent: QWidget or None - + :param dialog_caption: Used to customise the caption in the QFileDialog. :param dialog_caption: str - + :param default_path: The default path. This is set as the path when the revert button is clicked :type default_path: str :param show_revert: Used to determin if the 'revert button' should be visible. :type show_revert: bool - + :return: None :rtype: None """ @@ -72,7 +72,7 @@ class PathEdit(QtWidgets.QWidget): Set up the widget :param show_revert: Show or hide the revert button :type show_revert: bool - + :return: None :rtype: None """ diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index bb190ab65..e4025411c 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -341,10 +341,10 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): if not book_ref_id: book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection) elif not bible.get_book_by_book_ref_id(book_ref_id): - return False + return [] # We have not found the book so do not continue if not book_ref_id: - return False + return [] ranges = match.group('ranges') range_list = get_reference_match('range_separator').split(ranges) ref_list = [] @@ -403,7 +403,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): return ref_list else: log.debug('Invalid reference: {text}'.format(text=reference)) - return None + return [] class SearchResults(object): diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index aaa90efe4..9afdd2efc 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -158,6 +158,7 @@ class BibleDB(Manager): self.get_name() if 'path' in kwargs: self.path = kwargs['path'] + self._is_web_bible = None def get_name(self): """ @@ -426,6 +427,18 @@ class BibleDB(Manager): return 0 return count + @property + def is_web_bible(self): + """ + A read only property indicating if the bible is a 'web bible' + + :return: If the bible is a web bible. + :rtype: bool + """ + if self._is_web_bible is None: + self._is_web_bible = bool(self.get_object(BibleMeta, 'download_source')) + return self._is_web_bible + def dump_bible(self): """ Utility debugging method to dump the contents of a bible. diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index c9a159634..899d659b6 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -142,8 +142,8 @@ class BibleManager(OpenLPMixin, RegistryProperties): log.debug('Bible Name: "{name}"'.format(name=name)) self.db_cache[name] = bible # Look to see if lazy load bible exists and get create getter. - source = self.db_cache[name].get_object(BibleMeta, 'download_source') - if source: + if self.db_cache[name].is_web_bible: + source = self.db_cache[name].get_object(BibleMeta, 'download_source') download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server') web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value, @@ -278,7 +278,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): :param show_error: """ if not bible or not ref_list: - return None + return [] return self.db_cache[bible].get_verses(ref_list, show_error) def get_language_selection(self, bible): @@ -305,11 +305,17 @@ class BibleManager(OpenLPMixin, RegistryProperties): """ Does a verse search for the given bible and text. - :param bible: The bible to search in (unicode). - :param second_bible: The second bible (unicode). We do not search in this bible. - :param text: The text to search for (unicode). + :param bible: The bible to search + :type bible: str + :param text: The text to search for + :type text: str + + :return: The search results if valid, or None if the search is invalid. + :rtype: None, list """ log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) + if not text: + return None # If no bibles are installed, message is given. if not bible: self.main_window.information_message( @@ -317,8 +323,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): UiStrings().BibleNoBibles) return None # Check if the bible or second_bible is a web bible. - web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source') - if web_bible: + if self.db_cache[bible].is_web_bible: # If either Bible is Web, cursor is reset to normal and message is given. self.application.set_normal_cursor() self.main_window.information_message( @@ -328,41 +333,8 @@ class BibleManager(OpenLPMixin, RegistryProperties): 'This means that the currently selected Bible is a Web Bible.') ) return None - # Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked. - if len(text) - text.count(' ') < 3: - return None # Fetch the results from db. If no results are found, return None, no message is given for this. - elif text: - return self.db_cache[bible].verse_search(text) - else: - return None - - def verse_search_while_typing(self, bible, second_bible, text): - """ - Does a verse search for the given bible and text. - This is used during "Search while typing" - It's the same thing as the normal text search, but it does not show the web Bible error. - (It would result in the error popping every time a char is entered or removed) - It also does not have a minimum text len, this is set in mediaitem.py - - :param bible: The bible to search in (unicode). - :param second_bible: The second bible (unicode). We do not search in this bible. - :param text: The text to search for (unicode). - """ - # If no bibles are installed, message is given. - if not bible: - return None - # Check if the bible or second_bible is a web bible. - web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source') - second_web_bible = '' - if second_bible: - second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source') - if web_bible or second_web_bible: - # If either Bible is Web, cursor is reset to normal and search ends w/o any message. - self.application.set_normal_cursor() - return None - # Fetch the results from db. If no results are found, return None, no message is given for this. - elif text: + if text: return self.db_cache[bible].verse_search(text) else: return None diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py old mode 100644 new mode 100755 index 9344fab63..f0ef34dd6 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -22,6 +22,7 @@ import logging import re +from enum import IntEnum, unique from PyQt5 import QtCore, QtWidgets @@ -48,15 +49,45 @@ def get_reference_separators(): 'list': get_reference_separator('sep_l_display')} -class BibleSearch(object): +@unique +class BibleSearch(IntEnum): """ - Enumeration class for the different search methods for the "Search" tab. + Enumeration class for the different search types for the "Search" tab. """ Reference = 1 Text = 2 Combined = 3 +@unique +class ResultsTab(IntEnum): + """ + Enumeration class for the different tabs for the results list. + """ + Saved = 0 + Search = 1 + + +@unique +class SearchStatus(IntEnum): + """ + Enumeration class for the different search methods. + """ + SearchButton = 0 + SearchAsYouType = 1 + NotEnoughText = 2 + + +@unique +class SearchTabs(IntEnum): + """ + Enumeration class for the tabs on the media item. + """ + Search = 0 + Select = 1 + Options = 2 + + class BibleMediaItem(MediaManagerItem): """ This is the custom media manager item for Bibles. @@ -73,11 +104,13 @@ class BibleMediaItem(MediaManagerItem): :param kwargs: Keyword arguments to pass to the super method. (dict) """ self.clear_icon = build_icon(':/bibles/bibles_search_clear.png') - self.lock_icon = build_icon(':/bibles/bibles_search_lock.png') - self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png') + self.save_results_icon = build_icon(':/bibles/bibles_save_results.png') self.sort_icon = build_icon(':/bibles/bibles_book_sort.png') self.bible = None self.second_bible = None + self.saved_results = [] + self.current_results = [] + self.search_status = SearchStatus.SearchButton # TODO: Make more central and clean up after! self.search_timer = QtCore.QTimer() self.search_timer.setInterval(200) @@ -162,8 +195,10 @@ class BibleMediaItem(MediaManagerItem): self.select_tab.setVisible(False) self.page_layout.addWidget(self.select_tab) # General Search Opions - self.options_widget = QtWidgets.QGroupBox(translate('BiblesPlugin.MediaItem', 'Options'), self) - self.general_bible_layout = QtWidgets.QFormLayout(self.options_widget) + self.options_tab = QtWidgets.QWidget() + self.options_tab.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.search_tab_bar.addTab(translate('BiblesPlugin.MediaItem', 'Options')) + self.general_bible_layout = QtWidgets.QFormLayout(self.options_tab) self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box') self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box) self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box') @@ -171,20 +206,28 @@ class BibleMediaItem(MediaManagerItem): self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box') self.style_combo_box.addItems(['', '', '']) self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box) - self.search_button_layout = QtWidgets.QHBoxLayout() + self.options_tab.setVisible(False) + self.page_layout.addWidget(self.options_tab) + # This widget is the easier way to reset the spacing of search_button_layout. (Because page_layout has had its + # spacing set to 0) + self.search_button_widget = QtWidgets.QWidget() + self.search_button_layout = QtWidgets.QHBoxLayout(self.search_button_widget) self.search_button_layout.addStretch() # Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon. - self.clear_button = QtWidgets.QToolButton(self) + self.clear_button = QtWidgets.QPushButton() self.clear_button.setIcon(self.clear_icon) - self.lock_button = QtWidgets.QToolButton(self) - self.lock_button.setIcon(self.unlock_icon) - self.lock_button.setCheckable(True) + self.save_results_button = QtWidgets.QPushButton() + self.save_results_button.setIcon(self.save_results_icon) self.search_button_layout.addWidget(self.clear_button) - self.search_button_layout.addWidget(self.lock_button) + self.search_button_layout.addWidget(self.save_results_button) self.search_button = QtWidgets.QPushButton(self) self.search_button_layout.addWidget(self.search_button) - self.general_bible_layout.addRow(self.search_button_layout) - self.page_layout.addWidget(self.options_widget) + self.page_layout.addWidget(self.search_button_widget) + self.results_view_tab = QtWidgets.QTabBar(self) + self.results_view_tab.addTab('') + self.results_view_tab.addTab('') + self.results_view_tab.setCurrentIndex(ResultsTab.Search) + self.page_layout.addWidget(self.results_view_tab) def setupUi(self): super().setupUi() @@ -211,12 +254,15 @@ class BibleMediaItem(MediaManagerItem): # Buttons self.book_order_button.toggled.connect(self.on_book_order_button_toggled) self.clear_button.clicked.connect(self.on_clear_button_clicked) - self.lock_button.toggled.connect(self.on_lock_button_toggled) + self.save_results_button.clicked.connect(self.on_save_results_button_clicked) self.search_button.clicked.connect(self.on_search_button_clicked) # Other stuff self.search_edit.returnPressed.connect(self.on_search_button_clicked) self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed) + self.results_view_tab.currentChanged.connect(self.on_results_view_tab_current_changed) self.search_edit.textChanged.connect(self.on_search_edit_text_changed) + self.on_results_view_tab_total_update(ResultsTab.Saved) + self.on_results_view_tab_total_update(ResultsTab.Search) def retranslateUi(self): log.debug('retranslateUi') @@ -225,9 +271,9 @@ class BibleMediaItem(MediaManagerItem): self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide) self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine) self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous) - self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.')) - self.lock_button.setToolTip( - translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.')) + self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the results on the current tab.')) + self.save_results_button.setToolTip( + translate('BiblesPlugin.MediaItem', 'Add the search results to the saved list.')) self.search_button.setText(UiStrings().Search) def on_focus(self): @@ -241,8 +287,10 @@ class BibleMediaItem(MediaManagerItem): if self.search_tab.isVisible(): self.search_edit.setFocus() self.search_edit.selectAll() - else: + if self.select_tab.isVisible(): self.select_book_combo_box.setFocus() + if self.options_tab.isVisible(): + self.version_combo_box.setFocus() def config_update(self): """ @@ -415,14 +463,48 @@ class BibleMediaItem(MediaManagerItem): """ Show the selected tab and set focus to it - :param index: The tab selected (int) + :param index: The tab selected + :type index: int :return: None """ - search_tab = index == 0 - self.search_tab.setVisible(search_tab) - self.select_tab.setVisible(not search_tab) + if index == SearchTabs.Search or index == SearchTabs.Select: + self.search_button.setEnabled(True) + else: + self.search_button.setEnabled(False) + self.search_tab.setVisible(index == SearchTabs.Search) + self.select_tab.setVisible(index == SearchTabs.Select) + self.options_tab.setVisible(index == SearchTabs.Options) self.on_focus() + def on_results_view_tab_current_changed(self, index): + """ + Update list_widget with the contents of the selected list + + :param index: The index of the tab that has been changed to. (int) + :return: None + """ + if index == ResultsTab.Saved: + self.add_built_results_to_list_widget(self.saved_results) + elif index == ResultsTab.Search: + self.add_built_results_to_list_widget(self.current_results) + + def on_results_view_tab_total_update(self, index): + """ + Update the result total count on the tab with the given index. + + :param index: Index of the tab to update (int) + :return: None + """ + string = '' + count = 0 + if index == ResultsTab.Saved: + string = translate('BiblesPlugin.MediaItem', 'Saved ({result_count})') + count = len(self.saved_results) + elif index == ResultsTab.Search: + string = translate('BiblesPlugin.MediaItem', 'Results ({result_count})') + count = len(self.current_results) + self.results_view_tab.setTabText(index, string.format(result_count=count)) + def on_book_order_button_toggled(self, checked): """ Change the sort order of the book names @@ -442,22 +524,25 @@ class BibleMediaItem(MediaManagerItem): :return: None """ - self.list_view.clear() - self.search_edit.clear() - self.on_focus() + current_index = self.results_view_tab.currentIndex() + for item in self.list_view.selectedItems(): + self.list_view.takeItem(self.list_view.row(item)) + results = [item.data(QtCore.Qt.UserRole) for item in self.list_view.allItems()] + if current_index == ResultsTab.Saved: + self.saved_results = results + elif current_index == ResultsTab.Search: + self.current_results = results + self.on_results_view_tab_total_update(current_index) - def on_lock_button_toggled(self, checked): + def on_save_results_button_clicked(self): """ - Toggle the lock button, if Search tab is used, set focus to search field. + Add the selected verses to the saved_results list. - :param checked: The state of the toggle button. (bool) :return: None """ - self.list_view.locked = checked - if checked: - self.sender().setIcon(self.lock_icon) - else: - self.sender().setIcon(self.unlock_icon) + for verse in self.list_view.selectedItems(): + self.saved_results.append(verse.data(QtCore.Qt.UserRole)) + self.on_results_view_tab_total_update(ResultsTab.Saved) def on_style_combo_box_index_changed(self, index): """ @@ -490,16 +575,17 @@ class BibleMediaItem(MediaManagerItem): :return: None """ new_selection = self.second_combo_box.currentData() - if self.list_view.count(): + if self.saved_results: # Exclusive or (^) the new and previous selections to detect if the user has switched between single and # dual bible mode if (new_selection is None) ^ (self.second_bible is None): if critical_error_message_box( message=translate('BiblesPlugin.MediaItem', 'OpenLP cannot combine single and dual Bible verse search results. ' - 'Do you want to clear your search results and start a new search?'), + 'Do you want to clear your saved results?'), parent=self, question=True) == QtWidgets.QMessageBox.Yes: - self.list_view.clear(override_lock=True) + self.saved_results = [] + self.on_results_view_tab_total_update(ResultsTab.Saved) else: self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible)) return @@ -525,7 +611,8 @@ class BibleMediaItem(MediaManagerItem): log.warning('Not enough chapters in %s', book_ref_id) critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.')) else: - self.search_button.setEnabled(True) + if self.select_tab.isVisible(): + self.search_button.setEnabled(True) self.adjust_combo_box(1, self.chapter_count, self.from_chapter) self.adjust_combo_box(1, self.chapter_count, self.to_chapter) self.adjust_combo_box(1, verse_count, self.from_verse) @@ -602,6 +689,8 @@ class BibleMediaItem(MediaManagerItem): :return: None """ + self.search_timer.stop() + self.search_status = SearchStatus.SearchButton if not self.bible: self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles) return @@ -613,6 +702,7 @@ class BibleMediaItem(MediaManagerItem): elif self.select_tab.isVisible(): self.select_search() self.search_button.setEnabled(True) + self.results_view_tab.setCurrentIndex(ResultsTab.Search) self.application.set_normal_cursor() def select_search(self): @@ -636,18 +726,21 @@ class BibleMediaItem(MediaManagerItem): :return: None """ + self.search_results = [] verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text) self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True) if self.second_bible and self.search_results: self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True) self.display_results() - def on_text_search(self, text, search_while_type=False): + def on_text_search(self, text): """ We are doing a 'Text Search'. This search is called on def text_search by 'Search' Text and Combined Searches. """ self.search_results = self.plugin.manager.verse_search(self.bible.name, text) + if self.search_results is None: + return if self.second_bible and self.search_results: filtered_search_results = [] not_found_count = 0 @@ -663,7 +756,7 @@ class BibleMediaItem(MediaManagerItem): verse=verse.verse, bible_name=self.second_bible.name)) not_found_count += 1 self.search_results = filtered_search_results - if not_found_count != 0 and not search_while_type: + if not_found_count != 0 and self.search_status == SearchStatus.SearchButton: self.main_window.information_message( translate('BiblesPlugin.MediaItem', 'Verses not found'), translate('BiblesPlugin.MediaItem', @@ -673,22 +766,23 @@ class BibleMediaItem(MediaManagerItem): ).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count)) self.display_results() - def text_search(self, search_while_type=False): + def text_search(self): """ This triggers the proper 'Search' search based on which search type is used. "Eg. "Reference Search", "Text Search" or "Combined search". """ + self.search_results = [] log.debug('text_search called') text = self.search_edit.text() if text == '': - self.list_view.clear() + self.display_results() return - self.list_view.clear(search_while_typing=search_while_type) + self.on_results_view_tab_total_update(ResultsTab.Search) if self.search_edit.current_search_type() == BibleSearch.Reference: if get_reference_match('full').match(text): # Valid reference found. Do reference search. self.text_reference_search(text) - elif not search_while_type: + elif self.search_status == SearchStatus.SearchButton: self.main_window.information_message( translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'), translate('BiblesPlugin.BibleManager', @@ -700,10 +794,12 @@ class BibleMediaItem(MediaManagerItem): self.text_reference_search(text) else: # It can only be a 'Combined' search without a valid reference, or a 'Text' search - if search_while_type: - if len(text) > 8 and VALID_TEXT_SEARCH.search(text): - self.on_text_search(text, True) - elif VALID_TEXT_SEARCH.search(text): + if self.search_status == SearchStatus.SearchAsYouType: + if len(text) <= 8: + self.search_status = SearchStatus.NotEnoughText + self.display_results() + return + if VALID_TEXT_SEARCH.search(text): self.on_text_search(text) def on_search_edit_text_changed(self): @@ -713,9 +809,12 @@ class BibleMediaItem(MediaManagerItem): :return: None """ - if Settings().value('bibles/is search while typing enabled'): - if not self.search_timer.isActive(): - self.search_timer.start() + if not Settings().value('bibles/is search while typing enabled') or \ + not self.bible or self.bible.is_web_bible or \ + (self.second_bible and self.bible.is_web_bible): + return + if not self.search_timer.isActive(): + self.search_timer.start() def on_search_timer_timeout(self): """ @@ -724,7 +823,9 @@ class BibleMediaItem(MediaManagerItem): :return: None """ - self.text_search(True) + self.search_status = SearchStatus.SearchAsYouType + self.text_search() + self.results_view_tab.setCurrentIndex(ResultsTab.Search) def display_results(self): """ @@ -732,14 +833,16 @@ class BibleMediaItem(MediaManagerItem): :return: None """ - self.list_view.clear() - if self.search_results: - items = self.build_display_results(self.bible, self.second_bible, self.search_results) - for item in items: - self.list_view.addItem(item) - self.list_view.selectAll() + self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results) self.search_results = [] - self.second_search_results = [] + self.add_built_results_to_list_widget(self.current_results) + + def add_built_results_to_list_widget(self, results): + self.list_view.clear(self.search_status == SearchStatus.NotEnoughText) + for item in self.build_list_widget_items(results): + self.list_view.addItem(item) + self.list_view.selectAll() + self.on_results_view_tab_total_update(ResultsTab.Search) def build_display_results(self, bible, second_bible, search_results): """ @@ -789,11 +892,18 @@ class BibleMediaItem(MediaManagerItem): bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})' else: bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})' - bible_verse = QtWidgets.QListWidgetItem(bible_text.format(sep=verse_separator, **data)) - bible_verse.setData(QtCore.Qt.UserRole, data) - items.append(bible_verse) + data['item_title'] = bible_text.format(sep=verse_separator, **data) + items.append(data) return items + def build_list_widget_items(self, items): + list_widget_items = [] + for data in items: + bible_verse = QtWidgets.QListWidgetItem(data['item_title']) + bible_verse.setData(QtCore.Qt.UserRole, data) + list_widget_items.append(bible_verse) + return list_widget_items + def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, context=ServiceItemContext.Service): """ @@ -897,6 +1007,8 @@ class BibleMediaItem(MediaManagerItem): """ Search for some Bible verses (by reference). """ + if self.bible is None: + return [] reference = self.plugin.manager.parse_ref(self.bible.name, string) search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError) if search_results: @@ -908,6 +1020,9 @@ class BibleMediaItem(MediaManagerItem): """ Create a media item from an item id. """ + if self.bible is None: + return [] reference = self.plugin.manager.parse_ref(self.bible.name, item_id) search_results = self.plugin.manager.get_verses(self.bible.name, reference, False) - return self.build_display_results(self.bible, None, search_results) + items = self.build_display_results(self.bible, None, search_results) + return self.build_list_widget_items(items) diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 884f155a2..210f8a531 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 ############################################################################### diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index 74c8c89c8..082173bf5 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -69,7 +69,7 @@ class Ui_SongUsageDetailDialog(object): self.file_horizontal_layout.setSpacing(8) self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8) self.file_horizontal_layout.setObjectName('file_horizontal_layout') - self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False) + self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False) self.file_horizontal_layout.addWidget(self.report_path_edit) self.vertical_layout.addWidget(self.file_group_box) self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok']) diff --git a/resources/images/bibles_save_results.png b/resources/images/bibles_save_results.png new file mode 100644 index 000000000..ee119c395 Binary files /dev/null and b/resources/images/bibles_save_results.png differ diff --git a/resources/images/bibles_search_lock.png b/resources/images/bibles_search_lock.png deleted file mode 100644 index ac2fd6f90..000000000 Binary files a/resources/images/bibles_search_lock.png and /dev/null differ diff --git a/resources/images/bibles_search_unlock.png b/resources/images/bibles_search_unlock.png deleted file mode 100644 index d03a1f415..000000000 Binary files a/resources/images/bibles_search_unlock.png and /dev/null differ diff --git a/resources/images/network_ssl.png b/resources/images/network_ssl.png deleted file mode 100644 index 1169de67a..000000000 Binary files a/resources/images/network_ssl.png and /dev/null differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index e221474ff..1a50dbe78 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -34,8 +34,7 @@ bibles_search_text.png bibles_search_reference.png bibles_search_clear.png - bibles_search_unlock.png - bibles_search_lock.png + bibles_save_results.png plugin_alerts.png @@ -144,7 +143,6 @@ network_server.png - network_ssl.png network_auth.png @@ -188,4 +186,4 @@ android_app_qr.png ios_app_qr.png - \ No newline at end of file + diff --git a/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py b/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py old mode 100644 new mode 100755 index 8bb30e50f..0b79f7ab1 --- a/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py +++ b/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py @@ -23,6 +23,7 @@ This module contains tests for the openlp.core.lib.listwidgetwithdnd module """ from unittest import TestCase +from types import GeneratorType from openlp.core.common.uistrings import UiStrings from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD @@ -33,37 +34,6 @@ class TestListWidgetWithDnD(TestCase): """ Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class """ - def test_clear_locked(self): - """ - Test the clear method the list is 'locked' - """ - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method: - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: The list is 'locked' and clear has been called - widget.locked = True - widget.clear() - - # THEN: The super method should not have been called (i.e. The list not cleared) - self.assertFalse(mocked_clear_super_method.called) - - def test_clear_overide_locked(self): - """ - Test the clear method the list is 'locked', but clear is called with 'override_lock' set to True - """ - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method: - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: The list is 'locked' and clear has been called with override_lock se to True - widget.locked = True - widget.clear(override_lock=True) - - # THEN: The super method should have been called (i.e. The list is cleared regardless whether it is locked - # or not) - mocked_clear_super_method.assert_called_once_with() - def test_clear(self): """ Test the clear method when called without any arguments. @@ -90,6 +60,38 @@ class TestListWidgetWithDnD(TestCase): # THEN: The results text should be the 'short results' text. self.assertEqual(widget.no_results_text, UiStrings().ShortResults) + def test_all_items_no_list_items(self): + """ + Test allItems when there are no items in the list widget + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'item', side_effect=lambda x: [][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), []) + + def test_all_items_list_items(self): + """ + Test allItems when the list widget contains some items. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=2), \ + patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), [5, 3]) + def test_paint_event(self): """ Test the paintEvent method when the list is not empty diff --git a/tests/functional/openlp_plugins/bibles/test_mediaitem.py b/tests/functional/openlp_plugins/bibles/test_mediaitem.py old mode 100644 new mode 100755 index fd2d159f9..7ed904145 --- a/tests/functional/openlp_plugins/bibles/test_mediaitem.py +++ b/tests/functional/openlp_plugins/bibles/test_mediaitem.py @@ -31,7 +31,8 @@ from tests.helpers.testmixin import TestMixin from openlp.core.common import Registry from openlp.core.lib import MediaManagerItem -from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, get_reference_separators, VALID_TEXT_SEARCH +from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, ResultsTab, SearchStatus, \ + get_reference_separators, VALID_TEXT_SEARCH class TestBibleMediaItemModulefunctions(TestCase): @@ -143,6 +144,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item = BibleMediaItem(None, self.mocked_plugin) self.media_item.settings_section = 'bibles' + self.media_item.results_view_tab = MagicMock() self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1}) self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2}) @@ -658,56 +660,65 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The select_book_combo_box model sort should have been reset self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1) - def test_on_clear_button_clicked(self): + def test_on_clear_button_clicked_saved_tab(self): """ - Test on_clear_button_clicked + Test on_clear_button_clicked when the saved tab is selected + """ + # GIVEN: An instance of :class:`MediaManagerItem` and mocked out saved_tab and select_tab and a mocked out + # list_view and search_edit + self.media_item.list_view = MagicMock() + self.media_item.search_edit = MagicMock() + self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Saved}) + self.media_item.saved_results = ['Some', 'Results'] + with patch.object(self.media_item, 'on_focus'): + + # WHEN: Calling on_clear_button_clicked + self.media_item.on_clear_button_clicked() + + # THEN: The list_view should be cleared + self.assertEqual(self.media_item.saved_results, []) + self.media_item.list_view.clear.assert_called_once_with() + + def test_on_clear_button_clicked_search_tab(self): + """ + Test on_clear_button_clicked when the search tab is selected """ # GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out # list_view and search_edit self.media_item.list_view = MagicMock() self.media_item.search_edit = MagicMock() + self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Search}) + self.media_item.current_results = ['Some', 'Results'] with patch.object(self.media_item, 'on_focus'): # WHEN: Calling on_clear_button_clicked self.media_item.on_clear_button_clicked() # THEN: The list_view and the search_edit should be cleared + self.assertEqual(self.media_item.current_results, []) self.media_item.list_view.clear.assert_called_once_with() self.media_item.search_edit.clear.assert_called_once_with() - def test_on_lock_button_toggled_search_tab_lock_icon(self): + def test_on_save_results_button_clicked(self): """ - Test that "on_lock_button_toggled" toggles the lock properly. + Test that "on_save_results_button_clicked" saves the results. """ - # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view - self.media_item.list_view = MagicMock() - self.media_item.lock_icon = 'lock icon' - mocked_sender_instance = MagicMock() - with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance): + # GIVEN: An instance of :class:`MediaManagerItem` and a mocked list_view + result_1 = MagicMock(**{'data.return_value': 'R1'}) + result_2 = MagicMock(**{'data.return_value': 'R2'}) + result_3 = MagicMock(**{'data.return_value': 'R3'}) + self.media_item.list_view = MagicMock(**{'selectedItems.return_value': [result_1, result_2, result_3]}) - # WHEN: When the lock_button is checked - self.media_item.on_lock_button_toggled(True) + with patch.object(self.media_item, 'on_results_view_tab_total_update') as \ + mocked_on_results_view_tab_total_update: - # THEN: list_view should be 'locked' and the lock icon set - self.assertTrue(self.media_item.list_view.locked) - mocked_sender_instance.setIcon.assert_called_once_with('lock icon') + # WHEN: When the save_results_button is clicked + self.media_item.on_save_results_button_clicked() - def test_on_lock_button_toggled_unlock_icon(self): - """ - Test that "on_lock_button_toggled" toggles the lock properly. - """ - # GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view - self.media_item.list_view = MagicMock() - self.media_item.unlock_icon = 'unlock icon' - mocked_sender_instance = MagicMock() - with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance): - - # WHEN: When the lock_button is unchecked - self.media_item.on_lock_button_toggled(False) - - # THEN: list_view should be 'unlocked' and the unlock icon set - self.assertFalse(self.media_item.list_view.locked) - mocked_sender_instance.setIcon.assert_called_once_with('unlock icon') + # THEN: The selected results in the list_view should be added to the 'saved_results' list. And the saved_tab + # total should be updated. + self.assertEqual(self.media_item.saved_results, ['R1', 'R2', 'R3']) + mocked_on_results_view_tab_total_update.assert_called_once_with(ResultsTab.Saved) def test_on_style_combo_box_changed(self): """ @@ -815,7 +826,9 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.list_view = MagicMock(**{'count.return_value': 5}) self.media_item.style_combo_box = MagicMock() self.media_item.select_book_combo_box = MagicMock() + self.media_item.search_results = ['list', 'of', 'results'] with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \ + patch.object(self.media_item, 'display_results'), \ patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box', return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box: @@ -825,9 +838,8 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1}) self.media_item.on_second_combo_box_index_changed(5) - # THEN: The list_view should be cleared and the selected bible should be set as the current bible + # THEN: The selected bible should be set as the current bible self.assertTrue(mocked_critical_error_message_box.called) - self.media_item.list_view.clear.assert_called_once_with(override_lock=True) self.media_item.style_combo_box.setEnabled.assert_called_once_with(False) self.assertTrue(mocked_initialise_advanced_bible.called) self.assertEqual(self.media_item.second_bible, self.mocked_bible_1) @@ -841,7 +853,9 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.list_view = MagicMock(**{'count.return_value': 5}) self.media_item.style_combo_box = MagicMock() self.media_item.select_book_combo_box = MagicMock() + self.media_item.search_results = ['list', 'of', 'results'] with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \ + patch.object(self.media_item, 'display_results'), \ patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box', return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box: # WHEN: The previously is a bible new selection is None and the user selects yes @@ -850,9 +864,8 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None}) self.media_item.on_second_combo_box_index_changed(0) - # THEN: The list_view should be cleared and the selected bible should be set as the current bible + # THEN: The selected bible should be set as the current bible self.assertTrue(mocked_critical_error_message_box.called) - self.media_item.list_view.clear.assert_called_once_with(override_lock=True) self.media_item.style_combo_box.setEnabled.assert_called_once_with(True) self.assertFalse(mocked_initialise_advanced_bible.called) self.assertEqual(self.media_item.second_bible, None) @@ -1388,8 +1401,9 @@ class TestMediaItem(TestCase, TestMixin): # WHEN: Calling on_search_timer_timeout self.media_item.on_search_timer_timeout() - # THEN: The text_search method should have been called with True - mocked_text_search.assert_called_once_with(True) + # THEN: The search_status should be set to SearchAsYouType and text_search should have been called + self.assertEqual(self.media_item.search_status, SearchStatus().SearchAsYouType) + mocked_text_search.assert_called_once_with() def test_display_results_no_results(self): """ @@ -1407,7 +1421,6 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.display_results() # THEN: No items should be added to the list - self.media_item.list_view.clear.assert_called_once_with() self.assertFalse(self.media_item.list_view.addItem.called) def test_display_results_results(self): @@ -1415,7 +1428,10 @@ class TestMediaItem(TestCase, TestMixin): Test the display_results method when there are items to display """ # GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results - with patch.object(self.media_item, 'build_display_results', return_value=['list', 'items']): + with patch.object(self.media_item, 'build_display_results', return_value=[ + {'item_title': 'Title 1'}, {'item_title': 'Title 2'}]), \ + patch.object(self.media_item, 'add_built_results_to_list_widget') as \ + mocked_add_built_results_to_list_widget: self.media_item.search_results = ['results'] self.media_item.list_view = MagicMock() @@ -1423,5 +1439,5 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.display_results() # THEN: addItem should have been with the display items - self.media_item.list_view.clear.assert_called_once_with() - self.assertEqual(self.media_item.list_view.addItem.call_args_list, [call('list'), call('items')]) + mocked_add_built_results_to_list_widget.assert_called_once_with( + [{'item_title': 'Title 1'}, {'item_title': 'Title 2'}]) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py index 95f0be3b5..1916f267b 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py @@ -108,7 +108,7 @@ class TestBibleManager(TestCase, TestMixin): # WHEN asking to parse the bible reference results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock()) # THEN a verse array should be returned - self.assertEqual(False, results, "The bible Search should return False") + self.assertEqual([], results, "The bible Search should return an empty list") def test_parse_reference_five(self): """