def set_theme(self, theme=DayTheme): """ Connect the theme to a lexer and return the lexer for the editor to apply to the script text. """ self.lexer = PythonLexer() theme.apply_to(self.lexer) self.lexer.setDefaultPaper(theme.Paper) self.setCaretForegroundColor(theme.Caret) self.setMarginsBackgroundColor(theme.Margin) self.setMarginsForegroundColor(theme.Caret) self.setIndicatorForegroundColor(theme.IndicatorError, self.indicators['error']['id']) self.setIndicatorForegroundColor(theme.IndicatorStyle, self.indicators['style']['id']) self.setMarkerBackgroundColor(theme.IndicatorError, self.MARKER_NUMBER) api = QsciAPIs(self.lexer) for entry in self.api: api.add(entry) api.prepare() self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setLexer(self.lexer)
class EditorPane(QsciScintilla): """ Represents the text editor. """ # Signal fired when a script or hex is droped on this editor open_file = pyqtSignal(str) def __init__(self, path, text, newline=NEWLINE): super().__init__() self.setUtf8(True) self.path = path self.setText(text) self.newline = newline self.check_indicators = { # IDs are arbitrary 'error': {'id': 19, 'markers': {}}, 'style': {'id': 20, 'markers': {}} } self.BREAKPOINT_MARKER = 23 # Arbitrary self.search_indicators = { 'selection': {'id': 21, 'positions': []} } self.previous_selection = { 'line_start': 0, 'col_start': 0, 'line_end': 0, 'col_end': 0 } self.lexer = PythonLexer() self.api = None self.has_annotations = False self.setModified(False) self.breakpoint_lines = set() self.configure() def dropEvent(self, event): """ Run by Qt when *something* is dropped on this editor """ # Does the drag event have any urls? # Files are transfered as a url (by path not value) if event.mimeData().hasUrls(): # Qt doesn't seem to have an 'open' action, # this seems the most appropriate event.setDropAction(Qt.CopyAction) # Valid links links = [] # Iterate over each of the urls attached to the event for url in event.mimeData().urls(): # Check the url is to a local file # (not a webpage for example) if url.isLocalFile(): # Grab a 'real' path from the url path = url.toLocalFile() # Add it to the list of valid links links.append(path) # Did we get any? if len(links) > 0: # Only accept now we actually know we can do # something with the drop event event.accept() for link in links: # Start bubbling an open file request self.open_file.emit(link) # If the event wasn't handled let QsciScintilla have a go if not event.isAccepted(): super().dropEvent(event) def configure(self): """ Set up the editor component. """ # Font information font = Font().load() self.setFont(font) # Generic editor settings self.setUtf8(True) self.setAutoIndent(True) self.setIndentationsUseTabs(False) self.setIndentationWidth(4) self.setIndentationGuides(True) self.setBackspaceUnindents(True) self.setTabWidth(4) self.setEdgeColumn(79) self.setMarginLineNumbers(0, True) self.setMarginWidth(0, 50) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) self.set_theme() # Markers and indicators self.setMarginSensitivity(0, True) self.markerDefine(self.Circle, self.BREAKPOINT_MARKER) self.setMarginSensitivity(1, True) self.setIndicatorDrawUnder(True) for type_ in self.check_indicators: self.indicatorDefine( self.SquiggleIndicator, self.check_indicators[type_]['id']) for type_ in self.search_indicators: self.indicatorDefine( self.StraightBoxIndicator, self.search_indicators[type_]['id']) self.setAnnotationDisplay(self.AnnotationBoxed) self.selectionChanged.connect(self.selection_change_listener) def connect_margin(self, func): """ Connect clicking the margin to the passed in handler function. """ self.marginClicked.connect(func) def set_theme(self, theme=DayTheme): """ Connect the theme to a lexer and return the lexer for the editor to apply to the script text. """ theme.apply_to(self.lexer) self.lexer.setDefaultPaper(theme.Paper) self.setCaretForegroundColor(theme.Caret) self.setMarginsBackgroundColor(theme.Margin) self.setMarginsForegroundColor(theme.Caret) self.setIndicatorForegroundColor(theme.IndicatorError, self.check_indicators['error']['id']) self.setIndicatorForegroundColor(theme.IndicatorStyle, self.check_indicators['style']['id']) for type_ in self.search_indicators: self.setIndicatorForegroundColor( theme.IndicatorWordMatch, self.search_indicators[type_]['id']) self.setMarkerBackgroundColor(theme.BreakpointMarker, self.BREAKPOINT_MARKER) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setLexer(self.lexer) self.setMatchedBraceBackgroundColor(theme.BraceBackground) self.setMatchedBraceForegroundColor(theme.BraceForeground) self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground) self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground) def set_api(self, api_definitions): """ Sets the API entries for tooltips, calltips and the like. """ self.api = QsciAPIs(self.lexer) for entry in api_definitions: self.api.add(entry) self.api.prepare() @property def label(self): """ The label associated with this editor widget (usually the filename of the script we're editing). If the script has been modified since it was last saved, the label will end with an asterisk. """ if self.path: label = os.path.basename(self.path) else: label = 'untitled' # Add an asterisk to indicate that the file remains unsaved. if self.isModified(): return label + ' *' else: return label def reset_annotations(self): """ Clears all the assets (indicators, annotations and markers). """ self.clearAnnotations() self.markerDeleteAll() self.reset_search_indicators() self.reset_check_indicators() def reset_check_indicators(self): """ Clears all the text indicators related to the check code functionality. """ for indicator in self.check_indicators: for _, markers in \ self.check_indicators[indicator]['markers'].items(): line_no = markers[0]['line_no'] # All markers on same line. self.clearIndicatorRange( line_no, 0, line_no, 999999, self.check_indicators[indicator]['id']) self.check_indicators[indicator]['markers'] = {} def reset_search_indicators(self): """ Clears all the text indicators from the search functionality. """ for indicator in self.search_indicators: for position in self.search_indicators[indicator]['positions']: self.clearIndicatorRange( position['line_start'], position['col_start'], position['line_end'], position['col_end'], self.search_indicators[indicator]['id']) self.search_indicators[indicator]['positions'] = [] def annotate_code(self, feedback, annotation_type='error'): """ Given a list of annotations add them to the editor pane so the user can act upon them. """ indicator = self.check_indicators[annotation_type] for line_no, messages in feedback.items(): indicator['markers'][line_no] = messages for message in messages: col = message.get('column', 0) if col: col_start = col - 1 col_end = col + 1 self.fillIndicatorRange(line_no, col_start, line_no, col_end, indicator['id']) def show_annotations(self): """ Display all the messages to be annotated to the code. """ lines = defaultdict(list) for indicator in self.check_indicators: markers = self.check_indicators[indicator]['markers'] for k, marker_list in markers.items(): for m in marker_list: lines[m['line_no']].append('\u2191' + m['message'].capitalize()) for line, messages in lines.items(): text = '\n'.join(messages).strip() if text: self.annotate(line, text, self.annotationDisplay()) def find_next_match(self, text, from_line=-1, from_col=-1, case_sensitive=True, wrap_around=True): """ Finds the next text match from the current cursor, or the given position, and selects it (the automatic selection is the only available QsciScintilla behaviour). Returns True if match found, False otherwise. """ return self.findFirst( text, # Text to find, False, # Treat as regular expression case_sensitive, # Case sensitive search True, # Whole word matches only wrap_around, # Wrap search forward=True, # Forward search line=from_line, # -1 starts at current position index=from_col, # -1 starts at current position show=False, # Unfolds found text posix=False) # More POSIX compatible RegEx def range_from_positions(self, start_position, end_position): """Given a start-end pair, such as are provided by a regex match, return the corresponding Scintilla line-offset pairs which are used for searches, indicators etc. FIXME: Not clear whether the Scintilla conversions are expecting bytes or characters (ie codepoints) """ start_line, start_offset = self.lineIndexFromPosition(start_position) end_line, end_offset = self.lineIndexFromPosition(end_position) return start_line, start_offset, end_line, end_offset def highlight_selected_matches(self): """ Checks the current selection, if it is a single word it then searches and highlights all matches. Since we're interested in exactly one word: * Ignore an empty selection * Ignore anything which spans more than one line * Ignore more than one word * Ignore anything less than one word """ selected_range = line0, col0, line1, col1 = self.getSelection() # # If there's no selection, do nothing # if selected_range == (-1, -1, -1, -1): return # # Ignore anything which spans two or more lines # if line0 != line1: return # # Ignore if no text is selected or the selected text is not at most one # valid identifier-type word. # selected_text = self.selectedText() if not RE_VALID_WORD.match(selected_text): return # # Ignore anything which is not a whole word. # NB Although Scintilla defines a SCI_ISRANGEWORD message, # it's not exposed by QSciScintilla. Instead, we # ask Scintilla for the start end end position of # the word we're in and test whether our range end points match # those or not. # pos0 = self.positionFromLineIndex(line0, col0) word_start_pos = self.SendScintilla( QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1) _, start_offset = self.lineIndexFromPosition(word_start_pos) if col0 != start_offset: return pos1 = self.positionFromLineIndex(line1, col1) word_end_pos = self.SendScintilla( QsciScintilla.SCI_WORDENDPOSITION, pos1, 1) _, end_offset = self.lineIndexFromPosition(word_end_pos) if col1 != end_offset: return # # For each matching word within the editor text, add it to # the list of highlighted indicators and fill it according # to the current theme. # indicators = self.search_indicators['selection'] text = self.text() for match in re.finditer(selected_text, text): range = self.range_from_positions(*match.span()) # # Don't highlight the text we've selected # if range == selected_range: continue line_start, col_start, line_end, col_end = range indicators['positions'].append({ 'line_start': line_start, 'col_start': col_start, 'line_end': line_end, 'col_end': col_end }) self.fillIndicatorRange(line_start, col_start, line_end, col_end, indicators['id']) def selection_change_listener(self): """ Runs every time the text selection changes. This could get triggered multiple times while the mouse click is down, even if selection has not changed in itself. If there is a new selection is passes control to highlight_selected_matches. """ # Get the current selection, exit if it has not changed line_from, index_from, line_to, index_to = self.getSelection() if self.previous_selection['col_end'] != index_to or \ self.previous_selection['col_start'] != index_from or \ self.previous_selection['line_start'] != line_from or \ self.previous_selection['line_end'] != line_to: self.previous_selection['line_start'] = line_from self.previous_selection['col_start'] = index_from self.previous_selection['line_end'] = line_to self.previous_selection['col_end'] = index_to # Highlight matches self.reset_search_indicators() self.highlight_selected_matches()
class ControlCodeEditor(ControlBase): """ Control that offers a code editor with pretty-print and line numbers and a save button """ ARROW_MARKER_NUM = 8 def __init__(self, *args, **kwargs): """ :param label: :param default: :param helptext: """ self._read_only = kwargs.get('readonly', False) self._changed_func = None super(ControlCodeEditor, self).__init__(*args, **kwargs) self.discard_event = kwargs.get('discard_event', self.discard_event) def init_form(self): """ """ control_path = tools.getFileInSameDirectory(__file__, "code_editor.ui") self._form = uic.loadUi(control_path) self._code_editor = self._form.code_editor self._save_button = self._form.save_button self._discard_button = self._form.discard_button self._save_button.clicked[bool].connect(self.on_save_changes) self._discard_button.clicked[bool].connect(self.on_discard_changes) if self._read_only: self._code_editor.setReadOnly(True) self._save_button.setVisible(False) self._discard_button.setVisible(False) self.form.font_size.addItem('9') self.form.font_size.addItem('10') self.form.font_size.addItem('11') self.form.font_size.addItem('12') self.form.font_size.addItem('14') self.form.font_size.addItem('18') self.form.font_size.addItem('24') # Set the default font size index = self.form.font_size.findText( conf.PYFORMS_CONTROL_CODE_EDITOR_DEFAULT_FONT_SIZE) self.form.font_size.setCurrentIndex(index) self.form.font_size.currentIndexChanged.connect( self.__font_size_index_changed) self.form.save_button.setIcon(QIcon(conf.PYFORMS_ICON_CODEEDITOR_SAVE)) self.form.discard_button.setIcon( QIcon(conf.PYFORMS_ICON_CODEEDITOR_DISCARD)) self.lexer = QsciLexerPython self._code_editor.keyPressEvent = self._key_pressed self._changed_func = None self.value = self._value super(ControlCodeEditor, self).init_form() def __font_size_index_changed(self, index): item = self.form.font_size.currentText() if len(item) >= 1: self._load_code_editor_settings() def _load_code_editor_settings(self): """ Load settings on the code editor like, font style, margins, scroll, etc. Based on the example from http://eli.thegreenplace.net/2011/04/01/sample-using-qscintilla-with-pyqt/ """ item = self.form.font_size.currentText() size = int(item) # Set the default font font = QFont() font.setFamily('Courier New') font.setFixedPitch(True) font.setPointSize(size) self._code_editor.setFont(font) self._code_editor.setMarginsFont(font) # Margin 0 is used for line numbers fontmetrics = QFontMetrics(font) self._code_editor.setMarginsFont(font) self._code_editor.setMarginWidth(0, fontmetrics.width("00000") + 6) self._code_editor.setMarginLineNumbers(0, True) self._code_editor.setMarginsBackgroundColor(QColor("#cccccc")) # Clickable margin 1 for showing markers self._code_editor.setMarginSensitivity(1, True) self._code_editor.marginClicked.connect(self.on_margin_clicked) self._code_editor.markerDefine(QsciScintilla.RightArrow, self.ARROW_MARKER_NUM) self._code_editor.setMarkerBackgroundColor(QColor("#ee1111"), self.ARROW_MARKER_NUM) # Detect changes to text self._code_editor.modificationChanged.connect( self.on_modification_changed) # Brace matching: enable for a brace immediately before or after the current position self._code_editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Current line visible with special background color self._code_editor.setCaretLineVisible(True) self._code_editor.setCaretLineBackgroundColor(QColor("#ffe4e4")) # Set Python lexer # Set style for Python comments (style number 1) to a fixed-width Courier. lexer = self.lexer() lexer.setDefaultFont(font) self._code_editor.setLexer(lexer) self._code_editor.setIndentationWidth(4) # self._code_editor.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, 'Courier') self._code_editor.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1) # Don't want to see the horizontal scrollbar at all # Use raw message to Scintilla here (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html) self._code_editor.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) self._lexer_obj = lexer self.qsci_api = QsciAPIs(self._lexer_obj) ## Add autocompletion strings self.qsci_api.add("aLongString") self.qsci_api.add("aLongerString") self.qsci_api.add("aDifferentString") self.qsci_api.add("sOmethingElse") ## Compile the api for use in the lexer self.qsci_api.prepare() self._code_editor.setAutoCompletionThreshold(1) self._code_editor.setAutoCompletionSource(QsciScintilla.AcsAll) # not too small # self._code_editor.setMinimumSize(600, 450) ################################################################### ############ Events ############################################### ################################################################### def on_margin_clicked(self, nmargin, nline, modifiers): # pylint: disable=unused-argument """ On margin clicked, toggle marker for the line the margin was clicked on :param nmargin: :type nmargin: :param nline: :type nline: :param modifiers: :type modifiers: """ if self._code_editor.markersAtLine(nline) != 0: self._code_editor.markerDelete(nline, self.ARROW_MARKER_NUM) else: self._code_editor.markerAdd(nline, self.ARROW_MARKER_NUM) def on_modification_changed(self): """ On modification change, re-enable save button """ if self._changed_func: self._save_button.setEnabled(True) self._discard_button.setEnabled(True) def on_save_changes(self): """ On button save clicked, save changes made on the code editor to file """ if self.changed_event(): self._code_editor.setModified(False) self._save_button.setEnabled(False) self._discard_button.setEnabled(False) def on_discard_changes(self): if self.discard_event(): self._code_editor.setModified(False) self._save_button.setEnabled(False) self._discard_button.setEnabled(False) def discard_event(self): return True def _key_pressed(self, event): """ Handle KeyPressed event We only care about CTRL-S in order to save changes :param event: key event """ QsciScintilla.keyPressEvent(self._code_editor, event) if event.key() in [QtCore.Qt.Key_S, QtCore.Qt.Key_Save]: modifiers = QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier and self.is_modified: logger.debug("Saving...") self.on_save_changes() self.key_pressed_event(event) def key_pressed_event(self, event): """ Override KeyPressed event as you like :param event: key event """ pass @property def is_modified(self): return self._code_editor.isModified() ################################################################### ############ Properties ########################################### ################################################################### @property def lexer(self): return self._lexer @lexer.setter def lexer(self, value): self._lexer = value self._load_code_editor_settings() @property def value(self): return self._code_editor.text() @value.setter def value(self, value): if value is not None: self._code_editor.setText(str(value)) self._code_editor.setModified(False) self._save_button.setEnabled(False) self._discard_button.setEnabled(False) @property def changed_event(self): return self._changed_func if self._changed_func else (lambda: 0) @changed_event.setter def changed_event(self, value): self._changed_func = value
class IDEeditor(QsciScintilla): r""" 文本框类 """ ARROW_MARKER_NUM = 8 newFileSignal = pyqtSignal() def __init__(self, name, parent=None, parent_tabWidget=None, language='txt', font_content=None): super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setObjectName(name) # self.document().setModified(False) # self.setWindowModified(False) self.setUtf8(True) self.setModified(False) self.setText('') self.filepath = None self.language = language self.parent_tabw = parent_tabWidget self.font_content = font_content if font_content else { 'font': 'Andale Mono', 'size': '12' } self.lxr = None self.api = None self.setFontSize(font_content) #self.SendScintilla() #self.replaceSelectedText() # IDE settings # Brace matching: enable for a brace immediately before or after # the current position # self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Current line visible with special background color self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) # 自动缩进 self.setAutoIndent(True) self.setTabWidth(4) self.setAutoCompletionThreshold(1) self.setAutoCompletionSource(self.AcsAll) # self.cursorPositionChanged.connect(self.testEvent) # Hotspot # 5:关键词 # 8:类名 # 9:函数名 # 10:标点符号 # 11:变量名 self.SendScintilla(self.SCI_STYLESETHOTSPOT, 8, True) self.SendScintilla(self.SCI_STYLESETHOTSPOT, 9, True) self.SendScintilla(self.SCI_STYLESETHOTSPOT, 11, True) self.SCN_HOTSPOTCLICK.connect(self.hotspot_clicked) def hotspot_clicked(self, position, modifiers): r""" 点击hotspot触发事件 :return: """ print(position) QTimer.singleShot( 100, functools.partial(self.hotspot_clicked_delayed, position, modifiers)) def hotspot_clicked_delayed(self, position, modifiers): start = self.SendScintilla(self.SCI_WORDSTARTPOSITION, position) end = self.SendScintilla(self.SCI_WORDENDPOSITION, position) text = self.text(start, end) # [start:end] click_line = self.SendScintilla(self.SCI_LINEFROMPOSITION, position) + 1 click_column = self.SendScintilla(self.SCI_GETCOLUMN, position) + 1 new_row, new_col = goto_definition(text, click_line, click_column, contents=self.text()) if new_row is not None and new_col is not None: self.setCursorPosition(new_row - 1, new_col - 1) print(text) def keyPressEvent(self, e): r""" 监测文件内容是否修改,若修改则在tab中文件名末尾 添加一个 '*' :param e: :return: """ super().keyPressEvent(e) index = self.parent_tabw.currentIndex() tabtext = self.parent_tabw.tabText(index) if not tabtext.startswith('*') and self.isModified(): self.parent_tabw.setTabText(index, '*' + tabtext) if not self.isModified() and tabtext.startswith('*'): self.parent_tabw.setTabText(index, tabtext[1:]) def setlanguage(self, language): r""" 改变语言 :param language: :return: """ self.set_lexer(language) self.language = language def set_lexer(self, language): r""" 多语法代码高亮 :param language: :return: """ font = self.font_content['font'] size = int(self.font_content['size']) lexer_font = QFont(font, size) if language == 'py': self.lxr = QsciLexerPython() self.lxr.setFont(lexer_font) self.setLexer(self.lxr) self.__pythonCompletion() elif language == 'c': self.lxr = QsciLexerCPP() self.lxr.setFont(lexer_font) self.setLexer(self.lxr) self.__cCompletion() elif language == 'md': self.lxr = QsciLexerMarkdown() self.lxr.setFont(lexer_font) self.setLexer(self.lxr) else: self.setLexer(None) self.setText(self.text()) def __pythonCompletion(self): r""" python 自动补全 :return: """ python_keywords = [ "False", "None", "True", "and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "isinstance", "print", "len", "range", "enumerate", "input", "int", "float", "bool", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "next", "iter" ] try: if isinstance(self.api, QsciAPIs): del self.api except: pass self.api = QsciAPIs(self.lxr) for kw in python_keywords: self.api.add(kw) self.api.prepare() # self.api.add('class') # import PyQt5 # pyqt_path = os.path.dirname(PyQt5.__file__) # self.api.load(os.path.join(pyqt_path, "Qt/qsci/api/python/Python-3.6.api")) # self.api.prepare() # print('OK') def __cCompletion(self): r""" C自动补全 :return: """ c_keywords = [ "char", "double", "enum", "float", "int", "long", "short", "signed", "struct", "union", "unsigned", "void", "for", "do", "while", "break", "continue", "if", "else", "goto", "switch", "case", "default", "return", "auto", "extern", "register", "static", "const", "sizeof", "typedef", "volatile" ] try: if isinstance(self.api, QsciAPIs): del self.api except: pass self.api = QsciAPIs(self.lxr) for kw in c_keywords: self.api.add(kw) self.api.prepare() def setFontSize(self, font_content): r""" 修改字体大小和样式 :param fontSize: :return: """ # self.setStyleSheet(f"font: {fontSize}pt'.AppleSystemUIFont';") self.font_content = font_content font = font_content['font'] size = int(font_content['size']) qfont = QFont(font, size) self.setFont(qfont) self.set_lexer(self.language) self.codeRow() def codeRow(self): r""" 显示代码行数 :return: """ font_categoty = self.font_content['font'] size = int(self.font_content['size']) font = QFont(font_categoty, size) # Margin 0 is used for line numbers fontmetrics = QFontMetrics(font) self.setMarginsFont(font) self.setMarginWidth(0, fontmetrics.width("00000") + 6) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#cccccc")) # Clickable margin 1 for showing markers self.setMarginSensitivity(1, True) self.marginClicked.connect(self.on_margin_clicked) self.markerDefine(QsciScintilla.RightArrow, self.ARROW_MARKER_NUM) self.setMarkerBackgroundColor(QColor("#ee1111"), self.ARROW_MARKER_NUM) # 取消显示横向bar # self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) def on_margin_clicked(self, nmargin, nline, modifiers): # Toggle marker for the line the margin was clicked on if self.markersAtLine(nline) != 0: self.markerDelete(nline, self.ARROW_MARKER_NUM) else: self.markerAdd(nline, self.ARROW_MARKER_NUM) def load(self, file_path, mapping=None): r""" 读取文件 :param file_path: 文件路径 :return: None """ text = '' try: """读文件""" with open(file_path, 'r', encoding='utf-8') as f: for line in f.readlines(): text += line if mapping is not None or file_path.startswith('.tmp'): self.filepath = mapping else: self.filepath = file_path # self.setPlainText(text) self.setText(text) # 设置当前文件名 _, tmpfilename = os.path.split(file_path) self.setObjectName(tmpfilename) # 设置语言 _, prefix = os.path.splitext(tmpfilename) self.setlanguage(prefix[1:]) # 是否清除改变 if tmpfilename.startswith('*'): self.setModified(True) else: self.setModified(False) except FileNotFoundError: """弹出窗口,提示文件不存在""" QMessageBox.warning(self, 'Warning', 'Text does not exist!') def save(self, file_path=None): r""" 保存 :return: """ if self.filepath is not None or file_path: if file_path: save_path = file_path else: save_path = self.filepath with open(save_path, 'w', encoding='utf-8') as f: text = self.text() f.writelines(text) # self.document().setModified(False) # 把 '*' 去掉 index = self.parent_tabw.currentIndex() tabtext = self.parent_tabw.tabText(index) if tabtext.startswith('*'): self.parent_tabw.setTabText(index, tabtext[1:]) self.setModified(False) return False else: self.saveas() return True def saveas(self): r""" 另存为 :return: """ file_path, _ = QFileDialog.getSaveFileName(self, 'Save As') if len(file_path): """如果路径不为空,则保存""" self.filepath = file_path with open(file_path, 'w', encoding='utf-8') as f: text = self.text() f.writelines(text) # self.document().setModified(False) # 路径 self.filepath = file_path # 设置当前文件名 _, tmpfilename = os.path.split(file_path) self.setObjectName(tmpfilename) # 设置语言 _, prefix = os.path.splitext(tmpfilename) self.setlanguage(prefix[1:]) self.setModified(False) index = self.parent_tabw.currentIndex() self.parent_tabw.setTabText(index, tmpfilename) self.newFileSignal.emit() return True else: # QMessageBox.warning(self, 'Warning', 'File name should not be empty') return False def closeText(self): self.close()
def setQssAutocomplete(self): api = QsciAPIs(self) widgets = ("QAbstractScrollArea", "QCheckBox", "QColumnView", "QComboBox", "QDateEdit", "QDateTimeEdit", "QDialog", "QDialogButtonBox", "QDockWidget", "QDoubleSpinBox", "QFrame", "QGroupBox", "QHeaderView", "QLabel", "QLineEdit", "QListView", "QListWidget", "QMainWindow", "QMenu", "QMenuBar", "QMessageBox", "QProgressBar", "QPushButton", "QRadioButton", "QScrollBar", "QSizeGrip", "QSlider", "QSpinBox", "QSplitter", "QStatusBar", "QTabBar", "QTabWidget", "QTableView", "QTableWidget", "QTextEdit", "QTimeEdit", "QToolBar", "QToolButton", "QToolBox", "QToolTip", "QTreeView", "QTreeWidget", "QWidget") properties = ( "alternate-background-color", "background", "background-color", "background-image", "background-repeat", "background-position", "background-attachment", "background-clip", "background-origin", "border", "border-top", "border-right", "border-bottom", "border-left", "border-color", "border-top-color", "border-right-color", "border-bottom-color", "border-left-color", "border-image", "border-radius", "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius", "border-style", "border-top-style", "border-right-style", "border-bottom-style", "border-left-style", "border-width", "border-top-width", "border-right-width", "border-bottom-width", "border-left-width", "bottom", "button-layout", "color", "dialogbuttonbox-buttons-have-icons", "font", "font-family", "font-size", "font-style", "font-weight", "gridline-color", "height", "icon-size", "image", "image-position", "left", "lineedit-password-character", "lineedit-password-mask-delay", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "max-height", "max-width", "messagebox-text-interaction-flags", "min-height", "min-width", "opacity*", "outline", "outline-color", "outline-offset", "outline-style", "outline-radius", "outline-bottom-left-radius", "outline-bottom-right-radius", "outline-top-left-radius", "outline-top-right-radius", "padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "paint-alternating-row-colors-for-empty-area", "position", "right", "selection-background-color", "selection-color", "show-decoration-selected", "spacing", "subcontrol-origin", "subcontrol-position", "titlebar-show-tooltips-on-buttons", "widget-animation-duration", "text-align", "text-decoration", "top", "width") subcontrols0 = ("::add-line", "::add-page", "::branch", "::chunk", "::close-button", "::corner", "::down-arrow", "::down-button", "::drop-down", "::float-button", "::groove", "::indicator", "::handle", "::icon", "::item", "::left-arrow", "::left-corner", "::menu-arrow", "::menu-button", "::menu-indicator", "::right-arrow", "::pane", "::right-corner", "::scroller", "::section", "::separator", "::sub-line", "::sub-page", "::tab", "::tab-bar", "::tear", "::tearoff", "::text", "::title", "::up-arrow", "::up-button") pseudostates0 = (":active", ":adjoins-item", ":alternate", ":bottom", ":checked", ":closable", ":closed", ":default", ":disabled", ":editable", ":edit-focus", ":enabled", ":exclusive", ":first", ":flat", ":floatable", ":focus", ":has-children", ":has-siblings", ":horizontal", ":hover", ":indeterminate", ":last", ":left", ":maximized", ":middle", ":minimized", ":movable", ":no-frame", ":non-exclusive", ":off", ":on", ":only-one", ":open", ":next-selected", ":pressed", ":previous-selected", ":read-only", ":right", ":selected", ":top", ":unchecked", ":vertical", ":window") subcontrols = ("add-line", "add-page", "branch", "chunk", "close-button", "corner", "down-arrow", "down-button", "drop-down", "float-button", "groove", "indicator", "handle", "icon", "item", "left-arrow", "left-corner", "menu-arrow", "menu-button", "menu-indicator", "right-arrow", "pane", "right-corner", "scroller", "section", "separator", "sub-line", "sub-page", "tab", "tab-bar", "tear", "tearoff", "text", "title", "up-arrow", "up-button") pseudostates = ("active", "adjoins-item", "alternate", "bottom", "checked", "closable", "closed", "default", "disabled", "editable", "edit-focus", "enabled", "exclusive", "first", "flat", "floatable", "focus", "has-children", "has-siblings", "horizontal", "hover", "indeterminate", "last", "left", "maximized", "middle", "minimized", "movable", "no-frame", "non-exclusive", "off", "on", "only-one", "open", "next-selected", "pressed", "previous-selected", "read-only", "right", "selected", "top", "unchecked", "vertical", "window") kwset = (widgets, properties, subcontrols, pseudostates) for ks in kwset: for k in ks: api.add(k) api.prepare()
class PMBaseEditor(QWidget, Ui_FormEditor): def __init__(self, parent=None, comment_string: str = '//'): super(PMBaseEditor, self).__init__(parent=parent) self._parent = self.parent() self.setupUi(self) self._lexer = None self._apis = None self._path = '' self._extension_names: typing.List[str] = [] self._encoding = 'utf-8' self._action_format = None # 格式化 self._action_run_sel_code = None # 运行选中代码 self._action_run_code = None # 运行代码 self._shortcut_format = None self._shortcut_run = None self._shortcut_run_sel = None self._shortcut_goto = None self._indicator_error = -1 self._indicator_error2 = -1 self._indicator_warn = -1 self._indicator_info = -1 self._indicator_dict = {} # 指示器记录 self._smart_autocomp_on = True # 自定义属性用于控制QSS设置 self._theme = 'tomorrow' # 代码检测后详情提示颜色 self.fc_red = QColor(255, 23, 23) self.bc_red = QColor(255, 240, 240) self.fc_yellow = QColor(191, 153, 36) self.bc_yellow = QColor(255, 255, 240) self.fc_black = QColor(0, 0, 0) self.bc_black = QColor(239, 239, 239) self.fc_purple = QColor(197, 67, 153) self.bc_purple = QColor(255, 240, 255) self.extension_lib = None self.find_dialog: 'FindDialog' = None self.commenter = Commenter(self.textEdit, comment_string=comment_string) def update_settings(self, settings: typing.Dict[str, object]): wrap = settings['wrap'] if wrap: self.textEdit.setWrapMode(QsciScintilla.WrapWord) else: self.textEdit.setWrapMode(QsciScintilla.WrapNone) self.set_smart_autocomp_stat(settings['smart_autocomp_on']) def set_smart_autocomp_stat(self, autocomp_on: bool) -> None: self._smart_autocomp_on = autocomp_on # self.textEdit.setAutoCompletionThreshold(0) def search_word(self, text_to_find: str, wrap: bool, regex: bool, case_sensitive: bool, whole_word: bool, forward: bool, index=-1, line=-1, **kwargs): return self.textEdit.findFirst(text_to_find, regex, case_sensitive, whole_word, wrap, forward, line, index) def slot_cursor_position_changed(self, line: int, column: int) -> None: """ 光标变化槽函数 :param line: 行 :param column: 列 :type line: int :type column: int :return: None """ self.label_status_ln_col.setText( self.tr('Ln:{0} Col:{1}').format(format(line + 1, ','), format(column + 1, ','))) def on_textedit_focusin(self, e): QsciScintilla.focusInEvent(self.textEdit, e) self.extension_lib.UI.switch_toolbar('code_editor_toolbar', switch_only=True) def _init_apis(self) -> None: """ 加载自定义智能提示文件 :return: None """ self._apis = QsciAPIs(self._lexer) # for path in Path(os.path.join(os.path.dirname(__file__), 'api')).rglob('*.api'): # logger.info('load %s' % str(path.absolute())) # self._apis.load(str(path.absolute())) try: # 添加额外关键词 for word in self._parent.keywords(): self._apis.add(word) except Exception as e: logger.warning(str(e)) self._apis.prepare() def _init_editor(self) -> None: """ 初始化编辑器设置 :return: None """ self.label_status_ln_col.setText(self.tr('Ln:1 Col:1')) self.label_status_length.setText(self.tr('Length:0 Lines:1')) self.label_status_sel.setText(self.tr('Sel:0 | 0')) self.textEdit.setContextMenuPolicy(Qt.CustomContextMenu) # 设置字体 self.textEdit.setFont(QFont('Source Code Pro', 12)) # Consolas self.textEdit.setMarginsFont(self.textEdit.font()) # 自动换行 self.textEdit.setEolMode(QsciScintilla.EolUnix) # \n换行 self.textEdit.setWrapMode(QsciScintilla.WrapWord) # 自动换行 self.textEdit.setWrapVisualFlags(QsciScintilla.WrapFlagNone) self.textEdit.setWrapIndentMode(QsciScintilla.WrapIndentFixed) # 编码 self.textEdit.setUtf8(True) self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, QsciScintilla.SC_CP_UTF8) # 自动提示 self.textEdit.setAnnotationDisplay(QsciScintilla.AnnotationBoxed) # 提示显示方式 self.textEdit.setAutoCompletionSource(QsciScintilla.AcsAll) # 自动补全。对于所有Ascii字符 self.textEdit.setAutoCompletionReplaceWord(True) self.textEdit.setAutoCompletionCaseSensitivity(False) # 忽略大小写 # self.textEdit.setAutoCompletionFillupsEnabled(True) self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusNever) # self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusAlways) # self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusExplicit) self.textEdit.setAutoCompletionThreshold(1) # 输入多少个字符才弹出补全提示 # QsciScintilla.setAutoCompletionUseSingle() self.textEdit.setCallTipsPosition(QsciScintilla.CallTipsBelowText) # 设置提示位置 self.textEdit.setCallTipsStyle(QsciScintilla.CallTipsNoContext) # 设置提示样式 # 设置折叠样式 self.textEdit.setFolding(QsciScintilla.FoldStyle.BoxedTreeFoldStyle) # 代码折叠 # self.textEdit.setFoldMarginColors(QColor(233, 233, 233), Qt.white) # 折叠标签颜色 # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERSUB, # QColor('0xa0a0a0')) # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERMIDTAIL, # QColor('0xa0a0a0')) # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERTAIL, # QColor('0xa0a0a0')) # 设置当前行背景 self.textEdit.setCaretLineVisible(True) # self.textEdit.setCaretLineBackgroundColor(QColor(232, 232, 255)) # 设置选中文本颜色 # self.textEdit.setSelectionForegroundColor(QColor(192, 192, 192)) # self.textEdit.setSelectionBackgroundColor(QColor(192, 192, 192)) # 括号匹配 self.textEdit.setBraceMatching(QsciScintilla.StrictBraceMatch) # 大括号严格匹配 # self.textEdit.setMatchedBraceBackgroundColor(Qt.blue) # self.textEdit.setMatchedBraceForegroundColor(Qt.white) # self.textEdit.setUnmatchedBraceBackgroundColor(Qt.red) # self.textEdit.setUnmatchedBraceForegroundColor(Qt.white) # 启用活动热点区域的下划线 self.textEdit.setHotspotUnderline(True) self.textEdit.setHotspotWrap(True) # 缩进 self.textEdit.setAutoIndent(True) # 换行后自动缩进 self.textEdit.setTabWidth(4) self.textEdit.setIndentationWidth(4) self.textEdit.setTabIndents(True) # 缩进指南 self.textEdit.setIndentationGuides(True) self.textEdit.setIndentationsUseTabs(False) # 不使用Tab self.textEdit.setBackspaceUnindents(True) # 当一行没有其它字符时删除前面的缩进 # self.textEdit.setIndentationGuidesForegroundColor(QColor(192, 192, 192)) # self.textEdit.setIndentationGuidesBackgroundColor(Qt.white) # 显示行号 self.textEdit.setMarginLineNumbers(0, True) self.textEdit.setMarginWidth(0, 50) self.textEdit.setMarginWidth(1, 0) # 行号 # self.textEdit.setMarginWidth(2, 0) # 折叠 self.textEdit.setMarginWidth(3, 0) self.textEdit.setMarginWidth(4, 0) # # 折叠区域 # self.textEdit.setMarginType(3, QsciScintilla.SymbolMargin) # self.textEdit.setMarginLineNumbers(3, False) # self.textEdit.setMarginWidth(3, 15) # self.textEdit.setMarginSensitivity(3, True) # 设置空白字符显示 self.textEdit.setWhitespaceSize(1) # 可见的空白点的尺寸 self.textEdit.setWhitespaceVisibility(QsciScintilla.WsVisible) # 空白的可见性。默认的是空格是无形的 # self.textEdit.setWhitespaceForegroundColor(QColor(255, 181, 106)) # 设置右边边界线 self.textEdit.setEdgeColumn(120) self.textEdit.setEdgeMode(QsciScintilla.EdgeLine) # 设置代码检测后波浪线 self._indicator_error = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator) self._indicator_error2 = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator) self._indicator_warn = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator) self._indicator_info = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator) self.textEdit.setIndicatorForegroundColor(QColor(Qt.red), self._indicator_error) self.textEdit.setIndicatorForegroundColor(QColor(Qt.red), self._indicator_error2) self.textEdit.setIndicatorForegroundColor(QColor(244, 152, 16), self._indicator_warn) self.textEdit.setIndicatorForegroundColor(QColor(Qt.green), self._indicator_info) # 鼠标跟踪 # self.textEdit.viewport().setMouseTracking(True) # # 安装键盘过滤器 # self.textEdit.installEventFilter(self) # 安装鼠标移动过滤器 self.textEdit.viewport().installEventFilter(self) def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool: if event.type() == QEvent.ToolTip: # 如果有错误则显示详情 line = self.textEdit.lineAt(event.pos()) if line >= 0 and line in self._indicator_dict: text = self._indicator_dict.get(line, '') if text: color = self.textEdit.lexer().paper(0) QToolTip.showText(QCursor.pos(), '<html><head/><body><div style="background:{0};">{1}</div></body></html>'.format( color.name(), text), self) return False def indent(self): sel = self.textEdit.getSelection() if sel[0] == sel[3]: row = self.textEdit.getCursorPosition()[0] self.textEdit.indent(row) else: ke = QKeyEvent(QEvent.KeyPress, Qt.Key_Tab, Qt.NoModifier) self.textEdit.keyPressEvent(ke) def unindent(self): """ 取消缩进。 方式就是注入一个tab快捷键。 :return: """ sel = self.textEdit.getSelection() if sel[0] == sel[3]: row = self.textEdit.getCursorPosition()[0] self.textEdit.unindent(row) else: ke = QKeyEvent(QEvent.KeyPress, Qt.Key_Backtab, Qt.NoModifier) self.textEdit.keyPressEvent(ke) def _init_lexer(self, lexer: 'QsciLexer') -> None: """ 初始化语法解析器 :return: None """ self._lexer = lexer self._lexer.setFont(self.textEdit.font()) self.textEdit.setLexer(self._lexer) def _init_signals(self) -> None: """ 初始化信号绑定 :return: None """ # 绑定获得焦点信号 self.textEdit.focusInEvent = self.on_textedit_focusin # 绑定光标变化信号 self.textEdit.cursorPositionChanged.connect(self.slot_cursor_position_changed) # 绑定内容改变信号 self.textEdit.textChanged.connect(self.slot_text_changed) # 绑定选中变化信号 self.textEdit.selectionChanged.connect(self.slot_selection_changed) # 绑定是否被修改信号 self.textEdit.modificationChanged.connect(self.slot_modification_changed) # 绑定右键菜单信号 self.textEdit.customContextMenuRequested.connect(self.slot_custom_context_menu_requested) # 绑定快捷键信号 self._action_format.triggered.connect(self.slot_code_format) self._shortcut_format.activated.connect(self.slot_code_format) self._action_run_code.triggered.connect(self.slot_code_run) self._shortcut_run.activated.connect(self.slot_code_run) self._action_run_sel_code.triggered.connect(self.slot_code_sel_run) self._shortcut_run_sel.activated.connect(self.slot_code_sel_run) self._shortcut_save.activated.connect(self.slot_save) self._action_save.triggered.connect(self.slot_save) self._action_find_replace.triggered.connect(self.slot_find_or_replace) self._shortcut_find_replace.activated.connect(self.slot_find_or_replace) self._action_autocomp.triggered.connect(self.autocomp) self._shortcut_autocomp.activated.connect(self.autocomp) self._shortcut_goto.activated.connect(self.slot_goto_line) def autocomp(self): logger.warning('Manual Autocompletion Triggered!') def get_word_under_cursor(self): pos = self.textEdit.getCursorPosition() text = self.textEdit.text(pos[0]) try: line = text[:pos[1] + 1] except Exception as e: logger.debug(e) line = '' word: str = re.split(r'[;,:/ .\\!&\|\*\+-=\s\(\)\{\}\[\]]', line)[-1].strip() col = pos[1] while (1): col += 1 if col > len(text) - 1: break char = text[col] if char in ' \n()[]{}\'\";:\t!+-*/\\=.': break word += char return word def current_line_text(self): current_row = self.textEdit.getCursorPosition()[0] current_len = self.textEdit.lineLength(current_row) self.textEdit.setSelection(current_row, 0, current_row, current_len) return self.text(True) def text(self, selected: bool = False) -> str: """ 返回编辑器选中或者全部内容 :rtype: str :return: 返回编辑器选中或者全部内容 """ if selected: return self.textEdit.selectedText() return self.textEdit.text() def set_text(self, text: str) -> None: """ 设置编辑器内容 :type text: str :param text: 文本内容 :return: None """ # self.textEdit.setText(text) # 该方法会重置撤销历史 try: text = text.encode(self._encoding) except Exception as e: logger.warning(str(e)) text = text.encode('utf-8', errors='ignore') self.textEdit.SendScintilla(QsciScintilla.SCI_SETTEXT, text) def filename(self) -> str: """ 返回当前文件名 :rtype: str :return: 返回当前文件名 """ return os.path.basename(self._path) def path(self) -> str: """ 返回当前文件路径 :rtype: str :return: 返回当前文件路径 """ return self._path def set_path(self, path: str) -> None: """ 设置文件路径 :param path: 设置文件路径 :type path: str :return: None """ self._path = path def modified(self) -> bool: """ 返回内容是否被修改 :rtype: bool :return: 返回内容是否被修改 """ return self.textEdit.isModified() def set_modified(self, modified: bool) -> None: """ 设置内容是否被修改 :param modified: 是否被修改 True or False :type: bool :return: None """ self.textEdit.setModified(modified) def load_file(self, path: str) -> None: """ 加载文件 :param path: 文件路径 :type path: str :return: None """ self._path = '' try: # 读取文件内容并加载 with open(path, 'rb') as fp: text = fp.read() text, coding = Utilities.decode(text) self.set_encoding(coding) self.set_text(text) self.set_modified(False) self.set_eol_status() except Exception as e: logger.warning(str(e)) self.extension_lib.show_log('error', 'CodeEditor', str(e)) self._path = path self.setWindowTitle(self.filename()) def set_encoding(self, encoding: str): """ 设置文本编码,仅支持 ASCII 和 UTF-8 :param encoding: ascii or gbk or utf-8 :type: str :return: """ encoding = encoding.lower() self._encoding = encoding self.label_status_encoding.setText(encoding.upper()) if encoding.startswith('utf'): self.textEdit.setUtf8(True) self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, QsciScintilla.SC_CP_UTF8) else: self.textEdit.setUtf8(False) self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, 936) def slot_find_or_replace(self): if self.find_dialog is None: self.find_dialog = FindDialog(parent=self, text_edit=self) self.find_dialog.show() return # match_regex = False # case_sensitive = False # match_whole_word = False # wrap_find = False # # first = self.textEdit.findFirst('def', False, False, False, False) # self.textEdit.replace('ggg') def slot_about_close(self, save_all=False) -> QMessageBox.StandardButton: """ 是否需要关闭以及保存 :param save_all: 当整个窗口关闭时增加是否全部关闭 :return:QMessageBox.StandardButton """ if not self.modified(): return QMessageBox.Discard buttons = QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel if save_all: buttons |= QMessageBox.SaveAll # 保存全部 buttons |= QMessageBox.NoToAll # 放弃所有 ret = QMessageBox.question(self, self.tr('Save'), self.tr('Save file "{0}"?').format(self.filename()), buttons, QMessageBox.Save) if ret == QMessageBox.Save or ret == QMessageBox.SaveAll: if not self.slot_save(): return QMessageBox.Cancel return ret def slot_modification_changed(self, modified: bool) -> None: """ 内容被修改槽函数 :param modified: 是否被修改 :type modified: bool :return: """ title = self.windowTitle() if modified: if not title.startswith('*'): self.setWindowTitle('*' + title) else: if title.startswith('*'): self.setWindowTitle(title[1:]) def create_context_menu(self) -> 'QMenu': menu = self.textEdit.createStandardContextMenu() # 遍历本身已有的菜单项做翻译处理 # 前提是要加载了Qt自带的翻译文件 for action in menu.actions(): action.setText(QCoreApplication.translate('QTextControl', action.text())) # 添加额外菜单 menu.addSeparator() menu.addAction(self._action_format) menu.addAction(self._action_run_code) menu.addAction(self._action_run_sel_code) menu.addAction(self._action_save) menu.addAction(self._action_find_replace) # menu.addAction(self) return menu def slot_custom_context_menu_requested(self, pos: QPoint) -> None: """ 右键菜单修改 :param pos: :type pos: QPoint :return: None """ menu = self.create_context_menu() # 根据条件决定菜单是否可用 enabled = len(self.text().strip()) > 0 self._action_format.setEnabled(enabled) self._action_run_code.setEnabled(enabled) # self._action_run_sel_code.setEnabled(self.textEdit.hasSelectedText()) self._action_run_sel_code.setEnabled(enabled) menu.exec_(self.textEdit.mapToGlobal(pos)) del menu def slot_save(self) -> bool: """ 保存时触发的事件。 :return: """ return self.save() def slot_text_changed(self) -> None: self.label_status_length.setText(self.tr('Length:{0} Lines:{1}').format(format(self.textEdit.length(), ','), format(self.textEdit.lines(), ','))) self.slot_modification_changed(True) self.set_modified(True) def save(self): """ 保存文件时调用的方法 :param ext_name: :return: """ path = self._path.replace(os.sep, '/') if path.startswith(QDir.tempPath().replace(os.sep, '/')): # 弹出对话框要求选择真实路径保存 path, ext = QFileDialog.getSaveFileName(self, self.tr('Save file'), self.extension_lib.Program.get_work_dir(), filter='*.py') if not path: return False if not path.endswith('.py'): path += '.py' self._path = path try: with open(self._path, 'wb') as fp: fp.write(self.text().encode('utf-8', errors='ignore')) self.setWindowTitle(os.path.basename(path)) self.slot_modification_changed(False) self.set_modified(False) return True except Exception as e: # 保存失败 logger.warning(str(e)) return False def set_eol_status(self): """ 根据文件内容中的换行符设置底部状态 :return: """ eols = re.findall(r'\r\n|\r|\n', self.text()) if not eols: self.label_status_eol.setText('Unix(LF)') self.textEdit.setEolMode(QsciScintilla.EolUnix) # \n换行 return grouped = [(len(list(group)), key) for key, group in groupby(sorted(eols))] eol = sorted(grouped, reverse=True)[0][1] if eol == '\r\n': self.label_status_eol.setText('Windows(CR LF)') self.textEdit.setEolMode(QsciScintilla.EolWindows) # \r\n换行 return QsciScintilla.EolWindows if eol == '\r': self.label_status_eol.setText('Mac(CR)') self.textEdit.setEolMode(QsciScintilla.EolMac) # \r换行 return self.label_status_eol.setText('Unix(LF)') self.textEdit.setEolMode(QsciScintilla.EolUnix) # \n换行 def _init_actions(self) -> None: """ 初始化额外菜单项 :return: """ self._action_format = QAction(self.tr('Format Code'), self.textEdit) self._action_run_code = QAction(self.tr('Run Code'), self.textEdit) self._action_run_sel_code = QAction(self.tr('Run Selected Code'), self.textEdit) self._action_save = QAction(self.tr('Save'), self.textEdit) self._action_find_replace = QAction(self.tr('Find/Replace'), self.textEdit) self._action_autocomp = QAction(self.tr('AutoComp'), self.textEdit) # 设置快捷键 self._shortcut_format = QShortcut(QKeySequence('Ctrl+Alt+F'), self.textEdit) self._action_format.setShortcut(QKeySequence('Ctrl+Alt+F')) self._shortcut_autocomp = QShortcut(QKeySequence('Ctrl+P'), self.textEdit) self._action_autocomp.setShortcut(QKeySequence("Ctrl+P")) self._shortcut_run = QShortcut(QKeySequence('Ctrl+R'), self.textEdit) self._action_run_code.setShortcut(QKeySequence('Ctrl+R')) self._shortcut_run_sel = QShortcut(Qt.Key_F9, self.textEdit) self._action_run_sel_code.setShortcut(Qt.Key_F9) self._action_save.setShortcut(QKeySequence('Ctrl+S')) self._shortcut_save = QShortcut(QKeySequence('Ctrl+S'), self.textEdit) self._action_find_replace.setShortcut(QKeySequence('Ctrl+F')) self._shortcut_find_replace = QShortcut(QKeySequence('Ctrl+F'), self.textEdit) self._shortcut_goto = QShortcut(QKeySequence('Ctrl+G'), self.textEdit) def slot_selection_changed(self) -> None: """ 选中内容变化槽函数 :return: None """ line_from, index_from, line_to, index_to = self.textEdit.getSelection() lines = 0 if line_from == line_to == -1 else line_to - line_from + 1 self.label_status_sel.setText( self.tr('Sel:{0} | {1}').format(format(len(self.textEdit.selectedText()), ','), format(lines, ','))) def slot_run_in_terminal(self): logger.warning('不支持在终端运行!') pass def slot_code_sel_run(self): """ 运行选中代码 :return: """ logger.warning('不支持在ipython运行!') def slot_code_run(self): """ 运行代码 :return: """ logger.warning('不支持在ipython运行!') def slot_code_format(self): pass def slot_goto_line(self): """ 跳转到指定行列 :return: """ GotoLineDialog(self.textEdit, self).exec_() self.textEdit.setFocus() def slot_set_theme(self, name: str, language=None): """设置编辑器主题 :param name: :param language: :return: """ if not name.endswith('.xml'): name += '.xml' path = os.path.join(os.path.dirname(__file__), 'themes', name) if not os.path.exists(path): return # 默认样式 self.textEdit.SendScintilla(QsciScintilla.SCI_SETSELBACK, 1, QColor(128, 128, 128)) self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor(Qt.black)) self.textEdit.SendScintilla(QsciScintilla.SCI_SETEDGECOLOUR, QColor(192, 192, 192)) self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINCOLOUR, True, QColor(128, 128, 128)) self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINHICOLOUR, True, QColor(Qt.white)) # self.textEdit.SendScintilla(QsciScintilla.SCI_INDICSETHOVERFORE, 8, QColor(128, 128, 128)) background = QColor('#FFFFFF') try: style = etree.parse(path) # 全局样式 for c in style.xpath('/NotepadPlus/GlobalStyles/WidgetStyle'): name, styleID, fgColor, bgColor = c.get('name'), int(c.get('styleID', 0)), '#' + str( c.get('fgColor', '')), '#' + str(c.get('bgColor', '')) logger.debug('name:%s, styleID:%s, fgColor:%s, bgColor:%s', name, styleID, fgColor, bgColor) if name == 'Default Style': if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_DEFAULT, QColor(fgColor)) logger.debug('SCI_STYLESETFORE STYLE_DEFAULT %s', fgColor) if bgColor != '#': background = QColor(bgColor) self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_DEFAULT, QColor(bgColor)) logger.debug('SCI_STYLESETBACK STYLE_DEFAULT %s', bgColor) elif name == 'Current line background colour': if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETLINEBACK, QColor(bgColor)) logger.debug('SCI_SETCARETLINEBACK %s', bgColor) elif name == 'Selected text colour': if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETSELBACK, 1, QColor(bgColor)) logger.debug('SCI_SETSELBACK %s', bgColor) elif styleID == QsciScintilla.SCI_SETCARETFORE: if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor(fgColor)) logger.debug('SCI_SETCARETFORE %s', fgColor) elif name == 'Edge colour': if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETEDGECOLOUR, QColor(fgColor)) logger.debug('SCI_SETEDGECOLOUR %s', fgColor) elif name == 'Fold margin': if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINHICOLOUR, True, QColor(fgColor)) logger.debug('SCI_SETFOLDMARGINHICOLOUR %s', fgColor) if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINCOLOUR, True, QColor(bgColor)) logger.debug('SCI_SETFOLDMARGINCOLOUR %s', bgColor) # elif name == 'URL hovered': # if fgColor != '#': # self.textEdit.SendScintilla(QsciScintilla.SCI_INDICSETHOVERFORE, 8, QColor(fgColor)) # logger.debug('SCI_INDICSETHOVERFORE %s', fgColor) elif name == 'White space symbol': if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_SETWHITESPACEFORE, True, QColor(fgColor)) logger.debug('SCI_SETWHITESPACEFORE %s', fgColor) elif styleID == QsciScintilla.STYLE_INDENTGUIDE: if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_INDENTGUIDE, QColor(fgColor)) logger.debug('SCI_STYLESETFORE STYLE_INDENTGUIDE %s', fgColor) if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_INDENTGUIDE, QColor(bgColor)) logger.debug('SCI_STYLESETBACK STYLE_INDENTGUIDE %s', bgColor) elif styleID == QsciScintilla.STYLE_BRACELIGHT: if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_BRACELIGHT, QColor(fgColor)) logger.debug('SCI_STYLESETFORE STYLE_BRACELIGHT %s', fgColor) if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_BRACELIGHT, QColor(bgColor)) logger.debug('SCI_STYLESETBACK STYLE_BRACELIGHT %s', bgColor) elif styleID == QsciScintilla.STYLE_BRACEBAD: if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_BRACEBAD, QColor(fgColor)) logger.debug('SCI_STYLESETFORE STYLE_BRACEBAD %s', fgColor) if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_BRACEBAD, QColor(bgColor)) logger.debug('SCI_STYLESETBACK STYLE_BRACEBAD %s', bgColor) elif styleID == QsciScintilla.STYLE_LINENUMBER: if fgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_LINENUMBER, QColor(fgColor)) logger.debug('SCI_STYLESETFORE STYLE_LINENUMBER %s', fgColor) if bgColor != '#': self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_LINENUMBER, QColor(bgColor)) logger.debug('SCI_STYLESETBACK STYLE_LINENUMBER %s', bgColor) if not self._lexer: return self._lexer.setPaper(background) # 关键词高亮 logger.debug('lexer language: %s', self._lexer.language()) # print(self._lexer.lexer()) for w in style.xpath('/NotepadPlus/LexerStyles/LexerType[@name="{0}"]/WordsStyle'.format( language if language else self._lexer.lexer().lower())): name, styleID, fgColor, bgColor = w.get('name'), int(w.get('styleID', 0)), '#' + str( w.get('fgColor', '')), '#' + str(w.get('bgColor', '')) logger.debug('name:%s, styleID:%s, fgColor:%s, bgColor:%s', name, styleID, fgColor, bgColor) self._lexer.setColor(QColor(fgColor), styleID) except Exception as e: logger.warning(str(e), exc_info=1) @pyqtProperty(str) def theme(self) -> str: """返回编辑器主题 :return: """ return self._theme @theme.setter def theme(self, name): """设置编辑器主题 :param name: :return: """ if name == self._theme: return self._theme = name self.slot_set_theme(name)
class CodeEditor(QsciScintilla): def __init__(self, parent=None): super().__init__(parent) self.filename = None self.fileBrowser = None self.mainWindow = parent self.debugging = False c = Configuration() self.pointSize = int(c.getFontSize()) self.tabWidth = int(c.getTab()) # Scrollbars self.verticalScrollBar().setStyleSheet( """border: 20px solid black; background-color: darkgreen; alternate-background-color: #FFFFFF;""") self.horizontalScrollBar().setStyleSheet( """border: 20px solid black; background-color: darkgreen; alternate-background-color: #FFFFFF;""") # matched / unmatched brace color ... self.setMatchedBraceBackgroundColor(QColor('#000000')) self.setMatchedBraceForegroundColor(QColor('cyan')) self.setUnmatchedBraceBackgroundColor(QColor('#000000')) self.setUnmatchedBraceForegroundColor(QColor('red')) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # edge mode ... line at 79 characters self.setEdgeColumn(79) self.setEdgeMode(1) self.setEdgeColor(QColor('dark green')) # Set the default font self.font = QFont() system = platform.system().lower() if system == 'windows': self.font.setFamily('Consolas') else: self.font.setFamily('Monospace') self.font.setFixedPitch(True) self.font.setPointSize(self.pointSize) self.setFont(self.font) self.setMarginsFont(self.font) # Margin 0 is used for line numbers fontmetrics = QFontMetrics(self.font) self.setMarginsFont(self.font) self.setMarginWidth(0, fontmetrics.width("00000") + 5) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#000000")) self.setMarginsForegroundColor(QColor("#FFFFFF")) # Margin 1 for breakpoints self.setMarginSensitivity(1, True) self.markerDefine(QsciScintilla.RightArrow, 8) self.setMarkerBackgroundColor(QColor('#FF0000'), 8) # variable for breakpoint self.breakpoint = False self.breakpointLine = None # FoldingBox self.setFoldMarginColors(QColor('dark green'), QColor('dark green')) # CallTipBox self.setCallTipsForegroundColor(QColor('#FFFFFF')) self.setCallTipsBackgroundColor(QColor('#282828')) self.setCallTipsHighlightColor(QColor('#3b5784')) self.setCallTipsStyle(QsciScintilla.CallTipsContext) self.setCallTipsPosition(QsciScintilla.CallTipsBelowText) self.setCallTipsVisible(-1) # change caret's color self.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor('#98fb98')) self.setCaretWidth(4) # tab Width self.setIndentationsUseTabs(False) self.setTabWidth(self.tabWidth) # use Whitespaces instead tabs self.SendScintilla(QsciScintilla.SCI_SETUSETABS, False) self.setAutoIndent(True) self.setTabIndents(True) # BackTab self.setBackspaceUnindents(True) # Current line visible with special background color or not :) #self.setCaretLineVisible(False) #self.setCaretLineVisible(True) #self.setCaretLineBackgroundColor(QColor("#020202")) self.setMinimumSize(300, 300) # get style self.style = None # Call the Color-Function: ... self.setPythonStyle() #self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) # Contextmenu self.setContextMenuPolicy(Qt.ActionsContextMenu) undoAction = QAction("Undo", self) undoAction.triggered.connect(self.undoContext) redoAction = QAction("Redo", self) redoAction.triggered.connect(self.redoContext) sepAction1 = QAction("", self) sepAction1.setSeparator(True) cutAction = QAction("Cut", self) cutAction.triggered.connect(self.cutContext) copyAction = QAction("Copy", self) copyAction.triggered.connect(self.copyContext) pasteAction = QAction("Paste", self) pasteAction.triggered.connect(self.pasteContext) sepAction2 = QAction("", self) sepAction2.setSeparator(True) sepAction3 = QAction("", self) sepAction3.setSeparator(True) selectAllAction = QAction("Select All", self) selectAllAction.triggered.connect(self.getContext) sepAction4 = QAction("", self) sepAction4.setSeparator(True) breakpointAction = QAction("Run until Breakpoint", self) breakpointAction.triggered.connect(self.breakpointContext) terminalAction = QAction("Open Terminal", self) terminalAction.triggered.connect(self.termContext) self.addAction(undoAction) self.addAction(redoAction) self.addAction(sepAction1) self.addAction(cutAction) self.addAction(copyAction) self.addAction(pasteAction) self.addAction(sepAction2) self.addAction(selectAllAction) self.addAction(sepAction3) self.addAction(breakpointAction) self.addAction(sepAction4) self.addAction(terminalAction) # signals self.SCN_FOCUSIN.connect(self.onFocusIn) self.textChanged.connect(self.onTextChanged) self.marginClicked.connect(self.onMarginClicked) def onFocusIn(self): self.mainWindow.refresh(self) def onTextChanged(self): notebook = self.mainWindow.notebook textPad = notebook.currentWidget() index = notebook.currentIndex() if self.debugging is True: self.mainWindow.statusBar.showMessage('remember to update CodeView if you delete or change lines in CodeEditor !', 3000) if textPad == None: return if textPad.filename: if not '*' in notebook.tabText(index): fname = os.path.basename(textPad.filename) fname += '*' notebook.setTabText(index, fname) else: fname = notebook.tabText(index) fname += '*' if not '*' in notebook.tabText(index): notebook.setTabText(index, fname) def onMarginClicked(self, margin, line, modifiers): if self.markersAtLine(line) != 0: self.markerDelete(line, 8) self.breakpoint = False self.breakpointLine = None self.mainWindow.statusBar.showMessage('Breakpoint removed', 3000) else: if self.breakpoint == False: self.markerAdd(line, 8) self.breakpoint = True self.breakpointLine = line + 1 self.mainWindow.statusBar.showMessage('Breakpoint set on line ' + \ str(self.breakpointLine), 3000) def checkPath(self, path): if '\\' in path: path = path.replace('\\', '/') return path def undoContext(self): self.resetBreakpoint() self.undo() def redoContext(self): self.resetBreakpoint() self.redo() def cutContext(self): self.resetBreakpoint() self.cut() def copyContext(self): self.resetBreakpoint() self.copy() def pasteContext(self): self.resetBreakpoint() self.paste() def getContext(self): self.selectAll() def breakpointContext(self): code = '' lines = self.lines() c = Configuration() system = c.getSystem() if self.breakpointLine: for i in range(lines): if i < self.breakpointLine: code += self.text(i) randomNumber = random.SystemRandom() number = randomNumber.randint(0, sys.maxsize) filename = 'temp_file_' + str(number) + '.py' try: with open(filename, 'w') as f: f.write(code) command = c.getRun(system).format(filename) thread = RunThread(command) thread.start() except Exception as e: print(str(e)) finally: time.sleep(2) os.remove(filename) def termContext(self): c = Configuration() system = c.getSystem() command = c.getTerminal(system) thread = RunThread(command) thread.start() def getLexer(self): return self.lexer def setPythonStyle(self): self.style = 'Python' # Set Python lexer self.setAutoIndent(True) #self.lexer = QsciLexerPython() self.lexer = PythonLexer() self.lexer.setFont(self.font) self.lexer.setFoldComments(True) # set Lexer self.setLexer(self.lexer) self.setCaretLineBackgroundColor(QColor("#344c4c")) self.lexer.setDefaultPaper(QColor("black")) self.lexer.setDefaultColor(QColor("white")) self.lexer.setColor(QColor('white'), 0) # default self.lexer.setPaper(QColor('black'), -1) # default -1 vor all styles self.lexer.setColor(QColor('gray'), PythonLexer.Comment) # = 1 self.lexer.setColor(QColor('orange'), 2) # Number = 2 self.lexer.setColor(QColor('lightblue'), 3) # DoubleQuotedString self.lexer.setColor(QColor('lightblue'), 4) # SingleQuotedString self.lexer.setColor(QColor('#33cccc'), 5) # Keyword self.lexer.setColor(QColor('lightblue'), 6) # TripleSingleQuotedString self.lexer.setColor(QColor('lightblue'), 7) # TripleDoubleQuotedString self.lexer.setColor(QColor('#ffff00'), 8) # ClassName self.lexer.setColor(QColor('#ffff66'), 9) # FunctionMethodName self.lexer.setColor(QColor('magenta'), 10) # Operator self.lexer.setColor(QColor('white'), 11) # Identifier self.lexer.setColor(QColor('gray'), 12) # CommentBlock self.lexer.setColor(QColor('#ff471a'), 13) # UnclosedString self.lexer.setColor(QColor('gray'), 14) # HighlightedIdentifier self.lexer.setColor(QColor('#5DD3AF'), 15) # Decorator self.setPythonAutocomplete() self.setFold() def setPythonAutocomplete(self): self.autocomplete = QsciAPIs(self.lexer) self.keywords = self.lexer.keywords(1) self.keywords = self.keywords.split(' ') for word in self.keywords: self.autocomplete.add(word) self.autocomplete.add('super') self.autocomplete.add('self') self.autocomplete.add('__name__') self.autocomplete.add('__main__') self.autocomplete.add('__init__') self.autocomplete.add('__str__') self.autocomplete.add('__repr__') self.autocomplete.prepare() ## Set the length of the string before the editor tries to autocomplete self.setAutoCompletionThreshold(3) ## Tell the editor we are using a QsciAPI for the autocompletion self.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.updateAutoComplete() def setFold(self): # setup Fold Styles for classes and functions ... x = self.FoldStyle(self.FoldStyle(5)) #self.textPad.folding() if not x: self.foldAll(False) self.setFolding(x) #self.textPad.folding() def unsetFold(self): self.setFolding(0) def keyReleaseEvent(self, e): # feed the autocomplete with the words from editor # simple algorithm to do this ... everytime after Enter # refresh CodeView text = self.text() self.updateCodeView(text) # if ENTER was hit ... : if e.key() == Qt.Key_Return: self.updateAutoComplete() if e.key() == Qt.Key_Backspace: self.resetBreakpoint() def resetBreakpoint(self): self.markerDeleteAll() self.breakpoint = False self.breakpointLine = None def updateCodeView(self, text=''): codeView = self.mainWindow.codeView codeViewDict = codeView.makeDictForCodeView(text) codeView.updateCodeView(codeViewDict) def updateAutoComplete(self, text=None): if not text: firstList = [] # list to edit secondList = [] # collect all items for autocomplete text = self.text() # parse complete text .... firstList = text.splitlines() for line in firstList: if 'def' in line: item = line.strip() item = item.strip('def') item = item.replace(':', '') if not item in secondList: secondList.append(item) elif 'class' in line: item = line.strip() item = item.strip('class') item = item.replace(':', '') if not item in secondList: secondList.append(item) text = text.replace('"', " ").replace("'", " ").replace("(", " ").replace\ (")", " ").replace("[", " ").replace("]", " ").replace\ (':', " ").replace(',', " ").replace("<", " ").replace\ (">", " ").replace("/", " ").replace("=", " ").replace\ (";", " ") firstList = text.split('\n') for row in firstList: if (row.strip().startswith('#')) or (row.strip().startswith('//')): continue else: wordList = row.split() for word in wordList: if re.match("(^[0-9])", word): continue elif '#' in word or '//' in word: continue elif word in self.keywords: continue elif (word == '__init__') or (word == '__main__') or \ (word == '__name__') or (word == '__str__') or \ (word == '__repr__'): continue elif word in secondList: continue elif len(word) > 15: continue elif not len(word) < 3: w = re.sub("{}<>;,:]", '', word) #print(w) secondList.append(w) # delete doubled entries x = set(secondList) secondList = list(x) # debugging ... #print(secondList) for item in secondList: self.autocomplete.add(item) self.autocomplete.prepare() def setPythonPrintStyle(self): # Set None lexer self.font = QFont() system = platform.system().lower() if system == 'windows': self.font.setFamily('Consolas') else: self.font.setFamily('Monospace') self.font.setFixedPitch(True) self.font.setPointSize(10) self.setFont(self.font) self.lexer = PythonLexer() self.lexer.setFont(self.font) # set Lexer self.setLexer(self.lexer) self.setCaretLineBackgroundColor(QColor("#344c4c")) self.lexer.setDefaultPaper(QColor("white")) self.lexer.setDefaultColor(QColor("black")) self.lexer.setColor(QColor('black'), -1) # default self.lexer.setPaper(QColor('white'), -1) # default self.lexer.setColor(QColor('gray'), PythonLexer.Comment) # entspricht 1 self.lexer.setColor(QColor('orange'), 2) # Number entspricht 2 self.lexer.setColor(QColor('darkgreen'), 3) # DoubleQuotedString entspricht 3 self.lexer.setColor(QColor('darkgreen'), 4) # SingleQuotedString entspricht 4 self.lexer.setColor(QColor('darkblue'), 5) # Keyword entspricht 5 self.lexer.setColor(QColor('darkgreen'), 6) # TripleSingleQuotedString entspricht 6 self.lexer.setColor(QColor('darkgreen'), 7) # TripleDoubleQuotedString entspricht 7 self.lexer.setColor(QColor('red'), 8) # ClassName entspricht 8 self.lexer.setColor(QColor('crimson'), 9) # FunctionMethodName entspricht 9 self.lexer.setColor(QColor('green'), 10) # Operator entspricht 10 self.lexer.setColor(QColor('black'), 11) # Identifier entspricht 11 ### alle Wörter self.lexer.setColor(QColor('gray'), 12) # CommentBlock entspricht 12 self.lexer.setColor(QColor('#ff471a'), 13) # UnclosedString entspricht 13 self.lexer.setColor(QColor('gray'), 14) # HighlightedIdentifier entspricht 14 self.lexer.setColor(QColor('#5DD3AF'), 15) # Decorator entspricht 15 self.setNoneAutocomplete() self.unsetFold() self.font = QFont() system = platform.system().lower() if system == 'windows': self.font.setFamily('Consolas') else: self.font.setFamily('Monospace') self.font.setFixedPitch(True) self.font.setPointSize(self.pointSize) def setNoneAutocomplete(self): #AutoCompletion self.autocomplete = Qsci.QsciAPIs(self.lexer) self.autocomplete.clear() self.autocomplete.prepare() self.setAutoCompletionThreshold(3) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) def resetPythonPrintStyle(self, lexer): self.font = QFont() system = platform.system().lower() if system == 'windows': self.font.setFamily('Consolas') else: self.font.setFamily('Monospace') self.font.setFixedPitch(True) self.font.setPointSize(self.pointSize) self.setFont(self.font) lexer.setFont(self.font) # set Lexer self.setLexer(lexer) # margins reset # Margin 0 is used for line numbers fontmetrics = QFontMetrics(self.font) self.setMarginsFont(self.font) self.setMarginWidth(0, fontmetrics.width("00000") + 5) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#000000")) self.setMarginsForegroundColor(QColor("#FFFFFF")) # FoldingBox self.setFoldMarginColors(QColor('dark green'), QColor('dark green'))
class EditorPane(QsciScintilla): """ Represents the text editor. """ # Signal fired when a script or hex is droped on this editor open_file = pyqtSignal(str) def __init__(self, path, text, newline=NEWLINE): super().__init__() self.setUtf8(True) self.path = path self.setText(text) self.newline = newline self.check_indicators = { # IDs are arbitrary 'error': { 'id': 19, 'markers': {} }, 'style': { 'id': 20, 'markers': {} } } self.search_indicators = {'selection': {'id': 21, 'positions': []}} self.DEBUG_INDICATOR = 22 # Arbitrary self.BREAKPOINT_MARKER = 23 # Arbitrary self.previous_selection = { 'line_start': 0, 'col_start': 0, 'line_end': 0, 'col_end': 0 } if self.path: if self.path.endswith(".css"): self.lexer = CssLexer() elif self.path.endswith(".html") or self.path.endswith(".htm"): self.lexer = QsciLexerHTML() self.lexer.setDjangoTemplates(True) else: self.lexer = PythonLexer() else: self.lexer = PythonLexer() self.api = None self.has_annotations = False self.setModified(False) self.breakpoint_handles = set() self.configure() def wheelEvent(self, event): """ Stops QScintilla from doing the wrong sort of zoom handling. """ if not QApplication.keyboardModifiers(): super().wheelEvent(event) def dropEvent(self, event): """ Run by Qt when *something* is dropped on this editor """ # Does the drag event have any urls? # Files are transfered as a url (by path not value) if event.mimeData().hasUrls(): # Qt doesn't seem to have an 'open' action, # this seems the most appropriate event.setDropAction(Qt.CopyAction) # Valid links links = [] # Iterate over each of the urls attached to the event for url in event.mimeData().urls(): # Check the url is to a local file # (not a webpage for example) if url.isLocalFile(): # Grab a 'real' path from the url path = url.toLocalFile() # Add it to the list of valid links links.append(path) # Did we get any? if len(links) > 0: # Only accept now we actually know we can do # something with the drop event event.accept() for link in links: # Start bubbling an open file request self.open_file.emit(link) # If the event wasn't handled let QsciScintilla have a go if not event.isAccepted(): super().dropEvent(event) def configure(self): """ Set up the editor component. """ # Font information font = Font().load() self.setFont(font) # Generic editor settings self.setUtf8(True) self.setAutoIndent(True) self.setIndentationsUseTabs(False) self.setIndentationWidth(4) self.setIndentationGuides(True) self.setBackspaceUnindents(True) self.setTabWidth(4) self.setEdgeColumn(79) self.setMarginLineNumbers(0, True) self.setMarginWidth(0, 50) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) self.set_theme() # Markers and indicators self.setMarginSensitivity(0, True) self.markerDefine(self.Circle, self.BREAKPOINT_MARKER) self.setMarginSensitivity(1, True) # Additional dummy margin to prevent accidental breakpoint toggles when # trying to position the edit cursor to the left of the first column, # using the mouse and not being 100% accurate. This margin needs to be # set with "sensitivity on": otherwise clicking it would select the # whole text line, per QsciScintilla's behaviour. It is up to the # click handler to ignore clicks on this margin: self.connect_margin. self.setMarginWidth(4, 8) self.setMarginSensitivity(4, True) # Indicators self.setIndicatorDrawUnder(True) for type_ in self.check_indicators: self.indicatorDefine(self.SquiggleIndicator, self.check_indicators[type_]['id']) for type_ in self.search_indicators: self.indicatorDefine(self.StraightBoxIndicator, self.search_indicators[type_]['id']) self.indicatorDefine(self.FullBoxIndicator, self.DEBUG_INDICATOR) self.setAnnotationDisplay(self.AnnotationBoxed) self.selectionChanged.connect(self.selection_change_listener) self.set_zoom() def connect_margin(self, func): """ Connect clicking the margin to the passed in handler function, via a filtering handler that ignores clicks on margin 4. """ # Margin 4 motivation in self.configure comments. def func_ignoring_margin_4(margin, line, modifiers): if margin != 4: func(margin, line, modifiers) self.marginClicked.connect(func_ignoring_margin_4) def set_theme(self, theme=DayTheme): """ Connect the theme to a lexer and return the lexer for the editor to apply to the script text. """ theme.apply_to(self.lexer) self.lexer.setDefaultPaper(theme.Paper) self.setCaretForegroundColor(theme.Caret) self.setIndicatorForegroundColor(theme.IndicatorError, self.check_indicators['error']['id']) self.setIndicatorForegroundColor(theme.IndicatorStyle, self.check_indicators['style']['id']) self.setIndicatorForegroundColor(theme.DebugStyle, self.DEBUG_INDICATOR) for type_ in self.search_indicators: self.setIndicatorForegroundColor( theme.IndicatorWordMatch, self.search_indicators[type_]['id']) self.setMarkerBackgroundColor(theme.BreakpointMarker, self.BREAKPOINT_MARKER) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setLexer(self.lexer) self.setMarginsBackgroundColor(theme.Margin) self.setMarginsForegroundColor(theme.Caret) self.setMatchedBraceBackgroundColor(theme.BraceBackground) self.setMatchedBraceForegroundColor(theme.BraceForeground) self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground) self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground) def set_api(self, api_definitions): """ Sets the API entries for tooltips, calltips and the like. """ self.api = QsciAPIs(self.lexer) for entry in api_definitions: self.api.add(entry) self.api.prepare() def set_zoom(self, size='m'): """ Sets the font zoom to the specified base point size for all fonts given a t-shirt size. """ sizes = { 'xs': -4, 's': -2, 'm': 1, 'l': 4, 'xl': 8, 'xxl': 16, 'xxxl': 48, } self.zoomTo(sizes[size]) @property def label(self): """ The label associated with this editor widget (usually the filename of the script we're editing). """ if self.path: label = os.path.basename(self.path) else: label = _('untitled') return label @property def title(self): """ The title associated with this editor widget (usually the filename of the script we're editing). If the script has been modified since it was last saved, the label will end with an asterisk. """ if self.isModified(): return self.label + ' •' return self.label def reset_annotations(self): """ Clears all the assets (indicators, annotations and markers). """ self.clearAnnotations() self.markerDeleteAll() self.reset_search_indicators() self.reset_check_indicators() def reset_check_indicators(self): """ Clears all the text indicators related to the check code functionality. """ for indicator in self.check_indicators: for _, markers in \ self.check_indicators[indicator]['markers'].items(): line_no = markers[0]['line_no'] # All markers on same line. self.clearIndicatorRange( line_no, 0, line_no, 999999, self.check_indicators[indicator]['id']) self.check_indicators[indicator]['markers'] = {} def reset_search_indicators(self): """ Clears all the text indicators from the search functionality. """ for indicator in self.search_indicators: for position in self.search_indicators[indicator]['positions']: self.clearIndicatorRange( position['line_start'], position['col_start'], position['line_end'], position['col_end'], self.search_indicators[indicator]['id']) self.search_indicators[indicator]['positions'] = [] def annotate_code(self, feedback, annotation_type='error'): """ Given a list of annotations add them to the editor pane so the user can act upon them. """ indicator = self.check_indicators[annotation_type] for line_no, messages in feedback.items(): indicator['markers'][line_no] = messages for message in messages: col = message.get('column', 0) if col: col_start = col - 1 col_end = col + 1 self.fillIndicatorRange(line_no, col_start, line_no, col_end, indicator['id']) if feedback: # Ensure the first line with a problem is visible. first_problem_line = sorted(feedback.keys())[0] self.ensureLineVisible(first_problem_line) def debugger_at_line(self, line): """ Set the line to be highlighted with the DEBUG_INDICATOR. """ self.reset_debugger_highlight() # Calculate the line length & account for \r\n giving ObOE. line_length = len(self.text(line).rstrip()) self.fillIndicatorRange(line, 0, line, line_length, self.DEBUG_INDICATOR) self.ensureLineVisible(line) def reset_debugger_highlight(self): """ Reset all the lines so the DEBUG_INDICATOR is no longer displayed. We need to check each line since there's no way to tell what the currently highlighted line is. This approach also has the advantage of resetting the *whole* editor pane. """ for i in range(self.lines()): line_length = len(self.text(i)) self.clearIndicatorRange(i, 0, i, line_length, self.DEBUG_INDICATOR) def show_annotations(self): """ Display all the messages to be annotated to the code. """ lines = defaultdict(list) for indicator in self.check_indicators: markers = self.check_indicators[indicator]['markers'] for k, marker_list in markers.items(): for m in marker_list: lines[m['line_no']].append('\u2191 ' + m['message']) for line, messages in lines.items(): text = '\n'.join(messages).strip() if text: self.annotate(line, text, self.annotationDisplay()) def find_next_match(self, text, from_line=-1, from_col=-1, case_sensitive=True, wrap_around=True): """ Finds the next text match from the current cursor, or the given position, and selects it (the automatic selection is the only available QsciScintilla behaviour). Returns True if match found, False otherwise. """ return self.findFirst( text, # Text to find, False, # Treat as regular expression case_sensitive, # Case sensitive search True, # Whole word matches only wrap_around, # Wrap search forward=True, # Forward search line=from_line, # -1 starts at current position index=from_col, # -1 starts at current position show=False, # Unfolds found text posix=False) # More POSIX compatible RegEx def range_from_positions(self, start_position, end_position): """Given a start-end pair, such as are provided by a regex match, return the corresponding Scintilla line-offset pairs which are used for searches, indicators etc. NOTE: Arguments must be byte offsets into the underlying text bytes. """ start_line, start_offset = self.lineIndexFromPosition(start_position) end_line, end_offset = self.lineIndexFromPosition(end_position) return start_line, start_offset, end_line, end_offset def highlight_selected_matches(self): """ Checks the current selection, if it is a single word it then searches and highlights all matches. Since we're interested in exactly one word: * Ignore an empty selection * Ignore anything which spans more than one line * Ignore more than one word * Ignore anything less than one word """ selected_range = line0, col0, line1, col1 = self.getSelection() # # If there's no selection, do nothing # if selected_range == (-1, -1, -1, -1): return # # Ignore anything which spans two or more lines # if line0 != line1: return # # Ignore if no text is selected or the selected text is not at most one # valid identifier-type word. # selected_text = self.selectedText() if not RE_VALID_WORD.match(selected_text): return # # Ignore anything which is not a whole word. # NB Although Scintilla defines a SCI_ISRANGEWORD message, # it's not exposed by QSciScintilla. Instead, we # ask Scintilla for the start end end position of # the word we're in and test whether our range end points match # those or not. # pos0 = self.positionFromLineIndex(line0, col0) word_start_pos = self.SendScintilla( QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1) _, start_offset = self.lineIndexFromPosition(word_start_pos) if col0 != start_offset: return pos1 = self.positionFromLineIndex(line1, col1) word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos1, 1) _, end_offset = self.lineIndexFromPosition(word_end_pos) if col1 != end_offset: return # # For each matching word within the editor text, add it to # the list of highlighted indicators and fill it according # to the current theme. # indicators = self.search_indicators['selection'] encoding = 'utf8' if self.isUtf8() else 'latin1' text_bytes = self.text().encode(encoding) selected_text_bytes = selected_text.encode(encoding) for match in re.finditer(selected_text_bytes, text_bytes): range = self.range_from_positions(*match.span()) # # Don't highlight the text we've selected # if range == selected_range: continue line_start, col_start, line_end, col_end = range indicators['positions'].append({ 'line_start': line_start, 'col_start': col_start, 'line_end': line_end, 'col_end': col_end }) self.fillIndicatorRange(line_start, col_start, line_end, col_end, indicators['id']) def selection_change_listener(self): """ Runs every time the text selection changes. This could get triggered multiple times while the mouse click is down, even if selection has not changed in itself. If there is a new selection is passes control to highlight_selected_matches. """ # Get the current selection, exit if it has not changed line_from, index_from, line_to, index_to = self.getSelection() if self.previous_selection['col_end'] != index_to or \ self.previous_selection['col_start'] != index_from or \ self.previous_selection['line_start'] != line_from or \ self.previous_selection['line_end'] != line_to: self.previous_selection['line_start'] = line_from self.previous_selection['col_start'] = index_from self.previous_selection['line_end'] = line_to self.previous_selection['col_end'] = index_to # Highlight matches self.reset_search_indicators() self.highlight_selected_matches() def toggle_line(self, raw_line): """ Given a raw_line, will return the toggled version of it. """ clean_line = raw_line.strip() if not clean_line or clean_line.startswith('##'): # Ignore whitespace-only lines and compact multi-commented lines return raw_line if clean_line.startswith('#'): # It's a comment line, so replace only the first "# " or "#": if clean_line.startswith('# '): return raw_line.replace('# ', '', 1) else: return raw_line.replace('#', '', 1) else: # It's a normal line of code. return '# ' + raw_line def toggle_comments(self): """ Iterate through the selected lines and toggle their comment/uncomment state. So, lines that are not comments become comments and vice versa. """ if self.hasSelectedText(): # Toggle currently selected text. logger.info("Toggling comments") line_from, index_from, line_to, index_to = self.getSelection() selected_text = self.selectedText() lines = selected_text.split('\n') toggled_lines = [] for line in lines: toggled_lines.append(self.toggle_line(line)) new_text = '\n'.join(toggled_lines) self.replaceSelectedText(new_text) # Ensure the new text is also selected. last_newline = toggled_lines[-1] last_oldline = lines[-1] # Adjust the selection based on whether the last line got # longer, shorter, or stayed the same delta = len(last_newline) - len(last_oldline) index_to += delta self.setSelection(line_from, index_from, line_to, index_to) else: # Toggle the line currently containing the cursor. line_number, column = self.getCursorPosition() logger.info('Toggling line {}'.format(line_number)) line_content = self.text(line_number) new_line = self.toggle_line(line_content) self.setSelection(line_number, 0, line_number, len(line_content)) self.replaceSelectedText(new_line) self.setSelection(line_number, 0, line_number, len(new_line) - 1)
class TkPyTextEdit(QsciScintilla): key_pressed = pyqtSignal() format_code = pyqtSignal() sort_imports = pyqtSignal() def __init__(self, parent=None): QsciScintilla.__init__(self, parent) self.complete = AutoComplete() self.complete.prepare.connect(self.update_completes) self.lexer = text_lexer(self) self.setLexer(self.lexer) self.api = QsciAPIs(self.lexer) self.setAutoIndent(True) self.setMarginLineNumbers(0, True) self.setEdgeMode(QsciScintilla.EdgeLine) self.setEdgeColumn(79) self.setEdgeColor(QColor(0, 0, 0)) """ self.setIndentationsUseTabs(True) self.setIndentationWidth(get_configs()['tab_width']) self.setTabWidth(get_configs()['tab_width']) self.setTabIndents(get_configs()['indent_with_tabs']) self.setBackspaceUnindents(True) self.setCaretLineVisible(True) self.setIndentationGuides(True) self.setCaretForegroundColor(QColor(get_configs()['cursor_color'])) self.setCaretLineBackgroundColor(QColor(get_configs()['line_background_color'])) self.setCaretWidth(6) self.setWrapMode(QsciScintilla.WrapNone if not get_configs()['text_wrap'] else QsciScintilla.WrapWhitespace) self.setEolMode(get_eol_mode(get_configs()['eol_mode'])) # self.setMarginsForegroundColor(QColor("#ff888888")) """ self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20) self.setFolding(QsciScintilla.PlainFoldStyle) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setAutoCompletionCaseSensitivity(True) self.setAutoCompletionReplaceWord(True) self.autoCompleteFromAll() self.setAutoCompletionThreshold(1) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setUtf8(True) self.setBraceMatching(QsciScintilla.StrictBraceMatch) self.setMatchedBraceForegroundColor( QColor(self.lexer.styles[Name.Decorator])) self.setMatchedBraceBackgroundColor(RGB(0, 255, 0).to_pyqt_color()) self.setCallTipsVisible(-1) def goto_html_or_define(self, position): print(position) def keyPressEvent(self, event): if event.key() == Qt.Key_Space: if QApplication.keyboardModifiers() == Qt.ControlModifier: self.autoCompleteFromAll() return QsciScintilla.keyPressEvent(self, event) self.complete.line, self.complete.column = self.getCursorPosition() self.complete.text = self.text() self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20) # if event.key() not in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: # if not self.complete.started: # self.complete.start() self.key_pressed.emit() def contextMenuEvent(self, event: QContextMenuEvent) -> None: menu = QMenu(self) complete_code = menu.addAction('代码提示') complete_code.triggered.connect(self.autoCompleteFromAll) complete_code.setShortcut('Ctrl+Space') menu.addSeparator() copy = menu.addAction('复制') copy.setShortcut(get_event('Ctrl+C')) copy.triggered.connect(self.copy) paste = menu.addAction('粘贴') paste.setShortcut(get_event('Ctrl+V')) paste.triggered.connect(self.paste) cut = menu.addAction('剪切') cut.setShortcut(get_event('Ctrl+X')) cut.triggered.connect(self.cut) menu.addSeparator() undo = menu.addAction('撤销') undo.setShortcut(get_event('Ctrl+Z')) undo.triggered.connect(self.undo) redo = menu.addAction('撤回') redo.setShortcut('Ctrl+Y') redo.triggered.connect(self.redo) menu.addSeparator() format_code = menu.addAction('格式化代码') format_code.triggered.connect(self.format_code.emit) format_code.setShortcut('Ctrl+Alt+L') sort_imports = menu.addAction('整理Import语句') sort_imports.triggered.connect(self.sort_imports.emit) menu.exec_(event.globalPos()) def paste(self): QsciScintilla.paste(self) self.key_pressed.emit() self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20) def cut(self): QsciScintilla.cut(self) self.key_pressed.emit() self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20) def update_completes(self, completes): for complete in completes: self.api.add(complete) self.api.prepare() def goto_line(self, lineno: int, column: int = 0): self.setCursorPosition(lineno, column)
class EditorPane(QsciScintilla): """ Represents the text editor. """ # Signal fired when a script or hex is droped on this editor open_file = pyqtSignal(str) def __init__(self, path, text, newline=NEWLINE): super().__init__() self.setUtf8(True) self.path = path self.setText(text) self.newline = newline self.check_indicators = { # IDs are arbitrary 'error': { 'id': 19, 'markers': {} }, 'style': { 'id': 20, 'markers': {} } } self.BREAKPOINT_MARKER = 23 # Arbitrary self.search_indicators = {'selection': {'id': 21, 'positions': []}} self.previous_selection = { 'line_start': 0, 'col_start': 0, 'line_end': 0, 'col_end': 0 } self.lexer = PythonLexer() self.api = None self.has_annotations = False self.setModified(False) self.breakpoint_lines = set() self.configure() def dropEvent(self, event): """ Run by Qt when *something* is dropped on this editor """ # Does the drag event have any urls? # Files are transfered as a url (by path not value) if event.mimeData().hasUrls(): # Qt doesn't seem to have an 'open' action, # this seems the most appropriate event.setDropAction(Qt.CopyAction) # Valid links links = [] # Iterate over each of the urls attached to the event for url in event.mimeData().urls(): # Check the url is to a local file # (not a webpage for example) if url.isLocalFile(): # Grab a 'real' path from the url path = url.toLocalFile() # Add it to the list of valid links links.append(path) # Did we get any? if len(links) > 0: # Only accept now we actually know we can do # something with the drop event event.accept() for link in links: # Start bubbling an open file request self.open_file.emit(link) # If the event wasn't handled let QsciScintilla have a go if not event.isAccepted(): super().dropEvent(event) def configure(self): """ Set up the editor component. """ # Font information font = Font().load() self.setFont(font) # Generic editor settings self.setUtf8(True) self.setAutoIndent(True) self.setIndentationsUseTabs(False) self.setIndentationWidth(4) self.setIndentationGuides(True) self.setBackspaceUnindents(True) self.setTabWidth(4) self.setEdgeColumn(79) self.setMarginLineNumbers(0, True) self.setMarginWidth(0, 50) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) self.set_theme() # Markers and indicators self.setMarginSensitivity(0, True) self.markerDefine(self.Circle, self.BREAKPOINT_MARKER) self.setMarginSensitivity(1, True) self.setIndicatorDrawUnder(True) for type_ in self.check_indicators: self.indicatorDefine(self.SquiggleIndicator, self.check_indicators[type_]['id']) for type_ in self.search_indicators: self.indicatorDefine(self.StraightBoxIndicator, self.search_indicators[type_]['id']) self.setAnnotationDisplay(self.AnnotationBoxed) self.selectionChanged.connect(self.selection_change_listener) def connect_margin(self, func): """ Connect clicking the margin to the passed in handler function. """ self.marginClicked.connect(func) def set_theme(self, theme=DayTheme): """ Connect the theme to a lexer and return the lexer for the editor to apply to the script text. """ theme.apply_to(self.lexer) self.lexer.setDefaultPaper(theme.Paper) self.setCaretForegroundColor(theme.Caret) self.setMarginsBackgroundColor(theme.Margin) self.setMarginsForegroundColor(theme.Caret) self.setIndicatorForegroundColor(theme.IndicatorError, self.check_indicators['error']['id']) self.setIndicatorForegroundColor(theme.IndicatorStyle, self.check_indicators['style']['id']) for type_ in self.search_indicators: self.setIndicatorForegroundColor( theme.IndicatorWordMatch, self.search_indicators[type_]['id']) self.setMarkerBackgroundColor(theme.BreakpointMarker, self.BREAKPOINT_MARKER) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAll) self.setLexer(self.lexer) self.setMatchedBraceBackgroundColor(theme.BraceBackground) self.setMatchedBraceForegroundColor(theme.BraceForeground) self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground) self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground) def set_api(self, api_definitions): """ Sets the API entries for tooltips, calltips and the like. """ self.api = QsciAPIs(self.lexer) for entry in api_definitions: self.api.add(entry) self.api.prepare() @property def label(self): """ The label associated with this editor widget (usually the filename of the script we're editing). If the script has been modified since it was last saved, the label will end with an asterisk. """ if self.path: label = os.path.basename(self.path) else: label = 'untitled' # Add an asterisk to indicate that the file remains unsaved. if self.isModified(): return label + ' *' else: return label def reset_annotations(self): """ Clears all the assets (indicators, annotations and markers). """ self.clearAnnotations() self.markerDeleteAll() self.reset_search_indicators() self.reset_check_indicators() def reset_check_indicators(self): """ Clears all the text indicators related to the check code functionality. """ for indicator in self.check_indicators: for _, markers in \ self.check_indicators[indicator]['markers'].items(): line_no = markers[0]['line_no'] # All markers on same line. self.clearIndicatorRange( line_no, 0, line_no, 999999, self.check_indicators[indicator]['id']) self.check_indicators[indicator]['markers'] = {} def reset_search_indicators(self): """ Clears all the text indicators from the search functionality. """ for indicator in self.search_indicators: for position in self.search_indicators[indicator]['positions']: self.clearIndicatorRange( position['line_start'], position['col_start'], position['line_end'], position['col_end'], self.search_indicators[indicator]['id']) self.search_indicators[indicator]['positions'] = [] def annotate_code(self, feedback, annotation_type='error'): """ Given a list of annotations add them to the editor pane so the user can act upon them. """ indicator = self.check_indicators[annotation_type] for line_no, messages in feedback.items(): indicator['markers'][line_no] = messages for message in messages: col = message.get('column', 0) if col: col_start = col - 1 col_end = col + 1 self.fillIndicatorRange(line_no, col_start, line_no, col_end, indicator['id']) def show_annotations(self): """ Display all the messages to be annotated to the code. """ lines = defaultdict(list) for indicator in self.check_indicators: markers = self.check_indicators[indicator]['markers'] for k, marker_list in markers.items(): for m in marker_list: lines[m['line_no']].append('\u2191 ' + m['message']) for line, messages in lines.items(): text = '\n'.join(messages).strip() if text: self.annotate(line, text, self.annotationDisplay()) def find_next_match(self, text, from_line=-1, from_col=-1, case_sensitive=True, wrap_around=True): """ Finds the next text match from the current cursor, or the given position, and selects it (the automatic selection is the only available QsciScintilla behaviour). Returns True if match found, False otherwise. """ return self.findFirst( text, # Text to find, False, # Treat as regular expression case_sensitive, # Case sensitive search True, # Whole word matches only wrap_around, # Wrap search forward=True, # Forward search line=from_line, # -1 starts at current position index=from_col, # -1 starts at current position show=False, # Unfolds found text posix=False) # More POSIX compatible RegEx def range_from_positions(self, start_position, end_position): """Given a start-end pair, such as are provided by a regex match, return the corresponding Scintilla line-offset pairs which are used for searches, indicators etc. FIXME: Not clear whether the Scintilla conversions are expecting bytes or characters (ie codepoints) """ start_line, start_offset = self.lineIndexFromPosition(start_position) end_line, end_offset = self.lineIndexFromPosition(end_position) return start_line, start_offset, end_line, end_offset def highlight_selected_matches(self): """ Checks the current selection, if it is a single word it then searches and highlights all matches. Since we're interested in exactly one word: * Ignore an empty selection * Ignore anything which spans more than one line * Ignore more than one word * Ignore anything less than one word """ selected_range = line0, col0, line1, col1 = self.getSelection() # # If there's no selection, do nothing # if selected_range == (-1, -1, -1, -1): return # # Ignore anything which spans two or more lines # if line0 != line1: return # # Ignore if no text is selected or the selected text is not at most one # valid identifier-type word. # selected_text = self.selectedText() if not RE_VALID_WORD.match(selected_text): return # # Ignore anything which is not a whole word. # NB Although Scintilla defines a SCI_ISRANGEWORD message, # it's not exposed by QSciScintilla. Instead, we # ask Scintilla for the start end end position of # the word we're in and test whether our range end points match # those or not. # pos0 = self.positionFromLineIndex(line0, col0) word_start_pos = self.SendScintilla( QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1) _, start_offset = self.lineIndexFromPosition(word_start_pos) if col0 != start_offset: return pos1 = self.positionFromLineIndex(line1, col1) word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos1, 1) _, end_offset = self.lineIndexFromPosition(word_end_pos) if col1 != end_offset: return # # For each matching word within the editor text, add it to # the list of highlighted indicators and fill it according # to the current theme. # indicators = self.search_indicators['selection'] text = self.text() for match in re.finditer(selected_text, text): range = self.range_from_positions(*match.span()) # # Don't highlight the text we've selected # if range == selected_range: continue line_start, col_start, line_end, col_end = range indicators['positions'].append({ 'line_start': line_start, 'col_start': col_start, 'line_end': line_end, 'col_end': col_end }) self.fillIndicatorRange(line_start, col_start, line_end, col_end, indicators['id']) def selection_change_listener(self): """ Runs every time the text selection changes. This could get triggered multiple times while the mouse click is down, even if selection has not changed in itself. If there is a new selection is passes control to highlight_selected_matches. """ # Get the current selection, exit if it has not changed line_from, index_from, line_to, index_to = self.getSelection() if self.previous_selection['col_end'] != index_to or \ self.previous_selection['col_start'] != index_from or \ self.previous_selection['line_start'] != line_from or \ self.previous_selection['line_end'] != line_to: self.previous_selection['line_start'] = line_from self.previous_selection['col_start'] = index_from self.previous_selection['line_end'] = line_to self.previous_selection['col_end'] = index_to # Highlight matches self.reset_search_indicators() self.highlight_selected_matches()
class Editor(QsciScintilla): def __init__(self, parent=None): super().__init__(parent) self.fileName = None self.parent = parent self.debugging = False self.line = None self.column = None self.wordlist = [] self.searchtext = None self.font = QFont() self.font.setFamily("Inconsolata") self.pointSize = editor["pointSize"] self.font.setPointSize(self.pointSize) self.dialog = MessageBox(self) self.verticalScrollBar().setStyleSheet(""" background-color: transparent; """) self.horizontalScrollBar().setStyleSheet(""" background-color: transparent; """) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setCaretForegroundColor(QColor("#FFFFFF")) self.setEdgeColumn(121) self.setEdgeMode(1) self.setEdgeColor(QColor("#8c8c8c")) self.setFont(self.font) self.setMarginSensitivity(1, True) self.markerDefine(QsciScintilla.RightArrow, 8) self.setMarkerBackgroundColor(QColor("#FF0000"), 8) self.indicator_number = 0 self.indicator_value = 222 self.indicator_color = QColor("#FF0000") self.draw_under_text = True # Initializing some stuff self.set_brace_colors(QColor("#98b4f9"), QColor("#edf40e"), QColor("#98b4f9"), QColor("red")) self.cursorPositionChanged.connect(self.change_col) self.textChanged.connect(self.check_lines) self.set_linenumbers(QFontMetrics(self.font)) self.setFoldMarginColors(QColor("#212121"), QColor("#212121")) self.set_indentation_settings() def set_up_tooltips(self): self.setCallTipsStyle(QsciScintilla.CallTipsNoContext) self.setCallTipsVisible(0) self.setCallTipsPosition(QsciScintilla.CallTipsAboveText) self.setCallTipsBackgroundColor(QColor("#FF0000")) self.setCallTipsForegroundColor(QColor("#FF0000")) self.setCallTipsHighlightColor(QColor("#FF0000")) def set_brace_colors(self, matched_B=None, matched_F=None, unmatched_B=None, unmatched_F=None): self.setMatchedBraceBackgroundColor(matched_B) self.setMatchedBraceForegroundColor(matched_F) self.setUnmatchedBraceBackgroundColor(unmatched_B) self.setUnmatchedBraceForegroundColor(unmatched_F) self.setBraceMatching(QsciScintilla.SloppyBraceMatch) def set_linenumbers(self, fontmetrics): self.setMarginsFont(self.font) self.setMarginWidth(0, fontmetrics.width("00000")) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#212121")) self.setMarginsForegroundColor(QColor("#FFFFFF")) def set_indentation_settings(self): self.setIndentationsUseTabs(False) self.setTabWidth(4) self.SendScintilla(QsciScintilla.SCI_SETUSETABS, False) self.setAutoIndent(True) self.setTabIndents(True) def check_lines(self): line_n = self.lines() for i in range(line_n): if self.lineLength(i) > 121: # TODO: Make a character format or something pass # print("Line over 121 characters on line", str(i+1)) # self.setCursorPosition(i, 120) def python_highlighter(self): self.lexer = PythonLexer() self.lexer.setFoldComments(True) self.setCaretLineVisible(True) self.setDefaultSettings(self.lexer) self.setPythonAutocomplete() self.setFold() def json_highlighter(self): lexer = QsciLexerJSON() self.setDefaultSettings(lexer) def c_highlighter(self): lexer = QsciLexerCPP() self.setDefaultSettings(lexer) def xml_highlighter(self): lexer = QsciLexerXML() self.setDefaultSettings(lexer) def html_highlighter(self): lexer = QsciLexerHTML() self.setDefaultSettings(lexer) def setDefaultSettings(self, lexer): # self.setAutoIndent(True) lexer.setFont(self.font) lexer.setColor(QColor("white"), 0) # default lexer.setColor(QColor("#6B6E6C"), PythonLexer.Comment) # = 1 lexer.setColor(QColor("#ADD4FF"), 2) # Number = 2 lexer.setColor(QColor("#38ef7d"), 3) # DoubleQuotedString lexer.setColor(QColor("#38ef7d"), 4) # SingleQuotedString lexer.setColor(QColor("#F6DC74"), 5) # Keyword lexer.setColor(QColor("#38ef7d"), 6) # TripleSingleQuotedString lexer.setColor(QColor("#38ef7d"), 7) # TripleDoubleQuotedString lexer.setColor(QColor("#74F6C3"), 8) # ClassName lexer.setColor(QColor("#FF6666"), 9) # FunctionMethodName lexer.setColor(QColor("magenta"), 10) # Operator lexer.setColor(QColor("white"), 11) # Identifier lexer.setColor(QColor("gray"), 12) # CommentBlock lexer.setColor(QColor("#a8ff78"), 13) # UnclosedString lexer.setColor(QColor("gray"), 14) # HighlightedIdentifier lexer.setColor(QColor("#FF00E7"), 15) # Decorator lexer.setFont(QFont("Iosevka", weight=QFont.Bold), 5) self.setCaretLineBackgroundColor(QColor("#3C3B3F")) self.setLexer(lexer) def setPythonAutocomplete(self): self.autocomplete = QsciAPIs(self.lexer) self.keywords = wordList for word in self.keywords: self.autocomplete.add(word) self.setAutoCompletionThreshold(2) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.updateAutoComplete(self.parent.fileName) self.autocomplete.prepare() def setFold(self): # setup Fold Styles for classes and functions ... x = self.FoldStyle(self.FoldStyle(5)) # self.textPad.folding() if not x: self.foldAll(False) self.setFolding(x) # self.textPad.folding() def unsetFold(self): self.setFolding(0) def updateAutoComplete(self, file_path=None): for i in tokenize(file_path): for j in i: if j not in self.wordlist: self.wordlist.append(j) for word in self.wordlist: self.autocomplete.add(word) self.autocomplete.prepare() def change_col(self, line, column): # Responsible for changing the column bar. self.line = line self.column = column def check_if_func(self, word): # Checks if a word is a built in function word_array = list(word) for wo in word_array: if wo in ["{", "}", "'", '"', "[", "]", "(", ")"]: word_array.remove(wo) for w in funcList: if w == "".join(word_array): return True def check_if_error(self, word): if word in errorList: # This is the list where all possible errors are defined return True def keyReleaseEvent(self, e): if e.key() == Qt.Key_Return: try: self.updateAutoComplete(self.parent.fileName) except AttributeError as E: print(E, "on line 210 in TextEditor.py") if e.key() == Qt.Key_Backspace: pass def mousePressEvent(self, e): super().mousePressEvent(e) if QGuiApplication.queryKeyboardModifiers() == Qt.ControlModifier: word = self.wordAtLineIndex(self.getCursorPosition()[0], self.getCursorPosition()[1]) print(word) if self.check_if_func(word): url = "https://docs.python.org/3/library/functions.html#" + word self.parent.parent.openBrowser( url, word) # Runs the openBrowser function in Main class elif self.check_if_error(word): url = "https://docs.python.org/3/library/exceptions.html#" + word print(url) self.parent.parent.openBrowser(url, word) def keyPressEvent(self, e): if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_F: text, okPressed = QInputDialog.getText(self, "Find", "Find what: ") self.setSelectionBackgroundColor(QColor("#6be585")) if okPressed: if text == "": text = " " self.dialog.noMatch(text) self.searchtext = text """ This is the way to implement a search function using QScintilla http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciScintilla.html#a37ac2bea94eafcfa639173557a821200 """ if self.findFirst(self.searchtext, False, True, False, True, True, -1, -1, True, False): pass else: self.dialog.noMatch(self.searchtext) if e.key() == Qt.Key_F3: self.findNext() self.setSelectionBackgroundColor(QColor("#6be585")) if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_L: self.setCursorPosition(self.line, self.column + 1) return if e.modifiers() == Qt.ControlModifier and e.key() == 77: self.setCursorPosition(self.line + 1, self.column) return if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_J: self.setCursorPosition(self.line, self.column - 1) if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_I: self.setCursorPosition(self.line - 1, self.column) if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_T: self.parent.parent.realterminal() return super().keyPressEvent(e)
class CppEditor(QsciScintilla): ''' classdocs ''' def __init__(self, parent=None, fileName=None, readOnlyFiles=[]): ''' Constructor ''' super(CppEditor, self).__init__(parent) self.parent = parent self.roFiles = readOnlyFiles self.setAcceptDrops(False) # drag&drop is on its parent # Set the default font font = QtGui.QFont() font.setFamily('Courier') font.setFixedPitch(True) font.setPointSize(10) self.setFont(font) self.setMarginsFont(font) # C/C++ lexer self.lexer = QsciLexerCPP(self, True) self.lexer.setDefaultFont(font) self.libraryAPIs = QsciAPIs(QsciLexerCPP(self,True)) self.setLexer(self.lexer) #self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, 'Courier') # Auto-indent self.setTabWidth(4) #self.setIndentationsUseTabs(False) self.setAutoIndent(True) # Current line visible with special background color self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QtGui.QColor("#ffe4e4")) # Enable brace matching self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Enable folding visual- use boxes self.setFolding(QsciScintilla.BoxedTreeFoldStyle) # show line numbers fontmetrics = QtGui.QFontMetrics(font) self.setMarginsFont(font) self.setMarginWidth(0, fontmetrics.width("00000") + 4) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QtGui.QColor("#ccccee")) # not too small self.setMinimumSize(400, 200) # set the length of the string before the editor tries to auto-complete self.setAutoCompletionThreshold(3) # tell the editor we are using a QsciAPI for the auto-completion self.setAutoCompletionSource(QsciScintilla.AcsAPIs) # removed remaining right side characters from the current cursor self.setAutoCompletionReplaceWord(True) # "CTRL+Space" autocomplete self.shortcut_ctrl_space = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Space"), self) self.shortcut_ctrl_space.activated.connect(self.autoCompleteFromAll) if fileName: self.curFile = fileName self.loadFile(fileName) self.isUntitled = False else: self.curFile = PROJECT_NONAME + USER_CODE_EXT self.setText( __default_content__ ) self.isUntitled = True self.updateApiKeywords() self.isModified = False self.textChanged.connect(self.onTextChanged ) def onTextChanged(self): self.isModified = True self.parent.onChildContentChanged() def loadFile(self, fileName): try: self.clear() with open(fileName, 'r') as f: for line in f.readlines(): self.append(line) return True except: QtWidgets.QMessageBox.warning(self, PROJECT_ALIAS, "failed to read %s." % fileName ) return False def saveFile(self, fileName): if fileName.find(' ')>=0: QtWidgets.QMessageBox.warning(self, PROJECT_ALIAS, 'File path "%s" contains space(s). Please save to a valid location.'%fileName) return None try: with open(fileName, 'wt') as f: f.write(self.text()) except: QtWidgets.QMessageBox.warning(self, PROJECT_ALIAS, "Failed to save %s." % fileName ) return None self.curFile = fileName self.isUntitled = False self.isModified = False return fileName def saveAs(self): fileName, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save As", self.curFile, PROJECT_ALIAS + " (*" + USER_CODE_EXT + ");;" + "C source (*.c);;C++ source (*.cpp);;Text File (*.txt);;All files (*.*)" ) if not fileName: return None return self.saveFile(fileName) def save(self): f1 = os.path.abspath(self.curFile) for fname in self.roFiles: if f1 == os.path.abspath(fname): # same file if QtWidgets.QMessageBox.question(self.parent, "Project is read-only", "This project is marked as \"read-only\".\n" + \ "Please click \"Cancel\" and save this to another location.\n\n" + \ "Continue saving the current project anyway?", "OK", "Cancel"): return None #return self.saveAs() return self.saveFile(self.curFile) if self.isUntitled: return self.saveAs() else: return self.saveFile(self.curFile) def currentFile(self): return self.curFile def modified(self): return self.isModified def updateApiKeywords(self): self.libraryAPIs.clear() self.apiKeywords = self.parent.getDefaultKeywords() headerfiles = [] for line in range(self.lines()): txt = str(self.text(line)).strip() if txt.find('int') == 0: # e.g. reached "int main()" break elif txt.find('#include') == 0: txt = ''.join(txt.split()) temp = txt[len('#includes')-1 : ] header = temp[1:-1] # get the header file hfile = os.path.join('libraries', header[:-2], header) if os.path.isfile(hfile): if not (hfile in headerfiles): headerfiles.append( hfile ) if len( headerfiles ): #print 'parsing: ', headerfiles self.apiKeywords += getLibraryKeywords( headerfiles ) #self.apiKeywords = list(set(self.apiKeywords)) # remove duplicates for keyword in self.apiKeywords: self.libraryAPIs.add( keyword ) self.libraryAPIs.prepare() self.lexer.setAPIs(self.libraryAPIs) def insertIncludeDirective(self, library=''): directive = '#include <' + library + '.h>\r\n' insert_pos = 0 found_inc = False for line in range(self.lines()): txt = str(self.text(line)).strip() if txt.find('int') == 0: # e.g. reached "int main()" insert_pos = line - 1 break elif txt.find('#include') == 0: found_inc = True elif found_inc: insert_pos = line break if insert_pos < 0 or insert_pos >= self.lines(): insert_pos = 0 self.insertAt(directive, insert_pos, 0) self.updateApiKeywords()