diff --git a/src/calibre/gui2/store/search/models.py b/src/calibre/gui2/store/search/models.py index 975601c603..3d24499e94 100644 --- a/src/calibre/gui2/store/search/models.py +++ b/src/calibre/gui2/store/search/models.py @@ -16,8 +16,6 @@ from calibre.gui2 import NONE, FunctionDispatcher from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search.download_thread import DetailsThreadPool, \ CoverThreadPool -from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ - REGEXP_MATCH from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import SearchQueryParser @@ -153,7 +151,7 @@ class Matches(QAbstractItemModel): mod_query = query # Remove filter identifiers # Remove the prefix. - for loc in ('all', 'author', 'authors', 'title'): + for loc in ('all', 'author', 'author2', 'authors', 'title'): query = re.sub(r'%s:"(?P[^\s"]+)"' % loc, '\g', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. @@ -301,11 +299,16 @@ class Matches(QAbstractItemModel): class SearchFilter(SearchQueryParser): + CONTAINS_MATCH = 0 + EQUALS_MATCH = 1 + REGEXP_MATCH = 2 + IN_MATCH = 3 USABLE_LOCATIONS = [ 'all', 'affiliate', 'author', + 'author2', 'authors', 'cover', 'download', @@ -331,6 +334,26 @@ class SearchFilter(SearchQueryParser): def universal_set(self): return self.srs + def _match(self, query, value, matchkind): + for t in value: + try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished + t = icu_lower(t) + if matchkind == self.EQUALS_MATCH: + if query == t: + return True + elif matchkind == self.REGEXP_MATCH: + if re.search(query, t, re.I|re.UNICODE): + return True + elif matchkind == self.CONTAINS_MATCH: + if query in t: + return True + elif matchkind == self.IN_MATCH: + if t in query: + return True + except re.error: + pass + return False + def get_matches(self, location, query): query = query.strip() location = location.lower().strip() @@ -341,17 +364,17 @@ class SearchFilter(SearchQueryParser): elif location == 'formats': location = 'format' - matchkind = CONTAINS_MATCH + matchkind = self.CONTAINS_MATCH if len(query) > 1: if query.startswith('\\'): query = query[1:] elif query.startswith('='): - matchkind = EQUALS_MATCH + matchkind = self.EQUALS_MATCH query = query[1:] elif query.startswith('~'): - matchkind = REGEXP_MATCH + matchkind = self.REGEXP_MATCH query = query[1:] - if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + if matchkind != self.REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D query = query.lower() if location not in self.USABLE_LOCATIONS: @@ -372,6 +395,7 @@ class SearchFilter(SearchQueryParser): } for x in ('author', 'download', 'format'): q[x+'s'] = q[x] + q['author2'] = q['author'] # make the price in query the same format as result if location == 'price': @@ -415,16 +439,19 @@ class SearchFilter(SearchQueryParser): ### Can't separate authors because comma is used for name sep and author sep ### Exact match might not get what you want. For that reason, turn author ### exactmatch searches into contains searches. - if locvalue == 'author' and matchkind == EQUALS_MATCH: - m = CONTAINS_MATCH + if locvalue == 'author' and matchkind == self.EQUALS_MATCH: + m = self.CONTAINS_MATCH else: m = matchkind if locvalue == 'format': vals = accessor(sr).split(',') + elif locvalue == 'author2': + m = self.IN_MATCH + vals = re.sub(r'(^|\s)(and|not|or|a|the|is|of|,)(\s|$)', ' ', query).split(' ') else: vals = [accessor(sr)] - if _match(query, vals, m): + if self._match(query, vals, m): matches.add(sr) break except ValueError: # Unicode errors diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index adde77cce1..9f401d436c 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -13,7 +13,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel, QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout, QComboBox) -from calibre.gui2 import JSONConfig, info_dialog +from calibre.gui2 import JSONConfig, info_dialog, error_dialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget @@ -31,6 +31,8 @@ class SearchDialog(QDialog, Ui_Dialog): self.setupUi(self) self.config = JSONConfig('store/search') + self.search_title.initialize('store_search_search_title') + self.search_author.initialize('store_search_search_author') self.search_edit.initialize('store_search_search') # Loads variables that store various settings. @@ -60,13 +62,24 @@ class SearchDialog(QDialog, Ui_Dialog): self.setup_store_checks() # Set the search query + # Title + self.search_title.setText(query) + self.search_title.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.search_title.setMinimumContentsLength(25) + # Author + self.search_author.setText(query) + self.search_author.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.search_author.setMinimumContentsLength(25) + # Keyword self.search_edit.setText(query) self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) self.search_edit.setMinimumContentsLength(25) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) - self.top_layout.addWidget(self.pi) + self.button_layout.takeAt(0) + self.button_layout.setAlignment(Qt.AlignCenter) + self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter) self.adv_search_button.setIcon(QIcon(I('search.png'))) self.configure.setIcon(QIcon(I('config.png'))) @@ -152,8 +165,19 @@ class SearchDialog(QDialog, Ui_Dialog): self.results_view.model().clear_results() # Don't start a search if there is nothing to search for. - query = unicode(self.search_edit.text()) + query = [] + if self.search_title.text(): + query.append(u'title:"~%s"' % unicode(self.search_title.text()).replace(" ", ".*")) + if self.search_author.text(): + query.append(u'author2:"%s"' % unicode(self.search_author.text())) + #query.append(u'author:"~%s"' % unicode(self.search_author.text()).replace(" ", ".*")) + if self.search_edit.text(): + query.append(unicode(self.search_edit.text())) + query = " ".join(query) if not query.strip(): + error_dialog(self, _('No query'), + _('You must enter a title, author or keyword to' + ' search for.'), show=True) return # Give the query to the results model so it can do # futher filtering. @@ -188,7 +212,7 @@ class SearchDialog(QDialog, Ui_Dialog): query = query.replace('>', '') query = query.replace('<', '') # Remove the prefix. - for loc in ('all', 'author', 'authors', 'title'): + for loc in ('all', 'author', 'author2', 'authors', 'title'): query = re.sub(r'%s:"(?P[^\s"]+)"' % loc, '\g', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui index 83f42cd0d1..715f6788e7 100644 --- a/src/calibre/gui2/store/search/search.ui +++ b/src/calibre/gui2/store/search/search.ui @@ -14,49 +14,74 @@ Get Books - + :/images/store.png:/images/store.png true - - - - - - - Query: - - - - - - - ... - - - - - - - - 0 - 0 - - - - - - - - Search - - - - + + + + + Search by title + + - + + + + &Author: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + search_author + + + + + + + Search by author + + + + + + + ... + + + + + + + &Keyword: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + search_edit + + + + + + + + 0 + 0 + + + + Search by any keyword + + + + Qt::Horizontal @@ -82,8 +107,8 @@ 0 0 - 193 - 127 + 204 + 141 @@ -202,7 +227,7 @@ - + @@ -234,7 +259,47 @@ - Close + &Close + + + + + + + + + &Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + search_title + + + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + + + + &Search @@ -255,17 +320,19 @@ + search_title + search_author + adv_search_button search_edit search - results_view store_list select_all_stores select_invert_stores select_none_stores + results_view configure open_external close - adv_search_button diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index df2c5ffd1d..997502a9d7 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -594,6 +594,9 @@ class HistoryLineEdit(QComboBox): # {{{ self.setInsertPolicy(self.NoInsert) self.setMaxCount(10) + def setPlaceholderText(self, txt): + return self.lineEdit().setPlaceholderText(txt) + @property def store_name(self): return 'lineedit_history_'+self._name