%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()