diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 5292233dc9..d19b8019fa 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -4,12 +4,16 @@ __copyright__ = '2008, Kovid Goyal ' import re, copy from datetime import date -from PyQt5.Qt import QDialog, QDialogButtonBox +from PyQt5.Qt import ( + QDialog, QDialogButtonBox, QFrame, QLabel, QComboBox, QIcon, QVBoxLayout, + QSize, QHBoxLayout, QTabWidget, QLineEdit, QWidget, QGroupBox, QFormLayout, + QSpinBox, QRadioButton +) from calibre import strftime -from calibre.gui2.dialogs.search_ui import Ui_Dialog from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.gui2 import gprefs +from calibre.gui2.complete2 import EditWithComplete from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.utils.date import now @@ -19,6 +23,7 @@ box_values = {} last_matchkind = CONTAINS_MATCH +# UI {{{ def init_dateop(cb): for op, desc in [ ('=', _('equal to')), @@ -34,75 +39,225 @@ def current_dateop(cb): return unicode(cb.itemData(cb.currentIndex()) or '') -class SearchDialog(QDialog, Ui_Dialog): +def create_msg_label(self): + self.frame = f = QFrame(self) + f.setFrameShape(QFrame.StyledPanel) + f.setFrameShadow(QFrame.Raised) + f.l = l = QVBoxLayout(f) + f.um_label = la = QLabel(_( + "

You can also perform other kinds of advanced searches, for example checking" + ' for books that have no covers, combining multiple search expression using Boolean' + ' operators and so on. See the The Search Interface for more information.' + ) % localize_user_manual_link('https://manual.calibre-ebook.com/gui.html#the-search-interface')) + la.setMinimumSize(QSize(150, 0)) + la.setWordWrap(True) + la.setOpenExternalLinks(True) + l.addWidget(la) + return f + + +def create_match_kind(self): + self.cmk_label = la = QLabel(_("What &kind of match to use:")) + self.matchkind = m = QComboBox(self) + la.setBuddy(m) + m.addItems([ + _("Contains: the word or phrase matches anywhere in the metadata field"), + _("Equals: the word or phrase must match the entire metadata field"), + _("Regular expression: the expression must match anywhere in the metadata field"), + ]) + l = QHBoxLayout() + l.addWidget(la), l.addWidget(m) + return l + + +def create_button_box(self): + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.clear_button = bb.addButton(_('Clear'), QDialogButtonBox.ResetRole) + self.clear_button.clicked.connect(self.clear_button_pushed) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + return bb + + +def create_adv_tab(self): + self.adv_tab = w = QWidget(self.tab_widget) + self.tab_widget.addTab(w, _("A&dvanced Search")) + + w.g1 = QGroupBox(_("Find entries that have..."), w) + w.g2 = QGroupBox(("But don't show entries that have..."), w) + w.l = l = QVBoxLayout(w) + l.addWidget(w.g1), l.addWidget(w.g2), l.addStretch(10) + + w.g1.l = l = QFormLayout(w.g1) + l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow) + for key, text in ( + ('all', _("A&ll these words:")), + ('phrase', _("&This exact phrase:")), + ('any', _("O&ne or more of these words:")), + ): + le = QLineEdit(w) + setattr(self, key, le) + l.addRow(text, le) + + w.g2.l = l = QFormLayout(w.g2) + self.none = le = QLineEdit(w) + l.addRow(_("Any of these &unwanted words:"), le) + + +def create_simple_tab(self, db): + self.simple_tab = w = QWidget(self.tab_widget) + self.tab_widget.addTab(w, ("Titl&e/author/series...")) + + w.l = l = QFormLayout(w) + l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow) + + self.title_box = le = QLineEdit(w) + le.setPlaceholderText(_('The title to search for')) + l.addRow(_('&Title:'), le) + + self.authors_box = le = EditWithComplete(self) + le.lineEdit().setPlaceholderText(_('The author to search for')) + le.setEditText('') + le.set_separator('&') + le.set_space_before_sep(True) + le.set_add_separator(tweaks['authors_completer_append_separator']) + le.update_items_cache(db.all_author_names()) + l.addRow(_('&Author:'), le) + + self.series_box = le = EditWithComplete(self) + le.lineEdit().setPlaceholderText(_('The series to search for')) + all_series = sorted((x[1] for x in db.all_series()), key=sort_key) + le.set_separator(None) + le.update_items_cache(all_series) + le.show_initial_value('') + l.addRow(_('&Series:'), le) + + self.tags_box = le = EditWithComplete(self) + le.lineEdit().setPlaceholderText(_('The tags to search for')) + self.tags_box.update_items_cache(db.all_tags()) + l.addRow(_('Ta&gs:'), le) + + searchables = sorted(db.field_metadata.searchable_fields(), + key=lambda x: sort_key(x if x[0] != '#' else x[1:])) + self.general_combo = QComboBox(w) + self.general_combo.addItems(searchables) + self.box_last_values = copy.deepcopy(box_values) + if self.box_last_values: + for k,v in self.box_last_values.items(): + if k == 'general_index': + continue + getattr(self, k).setText(v) + self.general_combo.setCurrentIndex( + self.general_combo.findText(self.box_last_values['general_index'])) + self.general_box = le = QLineEdit(self) + l.addRow(self.general_combo, le) + + +def create_date_tab(self, db): + self.date_tab = w = QWidget(self.tab_widget) + self.tab_widget.addTab(w, ("&Date searches")) + w.l = l = QVBoxLayout(w) + + def a(w): + h.addWidget(w) + return w + + def add(text, w): + w.la = la = QLabel(text) + h.addWidget(la), h.addWidget(w) + la.setBuddy(w) + return w + + w.h1 = h = QHBoxLayout() + l.addLayout(h) + self.date_field = df = add(_("&Search the"), QComboBox(w)) + vals = [((v['search_terms'] or [k])[0], v['name'] or k) for k, v in db.field_metadata.iteritems() if v.get('datatype', None) == 'datetime'] + for k, v in sorted(vals, key=lambda (k, v): sort_key(v)): + df.addItem(v, k) + h.addWidget(df) + self.dateop_date = dd = add(_("date column for books whose &date is "), QComboBox(w)) + init_dateop(dd) + w.la3 = la = QLabel('...') + h.addWidget(la) + h.addStretch(10) + + w.h2 = h = QHBoxLayout() + l.addLayout(h) + self.sel_date = a(QRadioButton(_('&year'), w)) + self.date_year = dy = a(QSpinBox(w)) + dy.setRange(102, 10000) + dy.setValue(now().year) + self.date_month = dm = add(_('mo&nth'), QComboBox(w)) + for val, text in [(0, '')] + [(i, strftime('%B', date(2010, i, 1).timetuple())) for i in xrange(1, 13)]: + dm.addItem(text, val) + self.date_day = dd = add(_('&day'), QSpinBox(w)) + dd.setRange(0, 31) + dd.setSpecialValueText(u' \xa0') + h.addStretch(10) + + w.h3 = h = QHBoxLayout() + l.addLayout(h) + self.sel_daysago = a(QRadioButton('', w)) + self.date_daysago = da = a(QSpinBox(w)) + da.setRange(0, 9999999) + self.date_ago_type = dt = a(QComboBox(w)) + dt.addItems([_('days'), _('weeks'), _('months'), _('years')]) + w.la4 = a(QLabel(' ' + _('ago'))) + h.addStretch(10) + + w.h4 = h = QHBoxLayout() + l.addLayout(h) + self.sel_human = a(QRadioButton('', w)) + self.date_human = dh = a(QComboBox(w)) + for val, text in [('today', _('Today')), ('yesterday', _('Yesterday')), ('thismonth', _('This month'))]: + dh.addItem(text, val) + self.date_year.valueChanged.connect(lambda : self.sel_date.setChecked(True)) + self.date_month.currentIndexChanged.connect(lambda : self.sel_date.setChecked(True)) + self.date_day.valueChanged.connect(lambda : self.sel_date.setChecked(True)) + self.date_daysago.valueChanged.connect(lambda : self.sel_daysago.setChecked(True)) + self.date_human.currentIndexChanged.connect(lambda : self.sel_human.setChecked(True)) + self.sel_date.setChecked(True) + h.addStretch(10) + + l.addStretch(10) + + +def setup_ui(self, db): + self.setWindowTitle(_("Advanced Search")) + self.setWindowIcon(QIcon(I('search.png'))) + self.l = l = QVBoxLayout(self) + self.h = h = QHBoxLayout() + self.v = v = QVBoxLayout() + l.addLayout(h) + h.addLayout(v) + h.addWidget(create_msg_label(self)) + l.addWidget(create_button_box(self)) + v.addLayout(create_match_kind(self)) + self.tab_widget = tw = QTabWidget(self) + v.addWidget(tw) + create_adv_tab(self) + create_simple_tab(self, db) + create_date_tab(self, db) +# }}} + + +class SearchDialog(QDialog): + + mc = '' def __init__(self, parent, db): QDialog.__init__(self, parent) - self.setupUi(self) - self.um_label.setText(self.um_label.text() % localize_user_manual_link('https://manual.calibre-ebook.com/gui.html#the-search-interface')) - for val, text in [(0, '')] + [(i, strftime('%B', date(2010, i, 1).timetuple())) for i in xrange(1, 13)]: - self.date_month.addItem(text, val) - for val, text in [('today', _('Today')), ('yesterday', _('Yesterday')), ('thismonth', _('This month'))]: - self.date_human.addItem(text, val) - self.date_year.setValue(now().year) - self.date_day.setSpecialValueText(u' \xa0') - vals = [((v['search_terms'] or [k])[0], v['name'] or k) for k, v in db.field_metadata.iteritems() if v.get('datatype', None) == 'datetime'] - for k, v in sorted(vals, key=lambda (k, v): sort_key(v)): - self.date_field.addItem(v, k) - - self.date_year.valueChanged.connect(lambda : self.sel_date.setChecked(True)) - self.date_month.currentIndexChanged.connect(lambda : self.sel_date.setChecked(True)) - self.date_day.valueChanged.connect(lambda : self.sel_date.setChecked(True)) - self.date_daysago.valueChanged.connect(lambda : self.sel_daysago.setChecked(True)) - self.date_ago_type.addItems([_('days'), _('weeks'), _('months'), _('years')]) - self.date_human.currentIndexChanged.connect(lambda : self.sel_human.setChecked(True)) - init_dateop(self.dateop_date) - self.sel_date.setChecked(True) - self.mc = '' - searchables = sorted(db.field_metadata.searchable_fields(), - key=lambda x: sort_key(x if x[0] != '#' else x[1:])) - self.general_combo.addItems(searchables) - - all_authors = db.all_authors() - all_authors.sort(key=lambda x : sort_key(x[1])) - self.authors_box.setEditText('') - self.authors_box.set_separator('&') - self.authors_box.set_space_before_sep(True) - self.authors_box.set_add_separator(tweaks['authors_completer_append_separator']) - self.authors_box.update_items_cache(db.all_author_names()) - - all_series = db.all_series() - all_series.sort(key=lambda x : sort_key(x[1])) - self.series_box.set_separator(None) - self.series_box.update_items_cache([x[1] for x in all_series]) - self.series_box.show_initial_value('') - - all_tags = db.all_tags() - self.tags_box.update_items_cache(all_tags) - - self.box_last_values = copy.deepcopy(box_values) - if self.box_last_values: - for k,v in self.box_last_values.items(): - if k == 'general_index': - continue - getattr(self, k).setText(v) - self.general_combo.setCurrentIndex( - self.general_combo.findText(self.box_last_values['general_index'])) - - self.clear_button.clicked.connect(self.clear_button_pushed) + setup_ui(self, db) current_tab = gprefs.get('advanced search dialog current tab', 0) - self.tabWidget.setCurrentIndex(current_tab) + self.tab_widget.setCurrentIndex(current_tab) if current_tab == 1: self.matchkind.setCurrentIndex(last_matchkind) - - self.tabWidget.currentChanged[int].connect(self.tab_changed) - self.tab_changed(current_tab) self.resize(self.sizeHint()) def save_state(self): gprefs['advanced search dialog current tab'] = \ - self.tabWidget.currentIndex() + self.tab_widget.currentIndex() def accept(self): self.save_state() @@ -112,16 +267,20 @@ class SearchDialog(QDialog, Ui_Dialog): self.save_state() return QDialog.reject(self) - def tab_changed(self, idx): - bb = (self.buttonBox, self.tab_2_button_box, self.tab_3_button_box)[idx] - bb.button(QDialogButtonBox.Ok).setDefault(True) - def clear_button_pushed(self): - self.title_box.setText('') - self.authors_box.setText('') - self.series_box.setText('') - self.tags_box.setText('') - self.general_box.setText('') + w = self.tab_widget.currentWidget() + if w is self.date_tab: + for c in w.findChildren(QComboBox): + c.setCurrentIndex(0) + for c in w.findChildren(QSpinBox): + c.setValue(c.minimum()) + self.sel_date.setChecked(True) + self.date_year.setValue(now().year) + else: + for c in w.findChildren(QLineEdit): + c.setText('') + for c in w.findChildren(EditWithComplete): + c.setText('') def tokens(self, raw): phrases = re.findall(r'\s*".*?"\s*', raw) @@ -131,7 +290,7 @@ class SearchDialog(QDialog, Ui_Dialog): return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]] def search_string(self): - i = self.tabWidget.currentIndex() + i = self.tab_widget.currentIndex() return (self.adv_search_string, self.box_search_string, self.date_search_string)[i]() def date_search_string(self): @@ -236,3 +395,13 @@ class SearchDialog(QDialog, Ui_Dialog): if ans: return ' and '.join(ans) return '' + + +if __name__ == '__main__': + from calibre.library import db + db = db() + from calibre.gui2 import Application + app = Application([]) + d = SearchDialog(None, db) + d.exec_() + print(d.search_string()) diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui deleted file mode 100644 index 1994ae7288..0000000000 --- a/src/calibre/gui2/dialogs/search.ui +++ /dev/null @@ -1,669 +0,0 @@ - - - Dialog - - - - 0 - 0 - 872 - 411 - - - - Advanced Search - - - - :/images/search.png:/images/search.png - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 150 - 0 - - - - <p>You can also perform other kinds of advanced searches, for example checking for books that have no covers, combining multiple search expression using Boolean operators and so on. See the <a href="%s">The Search Interface</a> for more information. - - - true - - - true - - - - - - - - - - What &kind of match to use: - - - matchkind - - - - - - - - Contains: the word or phrase matches anywhere in the metadata field - - - - - Equals: the word or phrase must match the entire metadata field - - - - - Regular expression: the expression must match anywhere in the metadata field - - - - - - - - 0 - - - - A&dvanced Search - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - Find entries that have... - - - - - - - - A&ll these words: - - - all - - - - - - - - - - - - - - &This exact phrase: - - - all - - - - - - - - - - - - - - O&ne or more of these words: - - - all - - - - - - - - - - - - - - - But don't show entries that have... - - - - - - - - Any of these &unwanted words: - - - all - - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Titl&e/author/series... - - - - - - &Title: - - - title_box - - - - - - - Enter the title. - - - - - - - &Author: - - - authors_box - - - - - - - &Series: - - - series_box - - - - - - - Ta&gs: - - - tags_box - - - - - - - Enter an author's name. Only one author can be used. - - - - - - - Enter a series name, without an index. Only one series name can be used. - - - - - - - Enter tags separated by spaces - - - - - - - - - - - - - - - &Clear - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Search only in specific fields: - - - - - - - - &Date searches - - - - - - - - &Search the - - - date_field - - - - - - - - - - date column for books whose date is - - - - - - - - - - ... - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - &year - - - date_year - - - - - - - 102 - - - 10000 - - - - - - - mo&nth - - - date_month - - - - - - - - - - &day - - - date_day - - - - - - - 31 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - 999999 - - - - - - - - - - ago - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - - EnLineEdit - QLineEdit -

calibre/gui2/widgets.h
- - - EditWithComplete - QComboBox -
calibre/gui2/complete2.h
-
- - - all - phrase - any - none - buttonBox - title_box - authors_box - series_box - tags_box - general_combo - general_box - clear_button - tab_2_button_box - - - - - - - buttonBox - accepted() - Dialog - accept() - - - 256 - 396 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 324 - 396 - - - 286 - 274 - - - - - tab_3_button_box - accepted() - Dialog - accept() - - - 101 - 370 - - - 697 - 0 - - - - - tab_3_button_box - rejected() - Dialog - reject() - - - 101 - 370 - - - 696 - 36 - - - - - tab_2_button_box - accepted() - Dialog - accept() - - - 508 - 378 - - - 697 - 353 - - - - - tab_2_button_box - rejected() - Dialog - reject() - - - 350 - 376 - - - 697 - 269 - - - - -