mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Use QCompleter instead of custom replacement for it, simplifies the code greatly.
This commit is contained in:
parent
d6d4f9d444
commit
5e089ea376
@ -6,113 +6,14 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, \
|
from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \
|
||||||
QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \
|
QApplication, QCompleter
|
||||||
QStyle, QEvent, pyqtSignal
|
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.icu import sort_key, lower
|
from calibre.utils.icu import sort_key, lower
|
||||||
from calibre.gui2 import NONE
|
from calibre.gui2 import NONE
|
||||||
from calibre.gui2.widgets import EnComboBox
|
from calibre.gui2.widgets import EnComboBox
|
||||||
|
|
||||||
class CompleterItemDelegate(QItemDelegate): # {{{
|
|
||||||
|
|
||||||
''' Renders the current item as thought it were selected '''
|
|
||||||
|
|
||||||
def __init__(self, view):
|
|
||||||
self.view = view
|
|
||||||
QItemDelegate.__init__(self, view)
|
|
||||||
|
|
||||||
def paint(self, p, opt, idx):
|
|
||||||
opt = QStyleOptionViewItem(opt)
|
|
||||||
opt.showDecorationSelected = True
|
|
||||||
if self.view.currentIndex() == idx:
|
|
||||||
opt.state |= QStyle.State_HasFocus
|
|
||||||
QItemDelegate.paint(self, p, opt, idx)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class CompleteWindow(QListView): # {{{
|
|
||||||
|
|
||||||
'''
|
|
||||||
The completion popup. For keyboard and mouse handling see
|
|
||||||
:meth:`eventFilter`.
|
|
||||||
'''
|
|
||||||
|
|
||||||
#: This signal is emitted when the user selects one of the listed
|
|
||||||
#: completions, by mouse or keyboard
|
|
||||||
completion_selected = pyqtSignal(object)
|
|
||||||
|
|
||||||
def __init__(self, widget, model):
|
|
||||||
self.widget = widget
|
|
||||||
QListView.__init__(self)
|
|
||||||
self.setVisible(False)
|
|
||||||
self.setParent(None, Qt.Popup)
|
|
||||||
self.setAlternatingRowColors(True)
|
|
||||||
self.setFocusPolicy(Qt.NoFocus)
|
|
||||||
self._d = CompleterItemDelegate(self)
|
|
||||||
self.setItemDelegate(self._d)
|
|
||||||
self.setModel(model)
|
|
||||||
self.setFocusProxy(widget)
|
|
||||||
self.installEventFilter(self)
|
|
||||||
self.clicked.connect(self.do_selected)
|
|
||||||
self.entered.connect(self.do_entered)
|
|
||||||
self.setMouseTracking(True)
|
|
||||||
|
|
||||||
def do_entered(self, idx):
|
|
||||||
if idx.isValid():
|
|
||||||
self.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def do_selected(self, idx=None):
|
|
||||||
idx = self.currentIndex() if idx is None else idx
|
|
||||||
if idx.isValid():
|
|
||||||
data = unicode(self.model().data(idx, Qt.DisplayRole))
|
|
||||||
self.completion_selected.emit(data)
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
def eventFilter(self, o, e):
|
|
||||||
if o is not self:
|
|
||||||
return False
|
|
||||||
if e.type() == e.KeyPress:
|
|
||||||
key = e.key()
|
|
||||||
if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
|
|
||||||
(key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
|
|
||||||
self.hide()
|
|
||||||
return True
|
|
||||||
elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
|
|
||||||
if key == Qt.Key_Tab and not self.currentIndex().isValid():
|
|
||||||
if self.model().rowCount() > 0:
|
|
||||||
self.setCurrentIndex(self.model().index(0))
|
|
||||||
self.do_selected()
|
|
||||||
return True
|
|
||||||
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
|
|
||||||
Qt.Key_PageDown):
|
|
||||||
return False
|
|
||||||
# Send key event to associated line edit
|
|
||||||
self.widget.eat_focus_out = False
|
|
||||||
try:
|
|
||||||
self.widget.event(e)
|
|
||||||
finally:
|
|
||||||
self.widget.eat_focus_out = True
|
|
||||||
if not self.widget.hasFocus():
|
|
||||||
# Line edit lost focus
|
|
||||||
self.hide()
|
|
||||||
if e.isAccepted():
|
|
||||||
# Line edit consumed event
|
|
||||||
return True
|
|
||||||
elif e.type() == e.MouseButtonPress:
|
|
||||||
# Hide popup if user clicks outside it, otherwise
|
|
||||||
# pass event to popup
|
|
||||||
if not self.underMouse():
|
|
||||||
self.hide()
|
|
||||||
return True
|
|
||||||
elif e.type() in (e.InputMethod, e.ShortcutOverride):
|
|
||||||
QApplication.sendEvent(self.widget, e)
|
|
||||||
|
|
||||||
return False # Do not filter this event
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class CompleteModel(QAbstractListModel):
|
class CompleteModel(QAbstractListModel):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -120,43 +21,25 @@ class CompleteModel(QAbstractListModel):
|
|||||||
self.sep = ','
|
self.sep = ','
|
||||||
self.space_before_sep = False
|
self.space_before_sep = False
|
||||||
self.items = []
|
self.items = []
|
||||||
self.lowered_items = []
|
|
||||||
self.matches = []
|
|
||||||
|
|
||||||
def set_items(self, items):
|
def set_items(self, items):
|
||||||
items = [unicode(x.strip()) for x in items]
|
items = [unicode(x.strip()) for x in items]
|
||||||
self.items = list(sorted(items, key=lambda x: sort_key(x)))
|
self.items = list(sorted(items, key=lambda x: sort_key(x)))
|
||||||
self.lowered_items = [lower(x) for x in self.items]
|
self.lowered_items = [lower(x) for x in self.items]
|
||||||
self.matches = []
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def rowCount(self, *args):
|
def rowCount(self, *args):
|
||||||
return len(self.matches)
|
return len(self.items)
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
r = index.row()
|
r = index.row()
|
||||||
try:
|
try:
|
||||||
return self.matches[r]
|
return self.items[r]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def get_matches(self, prefix):
|
|
||||||
'''
|
|
||||||
Return all matches that (case insensitively) start with prefix
|
|
||||||
'''
|
|
||||||
prefix = lower(prefix)
|
|
||||||
ans = []
|
|
||||||
if prefix:
|
|
||||||
for i, test in enumerate(self.lowered_items):
|
|
||||||
if test.startswith(prefix):
|
|
||||||
ans.append(self.items[i])
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def update_matches(self, matches):
|
|
||||||
self.matches = matches
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
class MultiCompleteLineEdit(QLineEdit):
|
class MultiCompleteLineEdit(QLineEdit):
|
||||||
'''
|
'''
|
||||||
@ -170,17 +53,19 @@ class MultiCompleteLineEdit(QLineEdit):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
self.eat_focus_out = True
|
|
||||||
self.max_visible_items = 7
|
|
||||||
self.current_prefix = None
|
|
||||||
QLineEdit.__init__(self, parent)
|
QLineEdit.__init__(self, parent)
|
||||||
|
|
||||||
self._model = CompleteModel(parent=self)
|
self._model = CompleteModel(parent=self)
|
||||||
self.complete_window = CompleteWindow(self, self._model)
|
self._completer = c = QCompleter(self._model, self)
|
||||||
self.textEdited.connect(self.text_edited)
|
c.setWidget(self)
|
||||||
self.complete_window.completion_selected.connect(self.completion_selected,
|
c.setCompletionMode(QCompleter.PopupCompletion)
|
||||||
|
c.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
|
c.setModelSorting(QCompleter.CaseInsensitivelySortedModel)
|
||||||
|
c.setCompletionRole(Qt.DisplayRole)
|
||||||
|
c.popup().setAlternatingRowColors(True)
|
||||||
|
|
||||||
|
c.activated.connect(self.completion_selected,
|
||||||
type=Qt.QueuedConnection)
|
type=Qt.QueuedConnection)
|
||||||
self.installEventFilter(self)
|
self.textEdited.connect(self.text_edited)
|
||||||
|
|
||||||
# Interface {{{
|
# Interface {{{
|
||||||
def update_items_cache(self, complete_items):
|
def update_items_cache(self, complete_items):
|
||||||
@ -194,23 +79,12 @@ class MultiCompleteLineEdit(QLineEdit):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def eventFilter(self, o, e):
|
|
||||||
if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut:
|
|
||||||
if self.complete_window.isVisible():
|
|
||||||
return True # Filter this event since the cw is visible
|
|
||||||
return QLineEdit.eventFilter(self, o, e)
|
|
||||||
|
|
||||||
def hide_completion_window(self):
|
|
||||||
self.complete_window.hide()
|
|
||||||
|
|
||||||
|
|
||||||
def text_edited(self, *args):
|
def text_edited(self, *args):
|
||||||
self.update_completions()
|
self.update_completions()
|
||||||
|
self._completer.complete()
|
||||||
|
|
||||||
def update_completions(self):
|
def update_completions(self):
|
||||||
' Update the list of completions '
|
' Update the list of completions '
|
||||||
if not self.complete_window.isVisible() and not self.hasFocus():
|
|
||||||
return
|
|
||||||
cpos = self.cursorPosition()
|
cpos = self.cursorPosition()
|
||||||
text = unicode(self.text())
|
text = unicode(self.text())
|
||||||
prefix = text[:cpos]
|
prefix = text[:cpos]
|
||||||
@ -218,9 +92,7 @@ class MultiCompleteLineEdit(QLineEdit):
|
|||||||
complete_prefix = prefix.lstrip()
|
complete_prefix = prefix.lstrip()
|
||||||
if self.sep:
|
if self.sep:
|
||||||
complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
|
complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
|
||||||
|
self._completer.setCompletionPrefix(complete_prefix)
|
||||||
matches = self._model.get_matches(complete_prefix)
|
|
||||||
self.update_complete_window(matches)
|
|
||||||
|
|
||||||
def get_completed_text(self, text):
|
def get_completed_text(self, text):
|
||||||
'''
|
'''
|
||||||
@ -247,7 +119,7 @@ class MultiCompleteLineEdit(QLineEdit):
|
|||||||
|
|
||||||
|
|
||||||
def completion_selected(self, text):
|
def completion_selected(self, text):
|
||||||
prefix_len, ctext = self.get_completed_text(text)
|
prefix_len, ctext = self.get_completed_text(unicode(text))
|
||||||
if self.sep is None:
|
if self.sep is None:
|
||||||
self.setText(ctext)
|
self.setText(ctext)
|
||||||
self.setCursorPosition(len(ctext))
|
self.setCursorPosition(len(ctext))
|
||||||
@ -256,49 +128,6 @@ class MultiCompleteLineEdit(QLineEdit):
|
|||||||
self.setText(ctext)
|
self.setText(ctext)
|
||||||
self.setCursorPosition(cursor_pos - prefix_len + len(text))
|
self.setCursorPosition(cursor_pos - prefix_len + len(text))
|
||||||
|
|
||||||
def update_complete_window(self, matches):
|
|
||||||
self._model.update_matches(matches)
|
|
||||||
if matches:
|
|
||||||
self.show_complete_window()
|
|
||||||
else:
|
|
||||||
self.complete_window.hide()
|
|
||||||
|
|
||||||
|
|
||||||
def position_complete_window(self):
|
|
||||||
popup = self.complete_window
|
|
||||||
screen = QApplication.desktop().availableGeometry(self)
|
|
||||||
h = (popup.sizeHintForRow(0) * min(self.max_visible_items,
|
|
||||||
popup.model().rowCount()) + 3) + 3
|
|
||||||
hsb = popup.horizontalScrollBar()
|
|
||||||
if hsb and hsb.isVisible():
|
|
||||||
h += hsb.sizeHint().height()
|
|
||||||
|
|
||||||
rh = self.height()
|
|
||||||
pos = self.mapToGlobal(QPoint(0, self.height() - 2))
|
|
||||||
w = self.width()
|
|
||||||
|
|
||||||
if w > screen.width():
|
|
||||||
w = screen.width()
|
|
||||||
if (pos.x() + w) > (screen.x() + screen.width()):
|
|
||||||
pos.setX(screen.x() + screen.width() - w)
|
|
||||||
if (pos.x() < screen.x()):
|
|
||||||
pos.setX(screen.x())
|
|
||||||
|
|
||||||
top = pos.y() - rh - screen.top() + 2
|
|
||||||
bottom = screen.bottom() - pos.y()
|
|
||||||
h = max(h, popup.minimumHeight())
|
|
||||||
if h > bottom:
|
|
||||||
h = min(max(top, bottom), h)
|
|
||||||
if top > bottom:
|
|
||||||
pos.setY(pos.y() - h - rh + 2)
|
|
||||||
|
|
||||||
popup.setGeometry(pos.x(), pos.y(), w, h)
|
|
||||||
|
|
||||||
|
|
||||||
def show_complete_window(self):
|
|
||||||
self.position_complete_window()
|
|
||||||
self.complete_window.show()
|
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def all_items(self):
|
def all_items(self):
|
||||||
def fget(self):
|
def fget(self):
|
||||||
|
@ -24,7 +24,8 @@ from calibre.ebooks.conversion.config import GuiRecommendations, \
|
|||||||
load_defaults, load_specifics, save_specifics
|
load_defaults, load_specifics, save_specifics
|
||||||
from calibre.gui2.convert import bulk_defaults_for_input_format
|
from calibre.gui2.convert import bulk_defaults_for_input_format
|
||||||
|
|
||||||
def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format=None):
|
def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
|
||||||
|
out_format=None):
|
||||||
changed = False
|
changed = False
|
||||||
jobs = []
|
jobs = []
|
||||||
bad = []
|
bad = []
|
||||||
@ -95,7 +96,9 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
|
|||||||
msg).exec_()
|
msg).exec_()
|
||||||
|
|
||||||
return jobs, changed, bad
|
return jobs, changed, bad
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Bulk convert {{{
|
||||||
def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]):
|
def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]):
|
||||||
total = len(book_ids)
|
total = len(book_ids)
|
||||||
if total == 0:
|
if total == 0:
|
||||||
@ -207,7 +210,9 @@ class QueueBulk(QProgressDialog):
|
|||||||
self.jobs.reverse()
|
self.jobs.reverse()
|
||||||
self.queue(self.jobs, self.changed, self.bad, *self.args)
|
self.queue(self.jobs, self.changed, self.bad, *self.args)
|
||||||
|
|
||||||
def fetch_scheduled_recipe(arg):
|
# }}}
|
||||||
|
|
||||||
|
def fetch_scheduled_recipe(arg): # {{{
|
||||||
fmt = prefs['output_format'].lower()
|
fmt = prefs['output_format'].lower()
|
||||||
pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower())
|
pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower())
|
||||||
pt.close()
|
pt.close()
|
||||||
@ -248,7 +253,9 @@ def fetch_scheduled_recipe(arg):
|
|||||||
|
|
||||||
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
|
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
|
||||||
|
|
||||||
def generate_catalog(parent, dbspec, ids, device_manager, db):
|
# }}}
|
||||||
|
|
||||||
|
def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{
|
||||||
from calibre.gui2.dialogs.catalog import Catalog
|
from calibre.gui2.dialogs.catalog import Catalog
|
||||||
|
|
||||||
# Build the Catalog dialog in gui2.dialogs.catalog
|
# Build the Catalog dialog in gui2.dialogs.catalog
|
||||||
@ -306,8 +313,9 @@ def generate_catalog(parent, dbspec, ids, device_manager, db):
|
|||||||
# Which then calls gui2.convert.gui_conversion:gui_catalog() with the args inline
|
# Which then calls gui2.convert.gui_conversion:gui_catalog() with the args inline
|
||||||
return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \
|
return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \
|
||||||
d.catalog_title
|
d.catalog_title
|
||||||
|
# }}}
|
||||||
|
|
||||||
def convert_existing(parent, db, book_ids, output_format):
|
def convert_existing(parent, db, book_ids, output_format): # {{{
|
||||||
already_converted_ids = []
|
already_converted_ids = []
|
||||||
already_converted_titles = []
|
already_converted_titles = []
|
||||||
for book_id in book_ids:
|
for book_id in book_ids:
|
||||||
@ -323,3 +331,5 @@ def convert_existing(parent, db, book_ids, output_format):
|
|||||||
book_ids = [x for x in book_ids if x not in already_converted_ids]
|
book_ids = [x for x in book_ids if x not in already_converted_ids]
|
||||||
|
|
||||||
return book_ids
|
return book_ids
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user