From 449de8255f19acb1fd8bab750efe67885fcaf22d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 30 Nov 2010 12:56:27 +0000 Subject: [PATCH] Refactor search boxes to use placeholders, emit refocus events when appropriate, and improve the selected-text model. Also many general cleanups, such as renaming clear_to_help to clear. --- src/calibre/gui2/lrf_renderer/main.py | 2 +- src/calibre/gui2/search_box.py | 214 ++++++++----------- src/calibre/gui2/search_restriction_mixin.py | 4 +- src/calibre/gui2/tag_view.py | 10 +- src/calibre/gui2/ui.py | 4 +- src/calibre/gui2/viewer/main.py | 4 +- 6 files changed, 97 insertions(+), 141 deletions(-) diff --git a/src/calibre/gui2/lrf_renderer/main.py b/src/calibre/gui2/lrf_renderer/main.py index 8ddda175fa..2acfd3c9a7 100644 --- a/src/calibre/gui2/lrf_renderer/main.py +++ b/src/calibre/gui2/lrf_renderer/main.py @@ -127,7 +127,7 @@ class Main(MainWindow, Ui_MainWindow): self.progress_label.setText('Parsing '+ self.file_name) self.renderer = RenderWorker(self, stream, self.logger, self.opts) QObject.connect(self.renderer, SIGNAL('finished()'), self.parsed, Qt.QueuedConnection) - self.search.clear_to_help() + self.search.clear() self.last_search = None else: self.stack.setCurrentIndex(0) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index b37d74f51f..444ba156ca 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -8,9 +8,8 @@ __docformat__ = 'restructuredtext en' import re -from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \ - pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \ - QAction, QKeySequence, QTimer +from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \ + pyqtSignal, QCompleter, QAction, QKeySequence, QTimer from calibre.gui2 import config from calibre.gui2.dialogs.confirm_delete import confirm @@ -20,35 +19,30 @@ from calibre.utils.search_query_parser import saved_searches class SearchLineEdit(QLineEdit): key_pressed = pyqtSignal(object) - mouse_released = pyqtSignal(object) - focus_out = pyqtSignal(object) def keyPressEvent(self, event): self.key_pressed.emit(event) QLineEdit.keyPressEvent(self, event) def mouseReleaseEvent(self, event): - self.mouse_released.emit(event) QLineEdit.mouseReleaseEvent(self, event) + QLineEdit.selectAll(self) - def focusOutEvent(self, event): - self.focus_out.emit(event) - QLineEdit.focusOutEvent(self, event) + def focusInEvent(self, event): + QLineEdit.focusInEvent(self, event) + QLineEdit.selectAll(self) def dropEvent(self, ev): - if self.parent().help_state: - self.parent().normalize_state() + self.parent().normalize_state() return QLineEdit.dropEvent(self, ev) def contextMenuEvent(self, ev): - if self.parent().help_state: - self.parent().normalize_state() + self.parent().normalize_state() return QLineEdit.contextMenuEvent(self, ev) @pyqtSlot() def paste(self, *args): - if self.parent().help_state: - self.parent().normalize_state() + self.parent().normalize_state() return QLineEdit.paste(self) class SearchBox2(QComboBox): @@ -59,14 +53,17 @@ class SearchBox2(QComboBox): * Call initialize() * Connect to the search() and cleared() signals from this widget. * Connect to the cleared() signal to know when the box content changes + * Connect to focus_to_library signal to be told to manually change focus * Call search_done() after every search is complete - * Use clear() to clear back to the help message ''' INTERVAL = 1500 #: Time to wait before emitting search signal MAX_COUNT = 25 - search = pyqtSignal(object) + search = pyqtSignal(object) + cleared = pyqtSignal() + changed = pyqtSignal() + focus_to_library = pyqtSignal() def __init__(self, parent=None): QComboBox.__init__(self, parent) @@ -75,13 +72,9 @@ class SearchBox2(QComboBox): self.setLineEdit(self.line_edit) c = self.line_edit.completer() c.setCompletionMode(c.PopupCompletion) - self.line_edit.key_pressed.connect(self.key_pressed, - type=Qt.DirectConnection) - self.line_edit.mouse_released.connect(self.mouse_released, - type=Qt.DirectConnection) + self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) self.activated.connect(self.history_selected) self.setEditable(True) - self.help_state = False self.as_you_type = True self.prev_search = '' self.timer = QTimer() @@ -98,54 +91,37 @@ class SearchBox2(QComboBox): self.as_you_type = config['search_as_you_type'] self.opt_name = opt_name self.addItems(QStringList(list(set(config[opt_name])))) - self.help_text = help_text + self.line_edit.setPlaceholderText(help_text) self.colorize = colorize - self.clear_to_help() + self.clear() def normalize_state(self): self.setToolTip(self.tool_tip_text) - if self.help_state: - self.setEditText('') - self.line_edit.setStyleSheet( - 'QLineEdit { color: black; background-color: %s; }' % - self.normal_background) - self.help_state = False - else: - self.line_edit.setStyleSheet( - 'QLineEdit { color: black; background-color: %s; }' % - self.normal_background) - - def clear_to_help(self): - self.setToolTip(self.tool_tip_text) - if self.help_state: - return - self.help_state = True - self.search.emit('') - self._in_a_search = False - self.setEditText(self.help_text) - self.line_edit.home(False) self.line_edit.setStyleSheet( - 'QLineEdit { color: gray; background-color: %s; }' % - self.normal_background) - self.emit(SIGNAL('cleared()')) + 'QLineEdit{color:black;background-color:%s;}' % self.normal_background) def text(self): return self.currentText() def clear(self): - self.clear_to_help() + self.normalize_state() + self.setEditText('') + self.search.emit('') + self._in_a_search = False + self.cleared.emit() def search_done(self, ok): if isinstance(ok, basestring): self.setToolTip(ok) ok = False if not unicode(self.currentText()).strip(): - return self.clear_to_help() + self.clear() + return self._in_a_search = ok col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)' if not self.colorize: col = self.normal_background - self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col) + self.line_edit.setStyleSheet('QLineEdit{color:black;background-color:%s;}' % col) def key_pressed(self, event): k = event.key() @@ -154,40 +130,25 @@ class SearchBox2(QComboBox): return self.normalize_state() if self._in_a_search: - self.emit(SIGNAL('changed()')) + self.changed.emit() self._in_a_search = False if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.do_search() + self.focus_to_library.emit() if self.as_you_type: self.timer.start(1500) - def mouse_released(self, event): - self.normalize_state() - # Dont trigger a search since it make - # re-positioning the cursor using the mouse - # impossible - #if self.as_you_type: - # self.timer.start(1500) - def timer_event(self): self.do_search() def history_selected(self, text): - self.emit(SIGNAL('changed()')) + self.changed.emit() self.do_search() - @property - def smart_text(self): - text = unicode(self.currentText()).strip() - if not text or text == self.help_text: - return '' - return text - def do_search(self, *args): text = unicode(self.currentText()).strip() - if not text or text == self.help_text: + if not text: return self.clear() - self.help_state = False self.prev_search = text self.search.emit(text) @@ -220,7 +181,7 @@ class SearchBox2(QComboBox): def set_search_string(self, txt): if not txt: - self.clear_to_help() + self.clear() return self.normalize_state() self.setEditText(txt) @@ -243,25 +204,24 @@ class SavedSearchBox(QComboBox): if you care about changes to the list of saved searches. ''' + changed = pyqtSignal() + focus_to_library = pyqtSignal() + def __init__(self, parent=None): QComboBox.__init__(self, parent) self.normal_background = 'rgb(255, 255, 255, 0%)' self.line_edit = SearchLineEdit(self) self.setLineEdit(self.line_edit) - self.line_edit.key_pressed.connect(self.key_pressed, - type=Qt.DirectConnection) - self.line_edit.mouse_released.connect(self.mouse_released, - type=Qt.DirectConnection) - self.line_edit.focus_out.connect(self.focus_out, - type=Qt.DirectConnection) + self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) self.activated[str].connect(self.saved_search_selected) - completer = QCompleter(self) # turn off auto-completion + # Turn off auto-completion so that it doesn't interfere with typing + # names of new searches. + completer = QCompleter(self) self.setCompleter(completer) + self.setEditable(True) - self.help_state = True - self.prev_search = '' self.setInsertPolicy(self.NoInsert) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(10) @@ -269,50 +229,40 @@ class SavedSearchBox(QComboBox): def initialize(self, _search_box, colorize=False, help_text=_('Search')): self.search_box = _search_box - self.help_text = help_text + self.line_edit.setPlaceholderText(help_text) self.colorize = colorize - self.clear_to_help() + self.clear() def normalize_state(self): - self.setEditText('') - self.line_edit.setStyleSheet( - 'QLineEdit { color: black; background-color: %s; }' % - self.normal_background) - self.help_state = False + # need this because line_edit will call it in some cases such as paste + pass - def clear_to_help(self): - self.setToolTip(self.tool_tip_text) + def clear(self): + QComboBox.clear(self) self.initialize_saved_search_names() - self.setEditText(self.help_text) + self.setEditText('') self.line_edit.home(False) - self.help_state = True - self.line_edit.setStyleSheet( - 'QLineEdit { color: gray; background-color: %s; }' % - self.normal_background) - - def focus_out(self, event): - if self.currentText() == '': - self.clear_to_help() def key_pressed(self, event): - if self.help_state: - self.normalize_state() - - def mouse_released(self, event): - if self.help_state: - self.normalize_state() + if event.key() in (Qt.Key_Return, Qt.Key_Enter): + self.saved_search_selected(self.currentText()) + self.focus_to_library.emit() def saved_search_selected(self, qname): qname = unicode(qname) if qname is None or not qname.strip(): + self.search_box.clear() + return + if not saved_searches().lookup(qname): + self.search_box.clear() + self.setEditText(qname) return - self.normalize_state() self.search_box.set_search_string(u'search:"%s"' % qname) self.setEditText(qname) self.setToolTip(saved_searches().lookup(qname)) + self.focus_to_library.emit() def initialize_saved_search_names(self): - self.clear() qnames = saved_searches().names() self.addItems(qnames) self.setCurrentIndex(-1) @@ -330,25 +280,24 @@ class SavedSearchBox(QComboBox): if ss is None: return saved_searches().delete(unicode(self.currentText())) - self.clear_to_help() - self.search_box.clear_to_help() - self.emit(SIGNAL('changed()')) + self.clear() + self.search_box.clear() + self.changed.emit() # SIGNALed from the main UI def save_search_button_clicked(self): name = unicode(self.currentText()) - if self.help_state or not name.strip(): + if not name.strip(): name = unicode(self.search_box.text()).replace('"', '') saved_searches().delete(name) saved_searches().add(name, unicode(self.search_box.text())) # now go through an initialization cycle to ensure that the combobox has # the new search in it, that it is selected, and that the search box # references the new search instead of the text in the search. - self.clear_to_help() - self.normalize_state() + self.clear() self.setCurrentIndex(self.findText(name)) self.saved_search_selected (name) - self.emit(SIGNAL('changed()')) + self.changed.emit() # SIGNALed from the main UI def copy_search_button_clicked (self): @@ -362,11 +311,11 @@ class SearchBoxMixin(object): def __init__(self): self.search.initialize('main_search_history', colorize=True, help_text=_('Search (For Advanced Search click the button to the left)')) - self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) - self.connect(self.search, SIGNAL('changed()'), self.search_box_changed) - self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear) - QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), - self.do_advanced_search) + self.search.cleared.connect(self.search_box_cleared) + self.search.changed.connect(self.search_box_changed) + self.search.focus_to_library.connect(self.focus_to_library) + self.clear_button.clicked.connect(self.search.clear) + self.advanced_search_button.clicked[bool].connect(self.do_advanced_search) self.search.clear() self.search.setMaximumWidth(self.width()-150) @@ -384,11 +333,11 @@ class SearchBoxMixin(object): def search_box_cleared(self): self.tags_view.clear() - self.saved_search.clear_to_help() + self.saved_search.clear() self.set_number_of_books_shown() def search_box_changed(self): - self.saved_search.clear_to_help() + self.saved_search.clear() self.tags_view.clear() def do_advanced_search(self, *args): @@ -396,20 +345,24 @@ class SearchBoxMixin(object): if d.exec_() == QDialog.Accepted: self.search.set_search_string(d.search_string()) + def focus_to_library(self): + self.current_view().setFocus(Qt.OtherFocusReason) + class SavedSearchBoxMixin(object): def __init__(self): - self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) + self.saved_search.changed.connect(self.saved_searches_changed) + self.clear_button.clicked.connect(self.saved_search.clear) + self.saved_search.focus_to_library.connect(self.focus_to_library) + self.save_search_button.clicked.connect( + self.saved_search.save_search_button_clicked) + self.delete_search_button.clicked.connect( + self.saved_search.delete_search_button_clicked) + self.copy_search_button.clicked.connect( + self.saved_search.copy_search_button_clicked) self.saved_searches_changed() - self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) self.saved_search.initialize(self.search, colorize=True, help_text=_('Saved Searches')) - self.connect(self.save_search_button, SIGNAL('clicked()'), - self.saved_search.save_search_button_clicked) - self.connect(self.delete_search_button, SIGNAL('clicked()'), - self.saved_search.delete_search_button_clicked) - self.connect(self.copy_search_button, SIGNAL('clicked()'), - self.saved_search.copy_search_button_clicked) self.saved_search.setToolTip( _('Choose saved search or enter name for new saved search')) self.saved_search.setStatusTip(self.saved_search.toolTip()) @@ -420,7 +373,8 @@ class SavedSearchBoxMixin(object): def saved_searches_changed(self): p = sorted(saved_searches().names(), cmp=lambda x,y: cmp(x.lower(), y.lower())) t = unicode(self.search_restriction.currentText()) - self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches + # rebuild the restrictions combobox using current saved searches + self.search_restriction.clear() self.search_restriction.addItem('') self.tags_view.recount() for s in p: @@ -433,6 +387,8 @@ class SavedSearchBoxMixin(object): d.exec_() if d.result() == d.Accepted: self.saved_searches_changed() - self.saved_search.clear_to_help() + self.saved_search.clear() + def focus_to_library(self): + self.current_view().setFocus(Qt.OtherFocusReason) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 139d7c551d..6373e452e5 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -49,8 +49,8 @@ class SearchRestrictionMixin(object): restriction = '' self.restriction_count_of_books_in_view = \ self.library_view.model().set_search_restriction(restriction) - self.search.clear_to_help() - self.saved_search.clear_to_help() + self.search.clear() + self.saved_search.clear() self.tags_view.set_search_restriction(restriction) self.set_number_of_books_shown() diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 7210afd770..b841706439 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -843,7 +843,7 @@ class TagBrowserMixin(object): # {{{ self.tags_view.set_database(self.library_view.model().db, self.tag_match, self.sort_by) self.tags_view.tags_marked.connect(self.search.search_from_tags) - self.tags_view.tags_marked.connect(self.saved_search.clear_to_help) + self.tags_view.tags_marked.connect(self.saved_search.clear) self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) @@ -910,14 +910,14 @@ class TagBrowserMixin(object): # {{{ self.library_view.model().refresh() self.tags_view.set_new_model() self.tags_view.recount() - self.saved_search.clear_to_help() - self.search.clear_to_help() + self.saved_search.clear() + self.search.clear() def do_tag_item_renamed(self): # Clean up library view and search self.library_view.model().refresh() - self.saved_search.clear_to_help() - self.search.clear_to_help() + self.saved_search.clear() + self.search.clear() def do_author_sort_edit(self, parent, id): db = self.library_view.model().db diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 00bba2b491..cb25f75d4a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -383,8 +383,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.tags_view.set_database(db, self.tag_match, self.sort_by) self.library_view.model().set_book_on_device_func(self.book_on_device) self.status_bar.clear_message() - self.search.clear_to_help() - self.saved_search.clear_to_help() + self.search.clear() + self.saved_search.clear() self.book_details.reset_info() self.library_view.model().count_changed() prefs['library_path'] = self.library_path diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index f9a2432099..8fe176751d 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -237,9 +237,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.connect(self.action_previous_page, SIGNAL('triggered(bool)'), lambda x:self.view.previous_page()) self.connect(self.action_find_next, SIGNAL('triggered(bool)'), - lambda x:self.find(self.search.smart_text, repeat=True)) + lambda x:self.find(self.search.text(), repeat=True)) self.connect(self.action_find_previous, SIGNAL('triggered(bool)'), - lambda x:self.find(self.search.smart_text, + lambda x:self.find(self.search.text(), repeat=True, backwards=True)) self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),