class TextEditor(QsciScintilla): """QScintilla text editor.""" word_changed = Signal() def __init__(self, parent: QWidget): """UI settings.""" super(TextEditor, self).__init__(parent) # Set the default font. if system() == "Linux": font_name = "DejaVu Sans Mono" elif system() == "Windows": font_name = "Courier New" elif system() == "Darwin": font_name = "Andale Mono" else: font_name = "Courier New" self.font = QFont(font_name) self.font.setFixedPitch(True) self.font.setPointSize(14) self.setFont(self.font) self.setMarginsFont(self.font) self.setUtf8(True) self.setEolMode(QsciScintilla.EolUnix) # Margin 0 is used for line numbers. font_metrics = QFontMetrics(self.font) self.setMarginsFont(self.font) self.setMarginWidth(0, font_metrics.width("0000") + 4) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#cccccc")) # Brace matching. self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Current line visible with special background color. self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) # Set lexer. self.lexer_option = "Markdown" self.set_highlighter("Markdown") self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, font_name.encode('utf-8')) # Don't want to see the horizontal scrollbar at all. self.setWrapMode(QsciScintilla.WrapWord) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Auto completion. self.setAutoCompletionCaseSensitivity(True) self.setAutoCompletionSource(QsciScintilla.AcsDocument) self.setAutoCompletionThreshold(2) # Edge mode. self.setEdgeMode(QsciScintilla.EdgeNone) self.setEdgeColumn(80) self.setEdgeColor(Qt.blue) # Indentations. self.setAutoIndent(True) self.setIndentationsUseTabs(False) self.setTabWidth(4) self.setTabIndents(True) self.setBackspaceUnindents(True) self.setIndentationGuides(True) # Widget size. self.setMinimumSize(400, 450) # Remove trailing blanks. self.__no_trailing_blanks = True # Spell checker indicator [0] self.indicatorDefine(QsciScintilla.SquiggleIndicator, 0) # Keyword indicator [1] self.indicatorDefine(QsciScintilla.BoxIndicator, 1) self.cursorPositionChanged.connect(self.__catch_word) self.word = "" # Undo redo self.__set_command(QsciCommand.Redo, Qt.ControlModifier | Qt.ShiftModifier | Qt.Key_Z) def __set_command(self, command_type: int, shortcut: int): """Set editor shortcut to replace the default setting.""" commands: QsciCommandSet = self.standardCommands() command = commands.boundTo(shortcut) if command is not None: command.setKey(0) command: QsciCommand = commands.find(command_type) command.setKey(shortcut) @Slot(int, int) def __catch_word(self, line: int, index: int): """Catch and indicate current word.""" self.__clear_indicator_all(1) pos = self.positionFromLineIndex(line, index) _, _, self.word = self.__word_at_pos(pos) for m in _finditer(r'\b' + self.word + r'\b', self.text(), re.IGNORECASE): self.fillIndicatorRange( *self.lineIndexFromPosition(m.start()), *self.lineIndexFromPosition(m.end()), 1 ) @Slot(str) def set_highlighter(self, option: str): """Set highlighter by list.""" self.lexer_option = option lexer = QSCI_HIGHLIGHTERS[option]() lexer.setDefaultFont(self.font) self.setLexer(lexer) @Slot(bool) def setEdgeMode(self, option: bool): """Set edge mode option.""" super(TextEditor, self).setEdgeMode( QsciScintilla.EdgeLine if option else QsciScintilla.EdgeNone ) def setSelection(self, p1: int, p2: int, p3: Optional[int] = None, p4: Optional[int] = None): if p3 is p4 is None: line1, index1 = self.lineIndexFromPosition(p1) line2, index2 = self.lineIndexFromPosition(p2) super(TextEditor, self).setSelection(line1, index1, line2, index2) else: super(TextEditor, self).setSelection(p1, p2, p3, p4) @Slot(bool) def set_remove_trailing_blanks(self, option: bool): """Set remove trailing blanks during 'setText' method.""" self.__no_trailing_blanks = option def wheelEvent(self, event): """Mouse wheel event.""" if QApplication.keyboardModifiers() != Qt.ControlModifier: super(TextEditor, self).wheelEvent(event) return if event.angleDelta().y() >= 0: self.zoomIn() else: self.zoomOut() def contextMenuEvent(self, event): """Custom context menu.""" # Spell refactor. menu: QMenu = self.createStandardContextMenu() menu.addSeparator() correction_action = QAction("&Refactor Words", self) correction_action.triggered.connect(self.__refactor) menu.addAction(correction_action) menu.exec(self.mapToGlobal(event.pos())) def __replace_all(self, word: str, replace_word: str): """Replace the word for all occurrence.""" found = self.findFirst(word, False, False, True, True) while found: self.replace(replace_word) found = self.findNext() def __word_at_pos(self, pos: int) -> Tuple[int, int, str]: """Return the start and end pos of current word.""" return ( self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos, True), self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos, True), self.wordAtLineIndex(*self.getCursorPosition()) ) @Slot() def __refactor(self): """Refactor words.""" pos = self.positionFromLineIndex(*self.getCursorPosition()) start, end, words = self.__word_at_pos(pos) if not words: return # Camel case. word = words for m in _finditer(r'[A-Za-z][a-z]+', words): if m.start() < pos - start < m.end(): word = m.group(0) break answer, ok = QInputDialog.getItem( self, "Spell correction", f"Refactor word: \"{word}\"", _spell.candidates(word) ) if ok: self.__replace_all(words, words.replace(word, answer)) def __cursor_move_next(self): """Move text cursor to next character.""" line, index = self.getCursorPosition() self.setCursorPosition(line, index + 1) def __cursor_next_char(self) -> str: """Next character of cursor.""" pos = self.positionFromLineIndex(*self.getCursorPosition()) if pos + 1 > self.length(): return "" return self.text(pos, pos + 1) def keyPressEvent(self, event): """Input key event.""" key = event.key() selected_text = self.selectedText() # Commas and parentheses. parentheses = list(_parentheses) commas = list(_commas) if self.lexer_option in {"Python", "C++"}: parentheses.extend(_parentheses_code) if self.lexer_option in {"Markdown", "HTML"}: parentheses.extend(_parentheses_html) commas.extend(_commas_markdown) # Skip the closed parentheses. for k1, k2, t0, t1 in parentheses: if key == k2: if self.__cursor_next_char() == t1: self.__cursor_move_next() return # Wrap the selected text. if selected_text: if len(selected_text) == 1 and not selected_text.isalnum(): pass elif selected_text[0].isalnum() == selected_text[-1].isalnum(): for k1, k2, t0, t1 in parentheses: if key == k1: self.replaceSelectedText(t0 + selected_text + t1) self.word_changed.emit() return line, _ = self.getCursorPosition() doc_pre = self.text(line) super(TextEditor, self).keyPressEvent(event) doc_post = self.text(line) if doc_pre != doc_post: self.word_changed.emit() self.__spell_check_line() # Remove leading spaces when create newline. if key in {Qt.Key_Return, Qt.Key_Enter}: if len(doc_pre) - len(doc_pre.lstrip(" ")) == 0: line, _ = self.getCursorPosition() doc_post = self.text(line) while 0 < len(doc_post) - len(doc_post.lstrip(" ")): self.unindent(line) doc_post = self.text(line) return # Auto close of parentheses. if not (selected_text or self.__cursor_next_char().isalnum()): for k1, k2, t0, t1 in parentheses: if key == k1: self.insert(t1) return # Add space for commas. for co in commas: if key == co and self.__cursor_next_char() != " ": self.insert(" ") self.__cursor_move_next() return def __clear_indicator_all(self, indicator: int): """Clear all indicators.""" line, index = self.lineIndexFromPosition(self.length()) self.clearIndicatorRange(0, 0, line, index, indicator) def spell_check_all(self): """Spell check for all text.""" self.__clear_indicator_all(0) for start, end in _spell_check(self.text()): line1, index1 = self.lineIndexFromPosition(start) line2, index2 = self.lineIndexFromPosition(end) self.fillIndicatorRange(line1, index1, line2, index2, 0) def __clear_line_indicator(self, line: int, indicator: int): """Clear all indicators.""" self.clearIndicatorRange(line, 0, line, self.lineLength(line), indicator) def __spell_check_line(self): """Spell check for current line.""" line, index = self.getCursorPosition() self.__clear_line_indicator(line, 0) for start, end in _spell_check(self.text(line)): self.fillIndicatorRange(line, start, line, end, 0) def remove_trailing_blanks(self): """Remove trailing blanks in text editor.""" scroll_bar: QScrollBar = self.verticalScrollBar() pos = scroll_bar.sliderPosition() line, index = self.getCursorPosition() doc = "" for line_str in self.text().splitlines(): doc += line_str.rstrip() + '\n' self.selectAll() self.replaceSelectedText(doc) self.setCursorPosition(line, self.lineLength(line) - 1) scroll_bar.setSliderPosition(pos) def setText(self, doc: str): """Remove trailing blanks in text editor.""" super(TextEditor, self).setText(doc) if self.__no_trailing_blanks: self.remove_trailing_blanks() self.spell_check_all()
class NCEditor(QsciScintilla): """NC code editor.""" def __init__(self, parent): super(NCEditor, self).__init__(parent) # Set the default font. if system() == "Windows": font_name = "Courier New" else: font_name = "Mono" self.font = QFont(font_name) self.font.setFixedPitch(True) self.font.setPointSize(14) self.setFont(self.font) self.setMarginsFont(self.font) self.setUtf8(True) self.setEolMode(QsciScintilla.EolUnix) # Margin 0 is used for line numbers. font_metrics = QFontMetrics(self.font) self.setMarginsFont(self.font) self.setMarginWidth(0, font_metrics.width("0000") + 4) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#cccccc")) # Current line visible with special background color. self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) # Don't want to see the horizontal scrollbar at all. self.setWrapMode(QsciScintilla.WrapWord) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Keyword indicator [1] self.indicatorDefine(QsciScintilla.BoxIndicator, 1) self.cursorPositionChanged.connect(self.__catch_word) @pyqtSlot(int, int) def __catch_word(self, line: int, index: int): """Catch and indicate current word.""" self.__clear_indicator_all(1) pos = self.positionFromLineIndex(line, index) _, _, word = self.__word_at_pos(pos) word = r'\b' + word + r'\b' for m in re.finditer(word.encode('utf-8'), self.text().encode('utf-8'), re.IGNORECASE): self.fillIndicatorRange(*self.lineIndexFromPosition(m.start()), *self.lineIndexFromPosition(m.end()), 1) def __word_at_pos(self, pos: int) -> Tuple[int, int, str]: """Return pos of current word.""" return (self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos, True), self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos, True), self.wordAtLineIndex(*self.getCursorPosition())) def __clear_indicator_all(self, indicator: int): """Clear all indicators.""" self.clearIndicatorRange(0, 0, *self.lineIndexFromPosition(self.length()), indicator)
class TextEditor(QsciScintilla): """QScintilla text editor.""" currtWordChanged = pyqtSignal(str) def __init__(self, parent: QWidget): """UI settings.""" super(TextEditor, self).__init__(parent) #Set the default font. if platform.system().lower() == "windows": font_name = "Courier New" else: font_name = "Mono" self.font = QFont(font_name) self.font.setFixedPitch(True) self.font.setPointSize(14) self.setFont(self.font) self.setMarginsFont(self.font) self.setUtf8(True) #Margin 0 is used for line numbers. fontmetrics = QFontMetrics(self.font) self.setMarginsFont(self.font) self.setMarginWidth(0, fontmetrics.width("0000") + 4) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#cccccc")) #Brace matching. self.setBraceMatching(QsciScintilla.SloppyBraceMatch) #Current line visible with special background color. self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) #Set lexer. lexer = QsciLexerCustomPython() lexer.setDefaultFont(self.font) self.setLexer(lexer) self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, font_name.encode('utf-8')) #Don't want to see the horizontal scrollbar at all. self.setWrapMode(QsciScintilla.WrapWord) self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) #Auto completion. self.setAutoCompletionCaseSensitivity(True) self.setAutoCompletionSource(QsciScintilla.AcsDocument) self.setAutoCompletionThreshold(1) #Edge mode. self.setEdgeMode(QsciScintilla.EdgeLine) self.setEdgeColumn(80) self.setEdgeColor(Qt.blue) #Indentations. self.setAutoIndent(True) self.setIndentationsUseTabs(False) self.setTabWidth(4) self.setTabIndents(True) self.setBackspaceUnindents(True) self.setIndentationGuides(True) #Indicator. self.indicatorDefine(QsciScintilla.BoxIndicator, 0) self.SendScintilla(QsciScintilla.SCI_SETINDICATORCURRENT, 0) self.cursorPositionChanged.connect(self.__catchWords) #Widget size. self.setMinimumSize(400, 450) def __currentWordPosition(self) -> Tuple[int, int]: """Return pos of current word.""" pos = self.positionFromLineIndex(*self.getCursorPosition()) return ( self.SendScintilla(QsciScintilla.SCI_WORDSTARTPOSITION, pos, True), self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos, True), ) @pyqtSlot(int, int) def __catchWords(self, line: int, index: int): """Catch words that is same with current word.""" self.clearIndicatorRange(0, 0, *self.lineIndexFromPosition(self.length()), 0) wpos_start, wpos_end = self.__currentWordPosition() self.currtWordChanged.emit(self.text()[wpos_start:wpos_end]) self.fillIndicatorRange(*self.lineIndexFromPosition(wpos_start), *self.lineIndexFromPosition(wpos_end), 0) def wheelEvent(self, event): """Mouse wheel event.""" if QApplication.keyboardModifiers() != Qt.ControlModifier: super(TextEditor, self).wheelEvent(event) return if event.angleDelta().y() >= 0: self.zoomIn() else: self.zoomOut() def keyPressEvent(self, event): """Input key event.""" key = event.key() text = self.selectedText() #Commas and parentheses. parentheses = _parentheses commas = _commas #Wrap the selected text. if text: for match_key, t0, t1 in parentheses: if key == match_key: self.replaceSelectedText(t0 + text + t1) return super(TextEditor, self).keyPressEvent(event) #Auto close of parentheses. for match_key, t0, t1 in parentheses: if key == match_key: self.insert(t1) return #Add space for commas. for co in commas: if key == co: self.insert(" ") line, index = self.getCursorPosition() self.setCursorPosition(line, index + 1) return