class SQLiteCompleterText(QTextEdit): def __init__(self, parent=None): super(SQLiteCompleterText, self).__init__(parent) self.cp = QCompleter(words) self.cp.setWrapAround(False) self.cp.setWidget(self) self.cp.setCompletionMode(QCompleter.PopupCompletion) self.cp.activated.connect(self.insertCompletion) def insertCompletion(self, completion): tc = self.textCursor() extra = len(completion) - len(self.cp.completionPrefix()) tc.movePosition(QTextCursor.Left) tc.movePosition(QTextCursor.EndOfWord) tc.insertText(completion[-extra:]) self.setTextCursor(tc) def textUnderCursor(self): tc = self.textCursor() tc.select(QTextCursor.WordUnderCursor) return tc.selectedText() def focusInEvent(self, e): self.cp.setWidget(self) super(SQLiteCompleterText, self).focusInEvent(e) def keyPressEvent(self, e): if self.cp.popup().isVisible(): if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): e.ignore() return is_shortcut = ((e.modifiers() & Qt.ControlModifier) != 0 and e.key() == Qt.Key_E & Qt.ControlModifier) if not is_shortcut: super(SQLiteCompleterText, self).keyPressEvent(e) ctrl_or_shift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier) if ctrl_or_shift and len(e.text()) == 0: return eo = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-=" has_modifier = (e.modifiers() != Qt.NoModifier) and not ctrl_or_shift cp = self.textUnderCursor() if not is_shortcut and (cp in words or has_modifier or len(e.text()) == 0 or len(cp) < 1 or e.text()[-1] in eo): self.cp.popup().hide() return if cp != self.cp.completionPrefix(): self.cp.setCompletionPrefix(cp) self.cp.popup().setCurrentIndex(self.cp.completionModel().index(0, 0)) cr = self.cursorRect() cr.setWidth(self.cp.popup().sizeHintForColumn(0) + self.cp.popup().verticalScrollBar().sizeHint().width()) self.cp.complete(cr)
class CompletingTextEdit(QTextEdit): def __init__(self, parent=None): super().__init__(parent) self.completerSequence = QKeySequence("Ctrl+Space") self._completer = QCompleter() self._completer.setWidget(self) self._completer.activated.connect(self.insertCompletion) def insertCompletion(self, completion): if self._completer.widget() is not self: return tc = self.textCursor() extra = len(completion) - len(self._completer.completionPrefix()) tc.movePosition(QTextCursor.Left) tc.movePosition(QTextCursor.EndOfWord) tc.insertText(completion[-extra:]) self.setTextCursor(tc) def textUnderCursor(self): tc = self.textCursor() tc.select(QTextCursor.WordUnderCursor) return tc.selectedText() def loadFromFile(self, fileName): f = QFile(fileName) if not f.open(QFile.ReadOnly): model = QStringListModel() self._completer.setModel(model) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) words = [] while not f.atEnd(): line = f.readLine().trimmed() if line.length() != 0: try: line = str(line, encoding='utf-8') except TypeError: line = str(line) words.append(line) QApplication.restoreOverrideCursor() model = QStringListModel(words) self._completer.setModel(model) def loadFromList(self, words): model = QStringListModel(words) self._completer.setModel(model) def keyPressEvent(self, e): if self._completer.popup().isVisible(): # The following keys are forwarded by the completer to the widget. if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): e.ignore() # Let the completer do default behavior. return newSeq = QKeySequence(e.modifiers() | e.key()) isShortcut = newSeq == self.completerSequence if not isShortcut: # Do not process the shortcut when we have a completer. super().keyPressEvent(e) return ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier) if ctrlOrShift and not e.text(): return eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-=" hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift completionPrefix = self.textUnderCursor() if not isShortcut and (hasModifier or not e.text() or len(completionPrefix) < 3 or e.text()[-1] in eow): self._completer.popup().hide() return if completionPrefix != self._completer.completionPrefix(): self._completer.setCompletionPrefix(completionPrefix) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(0, 0)) cr = self.cursorRect() cr.setWidth( self._completer.popup().sizeHintForColumn(0) + self._completer.popup().verticalScrollBar().sizeHint().width()) self._completer.complete(cr)
class TextStatusEditComplete(TextStatusEdit): """ Adds Completion functions to the base class This class extends 'TextStatusEdit' by: 1. providing a QCompleter to validate lines for the 'fixupText' and 'lineChanged' signals 2. providing a popup for suggested completions as the user is typing 3. auto-completing the line when the user selects a suggestion. The task of auto completion and providing suggestions is provided directly by this class. The task validating and cleaning up text is provided by the PluginFinder. """ def __init__(self, parent: QWidget = None): super().__init__(parent) self._dataModel = None self._monitorDbChanges = False self._enableAutoCompletion = False self._completedAndSelected = False self._completer = QCompleter(self) self._completer.setWidget(self) self._completer.setWrapAround(False) self._completer.setCompletionMode(QCompleter.PopupCompletion) self._completer.setCaseSensitivity(Qt.CaseInsensitive) self._completer.setFilterMode(Qt.MatchStartsWith) self._completer.setModelSorting( QCompleter.CaseInsensitivelySortedModel) self._completer.activated.connect(self.replaceLine) self._pluginFinder = PluginFinder(self._completer, self) self.fixupText.connect(self._pluginFinder.fixupText) self.lineChanged.connect(self._pluginFinder.setRowForLine) QShortcut(Qt.CTRL + Qt.Key_E, self, self.toggleAutoCompletion) QShortcut(Qt.CTRL + Qt.Key_T, self, self.suggestCompletions) # --- Methods related to the completer's underlying data model def setModel(self, model: QAbstractItemModel): self._completer.setModel(model) def _updateModelSignals(self): """ We do not need to check for column changes due to the way our PluginModel is structured. """ if self._dataModel is not None: self._dataModel.rowsMoved.disconnect(self.resetData) self._dataModel.rowsInserted.disconnect(self.resetData) self._dataModel.rowsRemoved.disconnect(self.resetData) self._dataModel.modelReset.disconnect(self.resetData) self._dataModel.dataChanged.disconnect(self.resetData) self._dataModel.layoutChanged.disconnect(self.resetData) if self._monitorDbChanges: self._dataModel = self._completer.model() if self._dataModel is not None: self._dataModel.rowsMoved.connect(self.resetData) self._dataModel.rowsInserted.connect(self.resetData) self._dataModel.rowsRemoved.connect(self.resetData) self._dataModel.modelReset.connect(self.resetData) self._dataModel.dataChanged.connect(self.resetData) self._dataModel.layoutChanged.connect(self.resetData) else: self._dataModel = None def monitorDbChanges(self, enable: bool): """ Enable invalidating line status when the data model changes. Depending on the underlying data model, it may be unnecessary to monitor these changes, or, a higher level class can monitor specific signals more efficiently. So, this is not enabled by default. """ if self._monitorDbChanges == enable: return self._monitorDbChanges = enable if enable: self._dataModel = self._completer.model() self._completer.completionModel().sourceModelChanged.connect( self._updateModelSignals) else: self._completer.completionModel().sourceModelChanged.disconnect( self._updateModelSignals) self._updateModelSignals() # ---- Methods related to line completion def completer(self): return self._completer def enableAutoCompletion(self, enable: bool): self._enableAutoCompletion = enable def toggleAutoCompletion(self): self.enableAutoCompletion(not self._enableAutoCompletion) def _textUnderCursor(self): tc = self.textCursor() if tc.positionInBlock() == 0 and len(tc.block().text()) > 1: tc.movePosition(QTextCursor.NextCharacter) tc.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor) return tc.selectedText().lstrip() def suggestCompletions(self): if self.isLineInvalid(self.textCursor().blockNumber()): self._suggestCompletionsForText(self._textUnderCursor()) def _suggestCompletionsForText(self, prefix: str): if not prefix: return if prefix != self._completer.completionPrefix(): self._completer.setCompletionPrefix(prefix) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(0, 0)) if self._completer.completionCount() == 1: self._insertSuggestion(self._completer.currentCompletion()) else: rect = self.cursorRect() rect.moveRight(self.statusAreaWidth()) rect.setWidth( self._completer.popup().sizeHintForColumn( self._completer.completionColumn()) + self._completer.popup().verticalScrollBar().sizeHint().width()) self._completer.complete(rect) def _insertSuggestion(self, text: str): """ Only one suggestion matched, prefill line """ cursor = self.textCursor() # handle when cursor is in middle of line if not cursor.atBlockEnd(): cursor.beginEditBlock() cursor.select(QTextCursor.LineUnderCursor) cursor.removeSelectedText() cursor.insertText(text) cursor.movePosition(QTextCursor.StartOfLine) cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) self._completedAndSelected = True self.setTextCursor(cursor) cursor.endEditBlock() return # handle when cursor at end of line cursor.beginEditBlock() numCharsToComplete = len(text) - len( self._completer.completionPrefix()) insertionPosition = cursor.position() cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.insertText(text[-numCharsToComplete:]) cursor.setPosition(insertionPosition) cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) self._completedAndSelected = True self.setTextCursor(cursor) cursor.endEditBlock() def keyPressEvent(self, event: QKeyEvent): if self._completedAndSelected and self.handledCompletedAndSelected( event): return self._completedAndSelected = False if self._completer.popup().isVisible(): ignoredKeys = [ Qt.Key_Up, Qt.Key_Down, Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab, Qt.Key_Escape, ] if event.key() in ignoredKeys: event.ignore() return self._completer.popup().hide() super().keyPressEvent(event) if not self._enableAutoCompletion: return ctrlOrShift = (event.modifiers() & Qt.ShiftModifier == Qt.ShiftModifier or event.modifiers() & Qt.ControlModifier == Qt.ControlModifier) if ctrlOrShift and not event.text(): return if self.textCursor().atBlockEnd(): self.suggestCompletions() def mousePressEvent(self, event: QMouseEvent): if self._completedAndSelected: self._completedAndSelected = False self.document().undo() super().mousePressEvent(event) def handledCompletedAndSelected(self, event: QKeyEvent): """ The line is prefilled when only one completion matches. The user can accept the suggestion by pressing 'Enter'. The user can reject the suggestion by pressing 'Esc' or by continuing to type. """ self._completedAndSelected = False cursor = self.textCursor() acceptKeys = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab] if event.key() in acceptKeys: self.replaceLine(self._completer.currentCompletion()) elif event.key() == Qt.Key_Escape: self.document().undo() else: self.document().undo() return False self.setTextCursor(cursor) event.accept() return True def replaceLine(self, text: str): cursor = self.textCursor() cursor.beginEditBlock() cursor.select(QTextCursor.LineUnderCursor) cursor.removeSelectedText() cursor.insertText(text) cursor.movePosition(QTextCursor.EndOfLine) self.setTextCursor(cursor) cursor.endEditBlock() # ---- Methods related to Context Menu def createStandardContextMenu(self, pos: QPoint): menu = super().createStandardContextMenu(pos) menu.addSeparator() autoCompletionAction = menu.addAction( QIcon(), self.tr("Enable Auto Complete"), self.toggleAutoCompletion, QKeySequence(Qt.CTRL + Qt.Key_E), ) autoCompletionAction.setCheckable(True) autoCompletionAction.setChecked(self._enableAutoCompletion) completionAction = menu.addAction( QIcon(), self.tr("Suggest Completions"), self.suggestCompletions, QKeySequence(Qt.CTRL + Qt.Key_T), ) completionAction.setEnabled( self.isLineInvalid(self.textCursor().blockNumber())) return menu
class TextEditTechnique(QTextEdit): def __init__(self, text_file, input_trigger=None, active=True): super(TextEditTechnique, self).__init__() self._completer = None self.isActive = active word_list = self.get_suggestion_words(text_file) self.input_trigger = input_trigger # connects the text textChanged event self.textChanged.connect(self.handle_input) self.char_length = 1 self.set_completer(word_list) self.technique_used = False self.no_suggestion_input = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-= " # gets words for suggestion def get_suggestion_words(self, text_file): words = [] file = open(text_file) for line in file: for word in line.split(' '): word = word.strip().lower().replace('.', '').replace( ',', '').replace(':', '').replace('?', '') if word and word not in words: words.append(word) file.close() return words # initializes the completer def set_completer(self, words): if self._completer is not None: self._completer.activated.disconnect() self._completer = QCompleter(words, self) self._completer.setCaseSensitivity(Qt.CaseInsensitive) self._completer.setWrapAround(False) self._completer.setWidget(self) self._completer.setCompletionMode(QCompleter.PopupCompletion) self._completer.activated.connect(self.insert_sel_suggestion) # insert the selected suggestion and moves the text cursor def insert_sel_suggestion(self, suggestion): if self._completer.widget() is not self: return text_cursor = self.textCursor() extra = len(suggestion) - len(self._completer.completionPrefix()) text_cursor.movePosition(QTextCursor.Left) text_cursor.movePosition(QTextCursor.EndOfWord) text_cursor.insertText(suggestion[-extra:]) self.setTextCursor(text_cursor) # returns the current typed word def text_under_cursor(self): text_cursor = self.textCursor() text_cursor.select(QTextCursor.WordUnderCursor) return text_cursor.selectedText() # sets the widget def focusInEvent(self, e): if self._completer is not None and self.isActive: self._completer.setWidget(self) super(TextEditTechnique, self).focusInEvent(e) def keyPressEvent(self, e): # behaves like a normal input field! if not self.isActive or self._completer is None: super(TextEditTechnique, self).keyPressEvent(e) return # ignore events funktion keys on Textedit if popup is visible --> popup behaves default if self._completer.popup().isVisible(): if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): e.ignore() self.technique_used = True return # writes text in the editfield super(TextEditTechnique, self).keyPressEvent(e) ctrl_or_shift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier) if self._completer is None or (ctrl_or_shift and len(e.text()) == 0): return has_modifier = (e.modifiers() != Qt.NoModifier) and not ctrl_or_shift completion_prefix = self.text_under_cursor() # hide the popup for no chars, modifiers, shift and no text if (has_modifier or len(e.text()) == 0 or len(completion_prefix) < self.char_length or e.text()[-1] in self.no_suggestion_input): self._completer.popup().hide() return # shows the popup with the suggested words if completion_prefix != self._completer.completionPrefix(): self._completer.setCompletionPrefix(completion_prefix) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(0, 0)) cursor = self.cursorRect() cursor.setWidth( self._completer.popup().sizeHintForColumn(0) + self._completer.popup().verticalScrollBar().sizeHint().width()) self._completer.complete(cursor) # event called if textedit input has changed def handle_input(self): timestamp = time.time() self.input_trigger.emit(self.toPlainText(), timestamp) # clears the text input def clear_input(self): self.setHtml('') # deactivates the completer def deactivate_completer(self): self.isActive = False # activates the completer def activate_completer(self): self.isActive = True