%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/complete2.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from qt.core import (
QLineEdit, QAbstractListModel, Qt, pyqtSignal, QObject, QKeySequence, QAbstractItemView,
QApplication, QListView, QPoint, QModelIndex, QEvent,
QStyleOptionComboBox, QStyle, QComboBox, QTimer, sip)
from calibre.constants import ismacos
from calibre.utils.icu import sort_key, primary_startswith, primary_contains
from calibre.gui2.widgets import EnComboBox, LineEditECM
from calibre.utils.config import tweaks
def containsq(x, prefix):
return primary_contains(prefix, x)
class CompleteModel(QAbstractListModel): # {{{
def __init__(self, parent=None, sort_func=sort_key, strip_completion_entries=True):
QAbstractListModel.__init__(self, parent)
self.strip_completion_entries = strip_completion_entries
self.sort_func = sort_func
self.all_items = self.current_items = ()
self.current_prefix = ''
def set_items(self, items):
if self.strip_completion_entries:
items = (str(x).strip() for x in items if x)
else:
items = (str(x) for x in items if x)
items = tuple(sorted(items, key=self.sort_func))
self.beginResetModel()
self.all_items = self.current_items = items
self.current_prefix = ''
self.endResetModel()
def set_completion_prefix(self, prefix):
old_prefix = self.current_prefix
self.current_prefix = prefix
if prefix == old_prefix:
return
if not prefix:
self.beginResetModel()
self.current_items = self.all_items
self.endResetModel()
return
subset = prefix.startswith(old_prefix)
universe = self.current_items if subset else self.all_items
func = primary_startswith if tweaks['completion_mode'] == 'prefix' else containsq
self.beginResetModel()
self.current_items = tuple(x for x in universe if func(x, prefix))
self.endResetModel()
def rowCount(self, *args):
return len(self.current_items)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
try:
return self.current_items[index.row()].replace('\n', ' ')
except IndexError:
pass
if role == Qt.ItemDataRole.UserRole:
try:
return self.current_items[index.row()]
except IndexError:
pass
def index_for_prefix(self, prefix):
for i, item in enumerate(self.current_items):
if primary_startswith(item, prefix):
return self.index(i)
# }}}
class Completer(QListView): # {{{
item_selected = pyqtSignal(object)
apply_current_text = pyqtSignal()
relayout_needed = pyqtSignal()
def __init__(self, completer_widget, max_visible_items=7, sort_func=sort_key, strip_completion_entries=True):
QListView.__init__(self, completer_widget)
self.disable_popup = False
self.setWindowFlags(Qt.WindowType.Popup)
self.max_visible_items = max_visible_items
self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.setUniformItemSizes(True)
self.setAlternatingRowColors(True)
self.setModel(CompleteModel(self, sort_func=sort_func, strip_completion_entries=strip_completion_entries))
self.setMouseTracking(True)
self.activated.connect(self.item_chosen)
self.pressed.connect(self.item_chosen)
self.installEventFilter(self)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.tab_accepts_uncompleted_text = (tweaks['tab_accepts_uncompleted_text'] and
not tweaks['preselect_first_completion'])
def hide(self):
self.setCurrentIndex(QModelIndex())
QListView.hide(self)
def item_chosen(self, index):
if not self.isVisible():
return
self.hide()
text = self.model().data(index, Qt.ItemDataRole.UserRole)
self.item_selected.emit(str(text))
def set_items(self, items):
self.model().set_items(items)
if self.isVisible():
self.relayout_needed.emit()
def set_completion_prefix(self, prefix):
self.model().set_completion_prefix(prefix)
if self.isVisible():
self.relayout_needed.emit()
def next_match(self, previous=False):
c = self.currentIndex()
if c.isValid():
r = c.row()
else:
r = self.model().rowCount() if previous else -1
r = r + (-1 if previous else 1)
index = self.model().index(r % self.model().rowCount())
self.setCurrentIndex(index)
def scroll_to(self, orig):
if orig:
index = self.model().index_for_prefix(orig)
if index is not None and index.isValid():
self.setCurrentIndex(index)
def popup(self, select_first=True):
if self.disable_popup:
return
p = self
m = p.model()
widget = self.parent()
if widget is None:
return
screen = widget.screen().availableGeometry()
h = (p.sizeHintForRow(0) * min(self.max_visible_items, m.rowCount()) + 3) + 3
hsb = p.horizontalScrollBar()
if hsb and hsb.isVisible():
h += hsb.sizeHint().height()
rh = widget.height()
pos = widget.mapToGlobal(QPoint(0, widget.height() - 2))
w = min(widget.width(), 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, p.minimumHeight())
if h > bottom:
h = min(max(top, bottom), h)
if top > bottom:
pos.setY(pos.y() - h - rh + 2)
p.setGeometry(pos.x(), pos.y(), w, h)
if (tweaks['preselect_first_completion'] and select_first and not
self.currentIndex().isValid() and self.model().rowCount() > 0):
self.setCurrentIndex(self.model().index(0))
if not p.isVisible():
p.show()
def debug_event(self, ev):
from calibre.gui2 import event_type_name
print('Event:', event_type_name(ev))
if ev.type() in (QEvent.Type.KeyPress, QEvent.Type.ShortcutOverride, QEvent.Type.KeyRelease):
print('\tkey:', QKeySequence(ev.key()).toString())
def mouseMoveEvent(self, ev):
idx = self.indexAt(ev.pos())
if idx.isValid():
ci = self.currentIndex()
if idx.row() != ci.row():
self.setCurrentIndex(idx)
return QListView.mouseMoveEvent(self, ev)
def eventFilter(self, obj, e):
'Redirect key presses from the popup to the widget'
widget = self.parent()
if widget is None or sip.isdeleted(widget):
return False
etype = e.type()
if obj is not self:
return QObject.eventFilter(self, obj, e)
# self.debug_event(e)
if etype == QEvent.Type.KeyPress:
try:
key = e.key()
except AttributeError:
return QObject.eventFilter(self, obj, e)
if key == Qt.Key.Key_Escape:
self.hide()
e.accept()
return True
if key == Qt.Key.Key_F4 and e.modifiers() & Qt.KeyboardModifier.AltModifier:
self.hide()
e.accept()
return True
if key in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
# We handle this explicitly because on OS X activated() is
# not emitted on pressing Enter.
idx = self.currentIndex()
if idx.isValid():
self.item_chosen(idx)
self.hide()
e.accept()
return True
if key == Qt.Key.Key_Tab:
idx = self.currentIndex()
if idx.isValid():
self.item_chosen(idx)
self.hide()
elif self.tab_accepts_uncompleted_text:
self.hide()
self.apply_current_text.emit()
elif self.model().rowCount() > 0:
self.next_match()
e.accept()
return True
if key in (Qt.Key.Key_PageUp, Qt.Key.Key_PageDown):
# Let the list view handle these keys
return False
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down):
self.next_match(previous=key == Qt.Key.Key_Up)
e.accept()
return True
# Send to widget
widget.eat_focus_out = False
widget.keyPressEvent(e)
widget.eat_focus_out = True
if not widget.hasFocus():
# Widget lost focus hide the popup
self.hide()
if e.isAccepted():
return True
elif ismacos and etype == QEvent.Type.InputMethodQuery and e.queries() == (
Qt.InputMethodQuery.ImHints | Qt.InputMethodQuery.ImEnabled) and self.isVisible():
# In Qt 5 the Esc key causes this event and the line edit does not
# handle it, which causes the parent dialog to be closed
# See https://bugreports.qt-project.org/browse/QTBUG-41806
e.accept()
return True
elif etype == QEvent.Type.MouseButtonPress and hasattr(e, 'globalPos') and not self.rect().contains(self.mapFromGlobal(e.globalPos())):
# A click outside the popup, close it
if isinstance(widget, QComboBox):
# This workaround is needed to ensure clicking on the drop down
# arrow of the combobox closes the popup
opt = QStyleOptionComboBox()
widget.initStyleOption(opt)
sc = widget.style().hitTestComplexControl(QStyle.ComplexControl.CC_ComboBox, opt, widget.mapFromGlobal(e.globalPos()), widget)
if sc == QStyle.SubControl.SC_ComboBoxArrow:
QTimer.singleShot(0, self.hide)
e.accept()
return True
self.hide()
e.accept()
return True
elif etype in (QEvent.Type.InputMethod, QEvent.Type.ShortcutOverride):
QApplication.sendEvent(widget, e)
return False
# }}}
class LineEdit(QLineEdit, LineEditECM):
'''
A line edit that completes on multiple items separated by a
separator. Use the :meth:`update_items_cache` to set the list of
all possible completions. Separator can be controlled with the
:meth:`set_separator` and :meth:`set_space_before_sep` methods.
A call to self.set_separator(None) will allow this widget to be used
to complete non multiple fields as well.
'''
item_selected = pyqtSignal(object)
def __init__(self, parent=None, completer_widget=None, sort_func=sort_key, strip_completion_entries=True):
QLineEdit.__init__(self, parent)
self.setClearButtonEnabled(True)
self.sep = ','
self.space_before_sep = False
self.add_separator = True
self.original_cursor_pos = None
completer_widget = (self if completer_widget is None else
completer_widget)
self.mcompleter = Completer(completer_widget, sort_func=sort_func, strip_completion_entries=strip_completion_entries)
self.mcompleter.item_selected.connect(self.completion_selected,
type=Qt.ConnectionType.QueuedConnection)
self.mcompleter.apply_current_text.connect(self.apply_current_text,
type=Qt.ConnectionType.QueuedConnection)
self.mcompleter.relayout_needed.connect(self.relayout)
self.mcompleter.setFocusProxy(completer_widget)
self.textEdited.connect(self.text_edited)
self.no_popup = False
# Interface {{{
def update_items_cache(self, complete_items):
self.all_items = complete_items
def set_separator(self, sep):
self.sep = sep
def set_space_before_sep(self, space_before):
self.space_before_sep = space_before
def set_add_separator(self, what):
self.add_separator = bool(what)
@property
def all_items(self):
return self.mcompleter.model().all_items
@all_items.setter
def all_items(self, items):
self.mcompleter.model().set_items(items)
@property
def disable_popup(self):
return self.mcompleter.disable_popup
@disable_popup.setter
def disable_popup(self, val):
self.mcompleter.disable_popup = bool(val)
def set_elide_mode(self, val):
self.mcompleter.setTextElideMode(val)
# }}}
def event(self, ev):
# See https://bugreports.qt.io/browse/QTBUG-46911
try:
if ev.type() == QEvent.Type.ShortcutOverride and (
ev.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right) and (
ev.modifiers() & ~Qt.KeyboardModifier.KeypadModifier) == Qt.KeyboardModifier.ControlModifier):
ev.accept()
except AttributeError:
pass
return QLineEdit.event(self, ev)
def complete(self, show_all=False, select_first=True):
orig = None
if show_all:
orig = self.mcompleter.model().current_prefix
self.mcompleter.set_completion_prefix('')
if not self.mcompleter.model().current_items:
self.mcompleter.hide()
return
self.mcompleter.popup(select_first=select_first)
self.setFocus(Qt.FocusReason.OtherFocusReason)
self.mcompleter.scroll_to(orig)
def relayout(self):
self.mcompleter.popup()
self.setFocus(Qt.FocusReason.OtherFocusReason)
def text_edited(self, *args):
if self.no_popup:
return
self.update_completions()
select_first = len(self.mcompleter.model().current_prefix) > 0
if not select_first:
self.mcompleter.setCurrentIndex(QModelIndex())
self.complete(select_first=select_first)
def update_completions(self):
' Update the list of completions '
self.original_cursor_pos = cpos = self.cursorPosition()
text = str(self.text())
prefix = text[:cpos]
complete_prefix = prefix.lstrip()
if self.sep:
complete_prefix = prefix.split(self.sep)[-1].lstrip()
self.mcompleter.set_completion_prefix(complete_prefix)
def get_completed_text(self, text):
'Get completed text in before and after parts'
if self.sep is None:
return text, ''
else:
cursor_pos = self.original_cursor_pos
if cursor_pos is None:
cursor_pos = self.cursorPosition()
self.original_cursor_pos = None
# Split text
curtext = str(self.text())
before_text = curtext[:cursor_pos]
after_text = curtext[cursor_pos:].rstrip()
# Remove the completion prefix from the before text
before_text = self.sep.join(before_text.split(self.sep)[:-1]).rstrip()
if before_text:
# Add the separator to the end of before_text
if self.space_before_sep:
before_text += ' '
before_text += self.sep + ' '
if self.add_separator or after_text:
# Add separator to the end of completed text
if self.space_before_sep:
text = text.rstrip() + ' '
completed_text = text + self.sep + ' '
else:
completed_text = text
return before_text + completed_text, after_text
def completion_selected(self, text):
before_text, after_text = self.get_completed_text(str(text))
self.setText(before_text + after_text)
self.setCursorPosition(len(before_text))
self.item_selected.emit(text)
def apply_current_text(self):
if self.sep is not None:
txt = str(self.text())
sep_pos = txt.rfind(self.sep)
if sep_pos:
ntxt = txt[sep_pos+1:].strip()
self.completion_selected(ntxt)
class EditWithComplete(EnComboBox):
item_selected = pyqtSignal(object)
def __init__(self, *args, **kwargs):
EnComboBox.__init__(self, *args)
self.setLineEdit(LineEdit(
self, completer_widget=self, sort_func=kwargs.get('sort_func', sort_key),
strip_completion_entries=kwargs.get('strip_completion_entries', False)))
self.lineEdit().item_selected.connect(self.item_selected)
self.setCompleter(None)
self.eat_focus_out = True
self.installEventFilter(self)
# Interface {{{
def showPopup(self):
orig = self.disable_popup
self.disable_popup = False
try:
self.lineEdit().complete(show_all=True)
finally:
self.disable_popup = orig
def update_items_cache(self, complete_items):
self.lineEdit().update_items_cache(complete_items)
def set_separator(self, sep):
self.lineEdit().set_separator(sep)
def set_space_before_sep(self, space_before):
self.lineEdit().set_space_before_sep(space_before)
def set_add_separator(self, what):
self.lineEdit().set_add_separator(what)
def show_initial_value(self, what):
what = str(what) if what else ''
self.setText(what)
self.lineEdit().selectAll()
@property
def all_items(self):
return self.lineEdit().all_items
@all_items.setter
def all_items(self, val):
self.lineEdit().all_items = val
@property
def disable_popup(self):
return self.lineEdit().disable_popup
@disable_popup.setter
def disable_popup(self, val):
self.lineEdit().disable_popup = bool(val)
def set_elide_mode(self, val):
self.lineEdit().set_elide_mode(val)
def set_clear_button_enabled(self, val=True):
self.lineEdit().setClearButtonEnabled(bool(val))
# }}}
def text(self):
return str(self.lineEdit().text())
def selectAll(self):
self.lineEdit().selectAll()
def setText(self, val):
le = self.lineEdit()
le.no_popup = True
le.setText(val)
le.no_popup = False
def home(self, mark=False):
self.lineEdit().home(mark)
def setCursorPosition(self, *args):
self.lineEdit().setCursorPosition(*args)
@property
def textChanged(self):
return self.lineEdit().textChanged
def clear(self):
self.lineEdit().clear()
EnComboBox.clear(self)
def eventFilter(self, obj, e):
try:
c = self.lineEdit().mcompleter
except AttributeError:
return False
etype = e.type()
if self.eat_focus_out and self is obj and etype == QEvent.Type.FocusOut:
if c.isVisible():
return True
return EnComboBox.eventFilter(self, obj, e)
if __name__ == '__main__':
from qt.core import QDialog, QVBoxLayout
from calibre.gui2 import Application
app = Application([])
d = QDialog()
d.setLayout(QVBoxLayout())
le = EditWithComplete(d)
d.layout().addWidget(le)
items = ['oane\n line2\n line3', 'otwo', 'othree', 'ooone', 'ootwo', 'other', 'odd', 'over', 'orc', 'oven', 'owe',
'oothree', 'a1', 'a2','Edgas', 'Èdgar', 'Édgaq', 'Edgar', 'Édgar']
le.update_items_cache(items)
le.show_initial_value('')
d.exec()