Source code for exopy.utils.widgets.qt_completers

# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2015-2018 by Exopy Authors, see AUTHORS for more details.
#
# Distributed under the terms of the BSD license.
#
# The full license is in the file LICENCE, distributed with this software.
# -----------------------------------------------------------------------------
"""Widgets with support for text completion.

"""
from atom.api import List, Tuple, Str, Bool, Callable, Value
from enaml.core.declarative import d_
from enaml.qt import QtCore, QtWidgets
from enaml.widgets.api import RawWidget, Feature


[docs]class QDelimitedCompleter(QtWidgets.QCompleter): """A custom completer to use with QtLineCompleter, QtTextEdit. This completer only propose completion between specified characters. Parameters ---------- parent : QLineEdit or QTextEdit Widget for which to provide a completion. delimiters : tuple Tuple of length 2 specifying the characters marking the begining end of completion. entries : iterable Iterable of values used to propose completion. entries_updaters : callable Callable used to refresh the list of entries called once for the first completion after the widget gained focus. """ # Signal emmitted to notify the completer it should propose a completion. completionNeeded = QtCore.Signal() def __init__(self, parent, delimiters, entries, entries_updater): super(QDelimitedCompleter, self).__init__(parent) self.delimiters = delimiters if isinstance(parent, QtWidgets.QLineEdit): self.text_getter = parent.text self.cursor_pos = parent.cursorPosition self.insert_text = parent.insert parent.textChanged[str].connect(self.text_changed) self.completionNeeded.connect(self.complete) elif isinstance(parent, QtWidgets.QTextEdit): parent.textChanged.connect(self.text_changed) self.cursor_pos = lambda: parent.textCursor().position() self.insert_text =\ lambda text: parent.textCursor().insertText(text) self.text_getter = parent.toPlainText self.completionNeeded.connect(self._text_edit_complete) else: msg = 'Parent of QtCompleter must QLineEdit or QTextEdit, not {}' raise ValueError(msg.format(parent)) self.setCaseSensitivity(QtCore.Qt.CaseSensitive) self.setModel(QtCore.QStringListModel(entries, self)) self.activated[str].connect(self.complete_text) self.setWidget(parent) self._upddate_entries = True self._popup_active = False self.entries_updater = entries_updater
[docs] def text_changed(self, text=None): """Callback handling the text being edited on the parent. """ if not text: text = self.text_getter() if self._upddate_entries and self.entries_updater: entries = self.entries_updater() self.setModel(QtCore.QStringListModel(entries, self)) self._upddate_entries = False all_text = str(text) text = all_text[:self.cursor_pos()] split = text.split(self.delimiters[0]) prefix = split[-1].strip() if len(split) > 1: self.setCompletionPrefix(prefix) self.completionNeeded.emit() elif self.popup().isVisible(): self.popup().hide()
[docs] def complete_text(self, completion): """When the user validate a completion add it to the text. """ cursor_pos = self.cursor_pos() text = str(self.text_getter()) before_text = text[:cursor_pos] after_text = text[cursor_pos:] prefix_len = len(before_text.split(self.delimiters[0])[-1].strip()) completion = completion[prefix_len:] if not after_text.startswith(self.delimiters[1]): completion += self.delimiters[1] self.insert_text(completion)
[docs] def on_focus_gained(self): """Mark the entries for refreshing when the widget loses focus. """ self._upddate_entries = True
def _update_entries(self, entries): """Update the completer completion model. """ self.setModel(QtCore.QStringListModel(entries)) def _text_edit_complete(self): """Propose completion for QTextEdit. """ cr = self.widget().cursorRect() popup = self.popup() cr.setWidth(popup.sizeHintForColumn(0) + popup.verticalScrollBar().sizeHint().width()) self.complete(cr)
[docs]class QtLineCompleter(RawWidget): """Simple line editor supporting completion. """ #: Text being edited by this widget. text = d_(Str()) #: Static list of entries used to propose completion. This member value is #: not updated by the entries_updater. entries = d_(List()) #: Callable to use to refresh the completions. entries_updater = d_(Callable()) #: Delimiters marking the begining and end of completed section. delimiters = d_(Tuple(Str(), ('{', '}'))) hug_width = 'ignore' features = Feature.FocusEvents #: Flag avoiding circular updates. _no_update = Bool(False) #: Reference to the QCompleter used by the widget. _completer = Value() # PySide requires weakrefs for using bound methods as slots. # PyQt doesn't, but executes unsafe code if not using weakrefs. __slots__ = '__weakref__'
[docs] def create_widget(self, parent): """Finishes initializing by creating the underlying toolkit widget. """ widget = QtWidgets.QLineEdit(parent) self._completer = QDelimitedCompleter(widget, self.delimiters, self.entries, self.entries_updater) widget.setText(self.text) self.proxy.widget = widget # Anticipated so that selection works widget.textEdited.connect(self.update_object) return widget
[docs] def update_object(self): """ Handles the user entering input data in the edit control. """ if (not self._no_update) and self.activated: value = self.get_widget().text() self._no_update = True self.text = value self._no_update = False
def _post_setattr_text(self, old, new): """Updates the editor when the object changes externally to the editor. """ if (not self._no_update) and self.get_widget(): self._no_update = True self.get_widget().setText(new) self._no_update = False def _post_setattr_entries(self, old, new): """Updates the completer entries. """ if self._completer: self._completer._update_entries(new)
[docs] def focus_gained(self): """Notify the completer the focus was lost. """ self._completer.on_focus_gained()
[docs]class QCompletableTexEdit(QtWidgets.QTextEdit): """A QTextEdit letting the completer handles key presses when visible. """ __slots__ = ('completer', )
[docs] def keyPressEvent(self, event): """Overriden to let the completer handle some events when visible. """ if self.completer.popup().isVisible(): key = event.key() if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, QtCore.Qt.Key_Escape, QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab): event.ignore() return super(QCompletableTexEdit, self).keyPressEvent(event)
[docs]class QtTextCompleter(RawWidget): """Simple text editor supporting completion. """ #: Text being edited by this widget. text = d_(Str()) #: Static list of entries used to propose completion. This member value is #: not updated by the entries_updater. entries = d_(List()) #: Callable to use to refresh the completions. entries_updater = d_(Callable()) #: Delimiters marking the begining and end of completed section. delimiters = d_(Tuple(Str(), ('{', '}'))) hug_width = 'ignore' features = Feature.FocusEvents #: Flag avoiding circular updates. _no_update = Bool(False) #: Reference to the QCompleter used by the widget. _completer = Value() # PySide requires weakrefs for using bound methods as slots. # PyQt doesn't, but executes unsafe code if not using weakrefs. __slots__ = '__weakref__'
[docs] def create_widget(self, parent): """Finishes initializing by creating the underlying toolkit widget. """ widget = QCompletableTexEdit(parent) self._completer = QDelimitedCompleter(widget, self.delimiters, self.entries, self.entries_updater) widget.completer = self._completer widget.setText(self.text) self.proxy.widget = widget # Anticipated so that selection works widget.textChanged.connect(self.update_object) return widget
[docs] def update_object(self): """ Handles the user entering input data in the edit control. """ if (not self._no_update) and self.activated: value = self.get_widget().toPlainText() self._no_update = True self.text = value self._no_update = False
[docs] def focus_gained(self): """Notify the completer the focus was lost. """ self._completer.on_focus_gained()
def _post_setattr_text(self, old, new): """Updates the editor when the object changes externally to the editor. """ if (not self._no_update) and self.get_widget(): self._no_update = True self.get_widget().setText(new) self._no_update = False def _post_setattr_entries(self, old, new): """Updates the completer entries. """ if self.proxy_is_active and self._completer: self._completer._update_entries(new)