class AutoCompleteLineEdit(QLineEdit): # http://blog.elentok.com/2011/08/autocomplete-textbox-for-multiple.html def __init__(self, items, parent=None): super(AutoCompleteLineEdit, self).__init__(parent) self._separators = [",", " "] self._completer = QCompleter(items, self) self._completer.setWidget(self) self._completer.activated[str].connect(self.__insertCompletion) self._completer.setCaseSensitivity(Qt.CaseInsensitive) self.__keysToIgnore = [ Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab ] def __insertCompletion(self, completion): extra = len(completion) - len(self._completer.completionPrefix()) extra_text = completion[-extra:] extra_text += ', ' self.setText(self.text() + extra_text) def textUnderCursor(self): text = self.text() text_under_cursor = '' i = self.cursorPosition() - 1 while i >= 0 and text[i] not in self._separators: text_under_cursor = text[i] + text_under_cursor i -= 1 return text_under_cursor def keyPressEvent(self, event): if self._completer.popup().isVisible(): if event.key() in self.__keysToIgnore: event.ignore() return super(AutoCompleteLineEdit, self).keyPressEvent(event) completion_prefix = self.textUnderCursor() if completion_prefix != self._completer.completionPrefix(): self.__updateCompleterPopupItems(completion_prefix) if len(event.text()) > 0 and len(completion_prefix) > 0: self._completer.complete() if len(completion_prefix) == 0: self._completer.popup().hide() def __updateCompleterPopupItems(self, completionPrefix): self._completer.setCompletionPrefix(completionPrefix) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(0, 0))
class AutoCompleteLineEdit(QLineEdit): #http://blog.elentok.com/2011/08/autocomplete-textbox-for-multiple.html def __init__(self, items, parent=None): super(AutoCompleteLineEdit, self).__init__(parent) self.__separators = [",", " "] self.__completer = QCompleter(items, self) self.__completer.setWidget(self) self.__completer.activated[str].connect(self.__insertCompletion) self.__completer.setCaseSensitivity(Qt.CaseInsensitive) self.__keysToIgnore = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab] def __insertCompletion(self, completion): extra = len(completion) - len(self.__completer.completionPrefix()) extra_text = completion[-extra:] extra_text += ', ' self.setText(self.text() + extra_text) def textUnderCursor(self): text = self.text() text_under_cursor = '' i = self.cursorPosition() - 1 while i >=0 and text[i] not in self.__separators: text_under_cursor = text[i] + text_under_cursor i -= 1 return text_under_cursor def keyPressEvent(self, event): if self.__completer.popup().isVisible(): if event.key() in self.__keysToIgnore: event.ignore() return super(AutoCompleteLineEdit, self).keyPressEvent(event) completion_prefix = self.textUnderCursor() if completion_prefix != self.__completer.completionPrefix(): self.__updateCompleterPopupItems(completion_prefix) if len(event.text()) > 0 and len(completion_prefix) > 0: self.__completer.complete() if len(completion_prefix) == 0: self.__completer.popup().hide() def __updateCompleterPopupItems(self, completionPrefix): self.__completer.setCompletionPrefix(completionPrefix) self.__completer.popup().setCurrentIndex(self.__completer.completionModel().index(0,0))
class TagCompleterWidget(QObject): """ widget in lineEdit-style with integrated qcompleter """ def __init__(self, max_tags, expiry_prefix=None, tag_list=None, parent=None, separator=",", show_datestamp=False): QWidget.__init__(self, parent) self.__completer_active = False self.__max_tags = max_tags self.__tag_separator = separator self.__tag_list = tag_list self.__parent = parent self.__tag_line = QLineEdit(self.__parent) #self.__tag_line = TagLineEdit(self.__parent) self.__show_datestamp = show_datestamp self.__datestamp_format = TsConstants.DATESTAMP_FORMAT_DAY self.__expiry_prefix = expiry_prefix ## flag, if the line should be checked of emptiness self.__check_not_empty = False self.__check_tag_limit = False self.__restricted_vocabulary = False ## the latest activated suggestion self.__activated_text = None # value of the actual datestamp self.__datestamp = None self.__datestamp_hidden = False self.__completer = QCompleter(self.__tag_list, self); self.__completer.setCaseSensitivity(Qt.CaseInsensitive) self.__completer.setWidget(self.__tag_line) #self.__handle_datestamp() self.connect(self.__tag_line, SIGNAL("textChanged(QString)"), self.__text_changed_by_user) self.connect(self.__completer, SIGNAL("activated(QString)"), self.__text_activated) self.connect(self.__completer, SIGNAL("highlighted(QString)"), self.__text_highlighted) def __text_highlighted(self, item_name): """ a suggestion has been selected in the dropdownbox """ # set this variable to True just to know, that # this value comes from the completer and not from the user self.__completer_active = True self.__text_selected(item_name) self.__completer_active = False def __handle_datestamp(self, is_hidden): """ if the show_datestamp flag is set to True, provide an automatic datestamp on the tagline """ if self.__show_datestamp: self.__datestamp = time.strftime(self.__datestamp_format) if not is_hidden: self.__tag_line.clear() self.__tag_line.setText(self.__datestamp) def set_datestamp_format(self, format, is_hidden): self.__datestamp_format = format self.__datestamp_hidden = is_hidden self.__handle_datestamp(is_hidden) def show_datestamp(self, show): self.__show_datestamp = show self.__handle_datestamp(show) def clear_line(self): """ clear the tagline ... if auto datestamp is set to "on" a fresh stamp will be placed into the tagline """ self.__tag_line.clear() if self.__show_datestamp: self.__handle_datestamp(self.__datestamp_hidden) def set_check_not_empty(self, check_necessary): """ set this to True, if there should be sent a signal that indicates that the tagline is not empty anymore """ self.__check_not_empty = True def set_restricted_vocabulary(self, is_restricted): """ use True/False to turn the restricted function on/off """ self.__restricted_vocabulary = is_restricted def select_line(self): """ select the tagline ... """ self.__tag_line.selectAll() self.__tag_line.setFocus(QtCore.Qt.OtherFocusReason) def __text_changed_by_user(self, text): # create a QByteArray in utf8 all_text = text.toUtf8() # make a python string out of it all_text = str(all_text) # convert the python string tu unicode utf-8 all_text = unicode(all_text, "utf-8") if self.__check_not_empty: if all_text is not None and all_text != "": self.emit(QtCore.SIGNAL("line_empty"), False) else: self.emit(QtCore.SIGNAL("line_empty"), True) text = all_text[:self.__tag_line.cursorPosition()] ## remove whitespace and filter out duplicates by using a set tag_set = set([]) for tag in all_text.split(self.__tag_separator): strip_tag = tag.strip() if strip_tag != "": tag_set.add(strip_tag) max_tags = self.__max_tags if self.__datestamp_hidden: max_tags = max_tags - 1; ## do not proceed if the max tag count is reached if len(tag_set) > max_tags: self.emit(QtCore.SIGNAL("tag_limit_reached"), True) self.__check_tag_limit = True return else: if self.__check_tag_limit: self.emit(QtCore.SIGNAL("tag_limit_reached"), False) self.__check_tag_limit = False prefix = text.split(self.__tag_separator)[-1].strip() if not self.__completer_active: self.__update_completer(tag_set, prefix) def __update_completer(self, tag_set, completion_prefix): if self.__tag_list is None: return tags = list(set(self.__tag_list).difference(tag_set)) #tags = list(self.__tag_list) model = QStringListModel(tags, self) self.__completer.setModel(model) self.__completer.setCompletionPrefix(completion_prefix) if self.__restricted_vocabulary: self.__check_vocabulary(tag_set, completion_prefix) if completion_prefix.strip() != '': ## use the default completion algorithm self.__completer.complete() def __check_finished_tags(self, typed_tags_list): """ use this method to control all typed tags. this means all tags terminated with a comma """ pass def __check_in_completion_list(self, tag): """ if a written tag equals a tag of the completion list - the tag will be removed from the completion list so the completer will return a completion count of 0 for this tag. in this case there would be displayed an error message at the dialog (when controlled vocab is activated) so check manually, if the provided tag is contained in the suggestion_list """ #for sug_tag in self.__tag_list: # if sug_tag == tag: # return True #return False return tag in self.__tag_list def __check_vocabulary(self, tag_set, completion_prefix): """ have a look at the entered tag to be completed. if restricted vocabulary is turned on: datestamps do not have to be checked. """ not_allowed_tags_count = 0 no_completion_found = False stripped_text = unicode(self.__tag_line.text()).strip() ##when a tag separator is on the last position, there should have been entered a new tag ##check this tag for its correctness if len(stripped_text) > 0: ##check if all written tags are allowed (incl. datestamps an expiry tags) for tag in tag_set: ## tag can be a datestamp -> OK if not SpecialCharHelper.is_datestamp(tag) and tag != "": ## tag can be an expiry tag -> OK # if self.__expiry_prefix is not None and not SpecialCharHelper.is_expiry_tag(self.__expiry_prefix, tag): if self.__expiry_prefix is None or not SpecialCharHelper.is_partial_expiry_tag(self.__expiry_prefix, tag): if unicode(tag) not in self.__tag_list: not_allowed_tags_count += 1 if(completion_prefix.strip() == ""): ## if the prefix is an empty string - manually set the completion_count to 0 ## because the completer would return the whole number of tags in its suggestion list completion_count = 0 else: completion_count = self.__completer.completionCount() if self.__restricted_vocabulary and completion_count == 0: ## additionally check if the prefix equals a tag from the suggestion list ## this has to be done, because we do not get a completionCount > 0 if the prefix equals a given tag #if completion_prefix not in self.__tag_list: if completion_prefix is not None and len(completion_prefix) > 0 and completion_prefix.strip() != "": ## just send the signal if the tag is no datestamp AND if it is no full tag if not SpecialCharHelper.is_datestamp(completion_prefix) and not self.__check_in_completion_list(completion_prefix): if not SpecialCharHelper.is_partial_expiry_tag(self.__expiry_prefix, completion_prefix): no_completion_found = True ## there are tags (terminated with comma) which are not in the allowed tag_list if not_allowed_tags_count > 1: self.emit(QtCore.SIGNAL("no_completion_found"), True) return if not_allowed_tags_count > 0: ## in this case the user has entered a not allowed tag and terminated it with a comma to mark it as a tag ## the completion count is 0 because there is nothing after the last comma in the taglist if completion_count == 0: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## it could be the case, that the user is still typing an allowed tag ## so check, if the completer has a possible completion ## if not -> send the signal if no_completion_found: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## everytime there is no completion found, emit the signal elif no_completion_found: self.emit(QtCore.SIGNAL("no_completion_found"), True) return ## in this case everything is fine self.emit(QtCore.SIGNAL("no_completion_found"), False) def __text_selected(self, text): self.__activated_text = text cursor_pos = self.__tag_line.cursorPosition() before_text = unicode(self.__tag_line.text())[:cursor_pos] #after_text = unicode(self.__tag_line.text())[cursor_pos:] prefix_len = len(before_text.split(self.__tag_separator)[-1].strip()) self.__tag_line.setText("%s%s" % (before_text[:cursor_pos - prefix_len], text)) self.__tag_line.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) def __text_activated(self, text): """ a suggestion has been choosen by the user """ self.__text_selected(text) self.emit(QtCore.SIGNAL("activated")) def get_tag_list(self): tag_string = unicode(self.__tag_line.text()) result = set([]) tag_list = tag_string.split(self.__tag_separator) for tag in tag_list: strip_tag = tag.strip() if strip_tag != "": result.add(strip_tag) # if the datestamp is hidden, add it manually to the taglist if self.__datestamp_hidden: result.add(self.__datestamp) return result def get_tag_line(self): return self.__tag_line def get_completer(self): return self.__completer def set_enabled(self, enable): self.__tag_line.setEnabled(enable) def set_text(self, text): self.__tag_line.setText(text) def set_tag_completion_list(self, tag_list): self.__tag_list = tag_list self.__completer.setModel(QtGui.QStringListModel(QtCore.QStringList(tag_list))) def get_tag_completion_list(self): return self.__tag_list def is_empty(self): if self.__tag_line.text() == "": return True else: return False def set_place_holder_text(self, text): self.__tag_line.setPlaceholderText(text)
class IdePanel(QPlainTextEdit): def __init__(self): QPlainTextEdit.__init__(self) self.setWordWrapMode(QTextOption.NoWrap) self.setFont(QFont("monospace", 10)) self.setCursorWidth(2) self.installEventFilter(self) self.cursorPositionChanged.connect(self.showHelp) self.completer = QCompleter(self) self.completer.setWidget(self) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.activated.connect(self.insertCompletion) auto_complete = QShortcut(QKeySequence("Ctrl+Space"), self) auto_complete.activated.connect(self.activateCompleter) copy_line = QShortcut(QKeySequence("Ctrl+D"), self) copy_line.activated.connect(self.duplicateLine) select_fragment = QShortcut(QKeySequence("Ctrl+J"), self) select_fragment.activated.connect(self.selectFragment) def showHelp(self): text_cursor = self.textCursor() user_data = text_cursor.block().userData() if user_data is not None: configuration_line = user_data.configuration_line if configuration_line.keyword().hasKeywordDefinition(): HelpCenter.getHelpCenter("ERT").setHelpMessageLink("config/" + configuration_line.documentationLink()) def getText(self): return self.document().toPlainText() def eventFilter(self, qobject, qevent): if qobject == self and qevent.type() == QEvent.ToolTip: text_cursor = self.cursorForPosition(qevent.pos()) pos = text_cursor.positionInBlock() user_data = text_cursor.block().userData() if user_data is not None: #: :type: ConfigurationLine configuration_line = user_data.configuration_line # if configuration_line.keyword().hasKeywordDefinition(): # print(configuration_line.keyword().keywordDefinition().documentation) if pos in configuration_line.keyword(): self.setToolTip(configuration_line.validationStatusForToken(configuration_line.keyword()).message()) else: for argument in configuration_line.arguments(): if pos in argument: self.setToolTip(configuration_line.validationStatusForToken(argument).message()) else: self.setToolTip("") return QPlainTextEdit.eventFilter(self, qobject, qevent) def activateCompleter(self): text_cursor = self.textCursor() block = self.document().findBlock(text_cursor.position()) position_in_block = text_cursor.positionInBlock() self.selectWordUnderCursor(text_cursor) word = unicode(text_cursor.selectedText()) user_data = block.userData() self.completer.setCompletionPrefix(word) show_completer = False if user_data is None: self.completer.setModel(QStringListModel(self.handler_names)) show_completer = True else: keyword = user_data.keyword options = keyword.handler.parameterOptions(keyword, word, position_in_block) if len(options) == 1: self.insertCompletion(options[0]) elif len(options) > 1: self.completer.setModel(QStringListModel(options)) if self.completer.completionCount() == 1: self.insertCompletion(self.completer.currentCompletion()) else: show_completer = True if show_completer: rect = self.cursorRect(text_cursor) rect.setWidth( self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width() ) self.completer.complete(rect) def keyPressEvent(self, qkeyevent): if self.completer.popup().isVisible(): dead_keys = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab] if qkeyevent.key() in dead_keys: qkeyevent.ignore() return if qkeyevent.modifiers() == Qt.ShiftModifier: if qkeyevent.key() & Qt.Key_Delete == Qt.Key_Delete: self.deleteLine() QPlainTextEdit.keyPressEvent(self, qkeyevent) def insertCompletion(self, string): text_cursor = self.textCursor() self.selectWordUnderCursor(text_cursor) text_cursor.insertText(string) def isCursorInSpace(self): text_cursor = self.textCursor() if text_cursor.positionInBlock() > 0: text_cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor) text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) if text_cursor.positionInBlock() < text_cursor.block().length() - 1: text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) if unicode(text_cursor.selectedText()).strip() == "": return True return False def selectWordUnderCursor(self, text_cursor): if not self.isCursorInSpace(): # text_cursor.select(QTextCursor.WordUnderCursor) # pattern = "[\s|\v|\f|\n|\r|\t|\xe2\x80\xa8|\xe2\x80\xa9]" # pattern = "[\\s|\\xe2\\x80\\xa9|\\xe2\\x80\\xa8]" block_start = 0 block_end = text_cursor.block().length() cursor_pos = text_cursor.positionInBlock() pos = cursor_pos pattern = u"[\s\u2029\u2028]" while pos >= block_start: text_cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) text = text_cursor.selectedText() if re.search(pattern, text): break pos -= 1 text_cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor) while pos < block_end: text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) text = text_cursor.selectedText() if re.search(pattern, text): break pos += 1 text_cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) # pattern = "[\\s]" # start = self.document().find(QRegExp(pattern), text_cursor, QTextDocument.FindBackward | QTextDocument.FindCaseSensitively) # end = self.document().find(QRegExp(pattern), text_cursor, QTextDocument.FindCaseSensitively) # block_end_pos = text_cursor.block().position() + text_cursor.block().length() # # text_cursor.setPosition(start.position(), QTextCursor.MoveAnchor) # # text_cursor.setPosition(min(block_end_pos, end.position() - 1), QTextCursor.KeepAnchor) # text_cursor.setPosition(end.position() - 1, QTextCursor.KeepAnchor) def deleteLine(self): text_cursor = self.textCursor() text_cursor.beginEditBlock() text_cursor.select(QTextCursor.LineUnderCursor) text_cursor.removeSelectedText() text_cursor.deletePreviousChar() text_cursor.movePosition(QTextCursor.NextBlock) text_cursor.movePosition(QTextCursor.StartOfLine) self.setTextCursor(text_cursor) text_cursor.endEditBlock() def duplicateLine(self): text_cursor = self.textCursor() text_cursor.beginEditBlock() text_cursor.select(QTextCursor.LineUnderCursor) text = text_cursor.selectedText() text_cursor.movePosition(QTextCursor.EndOfLine) text_cursor.insertBlock() text_cursor.insertText(text) text_cursor.endEditBlock() def selectFragment(self): text_cursor = self.textCursor() self.selectWordUnderCursor(text_cursor) self.setTextCursor(text_cursor)
class IdePanel(QPlainTextEdit): def __init__(self): QPlainTextEdit.__init__(self) self.setWordWrapMode(QTextOption.NoWrap) self.setFont(QFont("monospace", 10)) self.setCursorWidth(2) self.installEventFilter(self) self.cursorPositionChanged.connect(self.showHelp) self.completer = QCompleter(self) self.completer.setWidget(self) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.activated.connect(self.insertCompletion) auto_complete = QShortcut(QKeySequence("Ctrl+Space"), self) auto_complete.activated.connect(self.activateCompleter) copy_line = QShortcut(QKeySequence("Ctrl+D"), self) copy_line.activated.connect(self.duplicateLine) select_fragment = QShortcut(QKeySequence("Ctrl+J"), self) select_fragment.activated.connect(self.selectFragment) def showHelp(self): text_cursor = self.textCursor() user_data = text_cursor.block().userData() if user_data is not None: configuration_line = user_data.configuration_line if configuration_line.keyword().hasKeywordDefinition(): HelpCenter.getHelpCenter("ERT").setHelpMessageLink("config/" + configuration_line.documentationLink()) def getText(self): return self.document().toPlainText() def eventFilter(self, qobject, qevent): if qobject == self and qevent.type() == QEvent.ToolTip: text_cursor = self.cursorForPosition(qevent.pos()) pos = text_cursor.positionInBlock() user_data = text_cursor.block().userData() if user_data is not None: #: :type: ConfigurationLine configuration_line = user_data.configuration_line # if configuration_line.keyword().hasKeywordDefinition(): # print(configuration_line.keyword().keywordDefinition().documentation) if pos in configuration_line.keyword(): self.setToolTip(configuration_line.validationStatusForToken(configuration_line.keyword()).message()) else: for argument in configuration_line.arguments(): if pos in argument: self.setToolTip(configuration_line.validationStatusForToken(argument).message()) else: self.setToolTip("") return QPlainTextEdit.eventFilter(self, qobject, qevent) def activateCompleter(self): text_cursor = self.textCursor() block = self.document().findBlock(text_cursor.position()) position_in_block = text_cursor.positionInBlock() self.selectWordUnderCursor(text_cursor) word = unicode(text_cursor.selectedText()) user_data = block.userData() self.completer.setCompletionPrefix(word) show_completer = False if user_data is None: self.completer.setModel(QStringListModel(self.handler_names)) show_completer = True else: keyword = user_data.keyword options = keyword.handler.parameterOptions(keyword, word, position_in_block) if len(options) == 1: self.insertCompletion(options[0]) elif len(options) > 1: self.completer.setModel(QStringListModel(options)) if self.completer.completionCount() == 1: self.insertCompletion(self.completer.currentCompletion()) else: show_completer = True if show_completer: rect = self.cursorRect(text_cursor) rect.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width()) self.completer.complete(rect) def keyPressEvent(self, qkeyevent): if self.completer.popup().isVisible(): dead_keys = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab] if qkeyevent.key() in dead_keys: qkeyevent.ignore() return if qkeyevent.modifiers() == Qt.ShiftModifier: if qkeyevent.key() & Qt.Key_Delete == Qt.Key_Delete: self.deleteLine() QPlainTextEdit.keyPressEvent(self, qkeyevent) def insertCompletion(self, string): text_cursor = self.textCursor() self.selectWordUnderCursor(text_cursor) text_cursor.insertText(string) def isCursorInSpace(self): text_cursor = self.textCursor() if text_cursor.positionInBlock() > 0: text_cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor) text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) if text_cursor.positionInBlock() < text_cursor.block().length() - 1: text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) if unicode(text_cursor.selectedText()).strip() == "": return True return False def selectWordUnderCursor(self, text_cursor): if not self.isCursorInSpace(): # text_cursor.select(QTextCursor.WordUnderCursor) # pattern = "[\s|\v|\f|\n|\r|\t|\xe2\x80\xa8|\xe2\x80\xa9]" # pattern = "[\\s|\\xe2\\x80\\xa9|\\xe2\\x80\\xa8]" block_start = 0 block_end = text_cursor.block().length() cursor_pos = text_cursor.positionInBlock() pos = cursor_pos pattern = u"[\s\u2029\u2028]" while pos >= block_start: text_cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) text = text_cursor.selectedText() if re.search(pattern, text): break pos -= 1 text_cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor) while pos < block_end: text_cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) text = text_cursor.selectedText() if re.search(pattern, text): break pos += 1 text_cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) # pattern = "[\\s]" # start = self.document().find(QRegExp(pattern), text_cursor, QTextDocument.FindBackward | QTextDocument.FindCaseSensitively) # end = self.document().find(QRegExp(pattern), text_cursor, QTextDocument.FindCaseSensitively) # block_end_pos = text_cursor.block().position() + text_cursor.block().length() # # text_cursor.setPosition(start.position(), QTextCursor.MoveAnchor) # # text_cursor.setPosition(min(block_end_pos, end.position() - 1), QTextCursor.KeepAnchor) # text_cursor.setPosition(end.position() - 1, QTextCursor.KeepAnchor) def deleteLine(self): text_cursor = self.textCursor() text_cursor.beginEditBlock() text_cursor.select(QTextCursor.LineUnderCursor) text_cursor.removeSelectedText() text_cursor.deletePreviousChar() text_cursor.movePosition(QTextCursor.NextBlock) text_cursor.movePosition(QTextCursor.StartOfLine) self.setTextCursor(text_cursor) text_cursor.endEditBlock() def duplicateLine(self): text_cursor = self.textCursor() text_cursor.beginEditBlock() text_cursor.select(QTextCursor.LineUnderCursor) text = text_cursor.selectedText() text_cursor.movePosition(QTextCursor.EndOfLine) text_cursor.insertBlock() text_cursor.insertText(text) text_cursor.endEditBlock() def selectFragment(self): text_cursor = self.textCursor() self.selectWordUnderCursor(text_cursor) self.setTextCursor(text_cursor)
class Console(QObject): @property def _currentCursor(self): return self.widget.textCursor() @_currentCursor.setter def _currentCursor(self, cursor): self.widget.setTextCursor(cursor) @property def _endCursor(self): return self._lastBlock.endCursor() @property def _currentBlock(self): return TextBlock(self._currentCursor) @property def _document(self): return self.widget.document() def _canDeletePreviousChar(self): if self._currentBlock.type in TextBlock.EDITABLE_TYPES and self._cursorInEditableArea(): return self._currentBlock.containsCursor(self._currentCursor, in_active_area='strict') return False def _cursorInEditableArea(self, cursor=None): if cursor is None: cursor = self._currentCursor return self._lastBlock.isCursorInRelatedBlock(cursor) and self._currentBlock.containsCursor(cursor) def _canEditCurrentChar(self): if not self._currentBlock.type in TextBlock.EDITABLE_TYPES: return False if not self._currentBlock.containsCursor(self._currentCursor): return False def _gotoBlockStart(self): self._currentCursor = self._currentBlock.startCursor() def _gotoBlockEnd(self): self._currentCursor = self._currentBlock.endCursor() @property def _lastBlock(self): return TextBlock(self._document.lastBlock()) def _gotoEnd(self): self._currentCursor = self._endCursor def _insertBlock(self, cursor=None, block_type=TextBlock.TYPE_CODE_CONTINUED, content=""): if cursor is None: cursor = self._currentCursor b = TextBlock(cursor, create_new=True, typ=block_type) if len(content): b.appendText(content) return b def _appendBlock(self, block_type, content="", html=False): b = TextBlock(self._endCursor, create_new=True, typ=block_type) if len(content) > 0: if html: b.appendHtml(content) else: b.appendText(content) return b def _blocks(self): b = TextBlock(self._document.begin()) ret = [] while not b.isLast(): ret.append(b) b = b.next() ret.append(b) return ret def _saveBlocksToJSON(self,num_of_blocks=None): if num_of_blocks is None: num_of_blocks = 0 ret = [] for b in self._blocks()[-num_of_blocks:]: ret += [json.dumps({'block_type':b.type,'content':b.content()})] return '\n'.join(ret) def save_history(self, fname=None): if fname is None: fname = config.history_file fname = expanduser(fname) if fname.endswith('.gz'): f = gzip.open(fname,'wb') else: f = open(fname,'wb') try: hist_size = config.history_size except: hist_size = None f.write(self._saveBlocksToJSON(num_of_blocks=hist_size)) f.close() def load_history(self, fname=None): if fname is None: fname = config.history_file fname = expanduser(fname) if not os.path.exists(fname): return if fname.endswith('.gz'): f = gzip.open(fname,'rb') else: f = open(fname,'rb') blocks = f.readlines() for b in blocks: bl = json.loads(b.strip()) self._appendBlock(**bl) def _joinCurrentToPreviousBlock(self): logger.debug(msg("Deleting current block")) cur = self._currentBlock prev = cur.previous() prev.appendText(cur.content()) cur.deleteBlock() def wordUnderCursor(self, cursor=None, delta=0): """ Returns the word (name) under cursor @cursor (or self._currentCursor) if @cursor is None. The cursor is shifted by @delta """ if cursor is None: cursor = self._currentCursor if delta > 0: cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, delta) elif delta < 0: cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, abs(delta)) return self._currentBlock.wordUnderCursor(cursor) def _containing_function(self): """ Determines whether the cursor is located in the argument part of a function. If yes, returns a (@cursor,@func_name) where @cursor points to the opening brace and @func_name is the function name. """ ct = self.textToCursor() i=len(ct)-1 brackets = 0 logger.debug("ct == " + ct) while i >= 0: if ct[i] == ')': brackets -=1 if ct[i] == '(': if brackets < 0: brackets += 1 elif i > 0 and ct[i-1] not in TextBlock.WORD_STOP_CHARS: cursor = self._currentBlock.cursorAt(i-1) func_name = self._currentBlock.wordUnderCursor(cursor) cursor = self._currentBlock.cursorAt(i+len(func_name)) logger.debug("func_name == "+ func_name) return (cursor,func_name) i -= 1 return None def textToCursor(self): return self._currentBlock.contentToCursor(self._currentCursor) def _guessDictPrefix(self, char): """ Tries to determine whether the user is typing a string-key in a dict. If yes returns the dict name and the key typed so far. Otherwise returns None. """ c = self._currentCursor block_content = self._currentBlock.content(include_decoration=True) pos = min(c.positionInBlock(), len(block_content)) block_content = unicode(block_content[:pos]+char) double_quotes = block_content.count('"') single_quotes = block_content.count("'") if double_quotes % 2 == 0 and single_quotes % 2 == 0: return None stop_chars = '"\' ' ret = [] while pos >= 0 and block_content[pos] not in stop_chars: ret.append(block_content[pos]) pos -= 1 ret.reverse() if pos > 0 and block_content[pos-1] == '[': pos -= 1 dct = [] while pos >= 0 and block_content[pos] not in [ ' ', "\n", "\t"]: dct.append(block_content[pos]) pos -= 1 dct.reverse() logger.debug(msg('dct:',''.join(dct[:-1]), 'key:',''.join(ret))) return ''.join(dct[:-1]), ''.join(ret) else: return None def _guessStringPrefix(self, char): """Tries to guess whether the user is typing a string (i.e. inside quotes) and if yes, returns the string typed so far. Otherwise returns None. """ c = self._currentCursor block_content = self._currentBlock.content(include_decoration=True) pos = min(c.positionInBlock(), len(block_content)) block_content = unicode(block_content[:pos]+char) double_quotes = block_content.count('"') single_quotes = block_content.count("'") if double_quotes % 2 == 0 and single_quotes % 2 == 0: return None stop_chars = '"\' ' ret = [] while pos >= 0 and block_content[pos] not in stop_chars: ret.append(block_content[pos]) pos -= 1 ret.reverse() logger.debug(''.join(ret)) return ''.join(ret) @pyqtSlot(unicode) def _insertCompletion(self, completion): insertion = completion[len(self.completer.completionPrefix()):] if len(insertion) == 0: self._process_enter() else: c = self._currentCursor c.movePosition(QTextCursor.Left) c.movePosition(QTextCursor.EndOfWord) c.insertText(insertion) self._currentCursor = c DEFAULT_INDENT = 4 MODE_RUNNING = 0 MODE_CODE_EDITING = 1 MODE_RAW_INPUT = 2 MODE_READ_ONLY = 3 MODE_WAITING_FOR_INTERRUPT = 4 # run_code = pyqtSignal(unicode) run_code = signal(unicode) read_line = signal(unicode) restart_shell = signal() interrupt_shell = signal() quit = pyqtSignal() file_watched = signal(unicode) def __init__(self, widget): super(Console, self).__init__() self.font_size = 10 self.font = QFont("Ubuntu Mono", self.font_size) self.allow_quit = True self.indent = self.DEFAULT_INDENT self.widget = widget self.hilighter = PythonHighlighter(self.widget.document()) self.parser = PyParser(self.indent, self.indent) self.widget.setFont(self.font) # The source file's we are watching self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self._sourceChanged) self._watched_files_menu = QMenu(self.widget.tr("&Watched Files")) self._watched_files_actions = {} self._lost_files = [] self._widgetKeyPressEvent = self.widget.keyPressEvent self.widget.keyPressEvent = self.keyPressEvent self._widgetFocusInEvent = self.widget.focusInEvent self.widget.focusInEvent = self.focusInEvent self.widget.dragEnterEvent = self.dragEnterEvent self.widget.dragMoveEvent = self.dragMoveEvent self.widget.dropEvent = self.dropEvent # Code Completion self.completer = QCompleter( [kw for kw in PythonHighlighter.keywords if len(kw) > 3]) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.activated.connect(self._insertCompletion) self.completer.popup().setStyleSheet( "QWidget {border-width: 1px; border-color: black;}") self.completer.popup().setVerticalScrollBarPolicy( Qt.ScrollBarAlwaysOff) self.completer.setWidget(self.widget) self.completion_enabled = True if config.history: self.load_history() self._lastBlock.setType(TextBlock.TYPE_MESSAGE) self._lastBlock.appendText("PyShell v 0.5: Starting ...") self._write_message("Python version ", sys.version) self.start_editing() # Restart shell timer self.timer = QTimer(self) @property def watched_files_menu(self): return self._watched_files_menu @property def mode(self): return self._mode @pyqtSlot() def increase_font(self): self.font_size += 1 self.font = QFont("Ubuntu Mono", self.font_size) self.widget.setFont(self.font) @pyqtSlot() def decrease_font(self): self.font_size -= 1 self.font = QFont("Ubuntu Mono", self.font_size) self.widget.setFont(self.font) @pyqtSlot(unicode, unicode) def write(self, stream, string): #assert self.mode == Console.MODE_RUNNING or self.mode == Console.MODE_WAITING_FOR_INTERRUPT, "Cannot write " + \ # string + " to console stream "+stream+" in "+str( # self.mode) + " (need to be RUNNING/INTERRUPT mode) " if string.startswith('<html_snippet>'): self._lastBlock.appendHtml(string.replace( "<html_snippet>", "").replace("</html_snippet>", "")) else: lines = string.split('\n') block_type = block_type_for_stream(stream) for ln in lines[:-1]: self._lastBlock.appendText(ln) self._lastBlock.setType(block_type) self._appendBlock(block_type) self._lastBlock.appendText(lines[-1]) self._gotoEnd() def write_object(self, obj): logger.debug(msg("Received object", obj, "writing it to stdout")) self.write('stdout',print_hooks.html_repr(obj, self._document)) @pyqtSlot() def do_readline(self): assert self.mode != Console.MODE_RAW_INPUT, "Cannot call readline before previous call finished" assert self.mode == Console.MODE_RUNNING, "readline may be called only in the RUNNING mode" self._lastBlock.setType(TextBlock.TYPE_RAW_INPUT) self._mode = Console.MODE_RAW_INPUT @pyqtSlot() def shell_restarted(self): logger.debug("Shell restarted!") self._write_message("<span style='color:green'>","="*20, "</span> SHELL RESTARTED <span style='color:green'>","="*20,"</span>", html=True) self._write_message('Python version ',sys.version, html=True) self.start_editing() @pyqtSlot() def finished_running(self): logger.debug("Finished running.") if len(self._lastBlock.content()) == 0: self._lastBlock.deleteBlock() self.start_editing() @pyqtSlot() def start_editing(self): new_code_block = self._appendBlock(TextBlock.TYPE_CODE_START) self._gotoEnd() self._mode = Console.MODE_CODE_EDITING @pyqtSlot() def exec_command(self,*args, **kwargs): """ Executes a command in the console context. The command is given by the cmd keyword argument. It is either a predefined command or the name of a method of the console object, in which case this method is called with *args and **kwargs (where cmd is first deleted from kwargs) """ if 'cmd' in kwargs: cmd = kwargs['cmd'] del kwargs['cmd'] if cmd == 'watch.list_files': self._write_message("Currently watching the following files:") self._write_message([unicode(x) for x in self.watcher.files()]) self._write_message("Total watched:",len(self.watcher.files())) logger.debug(msg([x for x in self.watcher.files()])) elif cmd == 'watch.reload': self._reload_watch_files() elif cmd in dir(self): attr = self.__getattribute__(cmd) if type(attr) == type(self.exec_command): attr(*args, **kwargs) else: self.write_object(attr) def _write_message(self, *args, **kwargs): """ Outputs a "System" message to the console. The message is composed of string representations of args. If the keyword argument html is present, the message is assumed to be html. """ if 'html' in kwargs: html = kwargs['html'] else: html = True msg = u'' for a in args: try: msg += unicode(a) except Exception, e: logger.warn("Exception while writing to console" + str(e)) self._appendBlock(TextBlock.TYPE_MESSAGE, msg, html)
class WithCompletion(QPlainTextEdit): """\ Mixin to add base completion funtionallity to QPlainTextEdit It will propose completion with words from current file When the word you are writting is bigger than two, it will request for possible completions but with no default selection. If you press Ctrl-Space, the proposal will choose the first one as default Specific mixings will have to implement the get_text_completion_list method """ def get_text_completion_list(self): return [] def __init__(self, *args): self.model_completer = QStringListModel() self.completer = QCompleter(self) self.completer.popup().setFont(QFont("Monospace", 11)) # self.completer.setModelSorting(QCompleter.CaseInsensitivelySortedModel self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setWrapAround(False) self.completer.setWidget(self) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setModel(self.model_completer) self.completer.activated.connect(self.insert_completion) def keyPressEvent(self, event): event_key = event.key() if self.completer.popup().isVisible(): # The following keys are forwarded by the completer to the widget if event_key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab]: if self.completer.popup().currentIndex().row() >= 0: event.ignore() return # let the completer do default behavior else: self.completer.popup().hide() super(WithCompletion, self).keyPressEvent(event) if (event.modifiers() | event.key()) == QKeySequence("Ctrl+Space"): self.show_completer(True) elif event_key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab]: pass else: pressed_key_as_string = QKeySequence(event.key()).toString() word_till_cursor = unicode(self.word_till_cursor()) if (len(word_till_cursor) > 2 or (self.completer.popup().isVisible() and len(word_till_cursor) > 0)) and ( (event.text() != "" and regex.match("^[A-Za-z0-9_-]*$", unicode(pressed_key_as_string[0]))) or self.completer.popup().isVisible() ): self.show_completer(self.completer.popup().currentIndex().row() >= 0) else: self.completer.popup().setCurrentIndex(self.completer.completionModel().index(-1, 0)) self.completer.popup().hide() def show_completer(self, select_first): QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) completion_words = self.get_text_completion_list() QApplication.restoreOverrideCursor() if not completion_words or len(completion_words) < 1: self.completer.popup().hide() return self.model_completer.setStringList(completion_words) cr = self.cursorRect() width = ( self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width() ) cr.setWidth(width if width < 300 else 300) self.completer.complete(cr) if select_first: self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0)) def word_under_cursor(self): result = "" for i in range(self.current_pos_init_of_word(), self.current_pos_end_of_word()): result = result + unichr(self.document().characterAt(i).unicode()) return QString(result) def word_till_cursor(self): result = "" for i in range(self.current_pos_init_of_word(), self.textCursor().position()): result = result + unichr(self.document().characterAt(i).unicode()) return QString(result) def current_pos_init_of_word(self): pos = self.textCursor().position() - 1 while True: char = unichr(self.document().characterAt(pos).unicode()) if not char in WORD_SYMBOLS or pos < 0: # if char=='\n' or re.match("^[A-Za-z0-9_-ñÑ]*$", unicode(char)) == None or pos==0: break pos = pos - 1 return pos + 1 def current_pos_end_of_word(self): pos = self.textCursor().position() while True: char = unichr(self.document().characterAt(pos).unicode()) if not char in WORD_SYMBOLS or pos == self.document().characterCount(): # if char.isSpace() or re.match("^[A-Za-z0-9_-ñÑ]*$", unicode(char)) == None: break pos = pos + 1 return pos def insert_completion(self, completion_text): if self.completer.widget() != self: return tc = self.textCursor() till_pos = tc.position() tc.movePosition( QTextCursor.Left, QTextCursor.MoveAnchor, self.textCursor().position() - self.current_pos_init_of_word() ) # tc.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, self.current_pos_end_of_word()-self.current_pos_init_of_word()) tc.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, till_pos - self.current_pos_init_of_word()) tc.removeSelectedText() tc.insertText(completion_text) self.setTextCursor(tc)