From bb6158f57e05ff4d0da9ad529094981f46ac2b1c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 21 May 2017 07:54:07 +0530 Subject: [PATCH] Refactor the advanced search dialog to use modern UI building techniques, getting rid of the .ui file Implement the Clear button for all tabs and also add placeholder text where appropriate. --- src/calibre/gui2/dialogs/search.py | 309 ++++++++++--- src/calibre/gui2/dialogs/search.ui | 669 ----------------------------- 2 files changed, 239 insertions(+), 739 deletions(-) delete mode 100644 src/calibre/gui2/dialogs/search.ui 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 - - - - -