Add history and history based completion to search boxes

This commit is contained in:
Kovid Goyal 2009-08-18 21:04:32 -06:00
parent 470b6792a9
commit 4544754e1a
8 changed files with 509 additions and 476 deletions

View File

@ -76,6 +76,12 @@ def _config():
'only take place when the Enter or Return key is pressed.')
c.add_opt('save_to_disk_template_history', default=[],
help='Previously used Save to Disk templates')
c.add_opt('main_search_history', default=[],
help='Search history for the main GUI')
c.add_opt('viewer_search_history', default=[],
help='Search history for the ebook viewer')
c.add_opt('lrf_viewer_search_history', default=[],
help='Search history for the LRF viewer')
return ConfigProxy(c)
config = _config()

View File

@ -8,10 +8,10 @@ from operator import attrgetter
from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, \
QPalette, QImage, QApplication, QMenu, \
QPen, QStyle, QPainter, \
QImage, QApplication, QMenu, \
QStyledItemDelegate, QCompleter
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, \
SIGNAL, QObject, QSize, QModelIndex, QDate
from calibre import strftime
@ -1100,94 +1100,4 @@ class DeviceBooksModel(BooksModel):
self.editable = editable
class SearchBox(QLineEdit):
INTERVAL = 1000 #: Time to wait before emitting search signal
def __init__(self, parent, help_text=_('Search (For Advanced Search click the button to the left)')):
QLineEdit.__init__(self, parent)
self.help_text = help_text
self.initial_state = True
self.as_you_type = True
self.default_palette = QApplication.palette(self)
self.gray = QPalette(self.default_palette)
self.gray.setBrush(QPalette.Text, QBrush(QColor('gray')))
self.prev_search = ''
self.timer = None
self.clear_to_help()
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
def normalize_state(self):
self.setText('')
self.setPalette(self.default_palette)
self.setStyleSheet('QLineEdit { background-color: white; }')
def clear_to_help(self):
self.setPalette(self.gray)
self.setText(self.help_text)
self.home(False)
self.initial_state = True
self.setStyleSheet('QLineEdit { background-color: white; }')
self.emit(SIGNAL('cleared()'))
def clear(self):
self.clear_to_help()
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
def search_done(self, ok):
col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)'
self.setStyleSheet('QLineEdit { background-color: %s; }' % col)
def keyPressEvent(self, event):
if self.initial_state:
self.normalize_state()
self.initial_state = False
if not self.as_you_type:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event):
if self.initial_state:
self.normalize_state()
self.initial_state = False
QLineEdit.mouseReleaseEvent(self, event)
def text_edited_slot(self, text):
if self.as_you_type:
text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
self.prev_text = text
self.timer = self.startTimer(self.__class__.INTERVAL)
def timerEvent(self, event):
self.killTimer(event.timerId())
if event.timerId() == self.timer:
self.do_search()
def do_search(self):
text = qstring_to_unicode(self.text())
refinement = text.startswith(self.prev_search) and ':' not in text
self.prev_search = text
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
def search_from_tokens(self, tokens, all):
ans = u' '.join([u'%s:%s'%x for x in tokens])
if not all:
ans = '[' + ans + ']'
self.set_search_string(ans)
def search_from_tags(self, tags, all):
joiner = ' and ' if all else ' or '
self.set_search_string(joiner.join(tags))
def set_search_string(self, txt):
self.normalize_state()
self.setText(txt)
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
self.end(False)
self.initial_state = False
def search_as_you_type(self, enabled):
self.as_you_type = enabled

View File

@ -15,7 +15,7 @@ from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
from calibre.gui2.main_window import MainWindow
from calibre.gui2.lrf_renderer.document import Document
from calibre.gui2.library import SearchBox
from calibre.gui2.search_box import SearchBox2
class RenderWorker(QThread):
@ -73,7 +73,8 @@ class Main(MainWindow, Ui_MainWindow):
self.slider_action = self.slider = QSlider(Qt.Horizontal)
self.tool_bar.addWidget(self.slider)
self.tool_bar.addSeparator()
self.search = SearchBox(self)
self.search = SearchBox2(self)
self.search.initialize('lrf_viewer_search_history')
self.search_action = self.tool_bar.addWidget(self.search)
QObject.connect(self.document, SIGNAL('chapter_rendered(int)'), self.chapter_rendered)
QObject.connect(self.document, SIGNAL('page_changed(PyQt_PyObject)'), self.page_changed)
@ -123,7 +124,6 @@ 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.help_text = 'Search'
self.search.clear_to_help()
self.last_search = None
else:

View File

@ -121,6 +121,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
Ui_MainWindow.__init__(self)
self.setupUi(self)
self.setWindowTitle(__appname__)
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
self.progress_indicator = ProgressIndicator(self)
self.verbose = opts.verbose
self.get_metadata = GetMetadata()
@ -148,7 +151,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.system_tray_icon.hide()
else:
self.system_tray_icon.show()
self.search.search_as_you_type(config['search_as_you_type'])
self.system_tray_menu = QMenu(self)
self.restore_action = self.system_tray_menu.addAction(
QIcon(':/images/page.svg'), _('&Restore'))

View File

@ -185,33 +185,18 @@
</widget>
</item>
<item>
<widget class="SearchBox" name="search">
<property name="enabled">
<bool>true</bool>
</property>
<widget class="SearchBox2" name="search">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Search the list of books by title or author&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
<string>&lt;p&gt;Search the list of books by title, author, publisher, tags, comments, etc.&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
</property>
<property name="whatsThis">
<string>Search the list of books by title, author, publisher, tags and comments&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="frame">
<bool>true</bool>
<string>&lt;p&gt;Search the list of books by title, author, publisher, tags, comments, etc.&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
</property>
</widget>
</item>
@ -673,11 +658,6 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>SearchBox</class>
<extends>QLineEdit</extends>
<header>library.h</header>
</customwidget>
<customwidget>
<class>BooksView</class>
<extends>QTableView</extends>
@ -698,26 +678,14 @@
<extends>QTreeView</extends>
<header>calibre/gui2/tag_view.h</header>
</customwidget>
<customwidget>
<class>SearchBox2</class>
<extends>QComboBox</extends>
<header>calibre.gui2.search_box</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc"/>
</resources>
<connections>
<connection>
<sender>clear_button</sender>
<signal>clicked()</signal>
<receiver>search</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">
<x>787</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<x>755</x>
<y>213</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>

View File

@ -0,0 +1,148 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList
from calibre.gui2 import config
class SearchLineEdit(QLineEdit):
def keyPressEvent(self, event):
self.emit(SIGNAL('key_pressed(PyQt_PyObject)'), event)
QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event):
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
QLineEdit.mouseReleaseEvent(self, event)
class SearchBox2(QComboBox):
INTERVAL = 1500 #: Time to wait before emitting search signal
MAX_COUNT = 25
def __init__(self, parent=None):
QComboBox.__init__(self, parent)
self.line_edit = SearchLineEdit(self)
self.setLineEdit(self.line_edit)
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
self.key_pressed, Qt.DirectConnection)
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
self.key_pressed, Qt.DirectConnection)
self.setEditable(True)
self.help_state = True
self.as_you_type = True
self.prev_search = ''
self.timer = None
self.setInsertPolicy(self.NoInsert)
self.setMaxCount(self.MAX_COUNT)
def initialize(self, opt_name, colorize=False,
help_text=_('Search')):
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.colorize = colorize
self.clear_to_help()
self.connect(self, SIGNAL('editTextChanged(QString)'), self.text_edited_slot)
def normalize_state(self):
self.setEditText('')
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: white; }')
self.help_state = False
def clear_to_help(self):
self.setEditText(self.help_text)
self.line_edit.home(False)
self.help_state = True
self.line_edit.setStyleSheet('QLineEdit { color: gray; background-color: white; }')
self.emit(SIGNAL('cleared()'))
def text(self):
return self.currentText()
def clear(self):
self.clear_to_help()
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
def search_done(self, ok):
if not unicode(self.currentText()).strip():
return self.clear_to_help()
col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)'
if not self.colorize:
col = 'white'
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
def key_pressed(self, event):
if self.help_state:
self.normalize_state()
if not self.as_you_type:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
def mouse_released(self, event):
if self.help_state:
self.normalize_state()
def text_edited_slot(self, text):
if self.as_you_type:
text = unicode(text)
self.prev_text = text
self.timer = self.startTimer(self.__class__.INTERVAL)
def timerEvent(self, event):
self.killTimer(event.timerId())
if event.timerId() == self.timer:
self.do_search()
def do_search(self):
text = unicode(self.currentText()).strip()
if not text or text == self.help_text:
return self.clear()
self.help_state = False
refinement = text.startswith(self.prev_search) and ':' not in text
self.prev_search = text
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
idx = self.findText(text, Qt.MatchFixedString)
self.block_signals(True)
if idx < 0:
self.insertItem(0, text)
else:
t = self.itemText(idx)
self.removeItem(idx)
self.insertItem(0, t)
self.setCurrentIndex(0)
self.block_signals(False)
config[self.opt_name] = [unicode(self.itemText(i)) for i in
range(self.count())]
def block_signals(self, yes):
self.blockSignals(yes)
self.line_edit.blockSignals(yes)
def search_from_tokens(self, tokens, all):
ans = u' '.join([u'%s:%s'%x for x in tokens])
if not all:
ans = '[' + ans + ']'
self.set_search_string(ans)
def search_from_tags(self, tags, all):
joiner = ' and ' if all else ' or '
self.set_search_string(joiner.join(tags))
def set_search_string(self, txt):
self.normalize_state()
self.setEditText(txt)
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
self.line_edit.end(False)
self.initial_state = False
def search_as_you_type(self, enabled):
self.as_you_type = enabled

View File

@ -21,7 +21,7 @@ from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux
from calibre.utils.config import Config, StringConfig, dynamic
from calibre.gui2.library import SearchBox
from calibre.gui2.search_box import SearchBox2
from calibre.ebooks.metadata import MetaInformation
from calibre.customize.ui import available_input_formats
@ -218,7 +218,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.tool_bar2.insertWidget(self.action_find_next, self.reference)
self.tool_bar2.insertSeparator(self.action_find_next)
self.setFocusPolicy(Qt.StrongFocus)
self.search = SearchBox(self, _('Search'))
self.search = SearchBox2(self)
self.search.initialize('viewer_search_history')
self.search.setToolTip(_('Search for text in book'))
self.tool_bar2.insertWidget(self.action_find_next, self.search)
self.view.set_manager(self)
@ -408,10 +409,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
def find(self, text, refinement, repeat=False):
if not text:
return
return self.search.search_done(False)
if self.view.search(text):
self.scrolled(self.view.scroll_fraction)
return
return self.search.search_done(True)
index = self.iterator.search(text, self.current_index)
if index is None:
if self.current_index > 0:
@ -419,8 +420,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if index is None:
info_dialog(self, _('No matches found'),
_('No matches found for: %s')%text).exec_()
return
return
return self.search.search_done(True)
return self.search.search_done(True)
self.pending_search = text
self.load_path(self.iterator.spine[index])

File diff suppressed because it is too large Load Diff