def __init__(self): super().__init__() self.setEolMode(self.SC_EOL_LF) # 以\n换行 self.setWrapMode(self.WrapWord) # 自动换行。self.WrapWord是父类QsciScintilla的 self.setAutoCompletionSource(self.AcsAll) # 自动补全。对于所有Ascii字符 self.setAutoCompletionCaseSensitivity(False) # 自动补全大小写敏感 self.setAutoCompletionThreshold(1) # 输入多少个字符才弹出补全提示 self.setFolding(True) # 代码可折叠 self.setTabWidth(4) # 设置缩进长度 self.setUtf8(True) # 设置文档的编码格式为 “utf8” self.setIndentationGuides(True) # 设置缩进参考线 self.setAutoIndent(True) # 设置自动缩进 self.setCaretLineVisible(True) self.lexer = LexerPython(self) # 语法高亮显示 self.setLexer(self.lexer) self.setCaretLineVisible(True) # 是否高亮显示光标所在行 self.setCaretLineBackgroundColor(QtGui.QColor(250, 244, 217)) # 光标所在行的底色 self.setIndentationsUseTabs(True) # 设置使用Tabs缩进 # self.setFont(QtGui.QFont('Consolas', 20)) # 设置默认字体 self.setMarginType(0, self.NumberMargin) # 0~4。第0个左边栏显示行号 self.setMarginsBackgroundColor(QtGui.QColor(255, 255, 255)) # 边栏背景颜色 # self.setScrollWidthTracking(True) # self.setScrollWidth(500) self.setMarginWidth(0, 40) # 边栏宽度 self.setAutoIndent(True) # 换行后自动缩进 self.__api = QsciAPIs(self.lexer) for kw in keyword.kwlist: self.__api.add(kw) self.__api.prepare() self.setText("#!/usr/bin/python \n# coding=utf-8\n# version: Python3\n# please edit your code here:\n")
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 __init__(self, language, forPreparation=False, parent=None): """ Constructor @param language language of the APIs object (string) @param forPreparation flag indicating this object is just needed for a preparation process (boolean) @param parent reference to the parent object (QObject) """ super(APIs, self).__init__(parent) self.setObjectName("APIs_{0}".format(language)) self.__inPreparation = False self.__language = language self.__forPreparation = forPreparation self.__lexer = Lexers.getLexer(self.__language) self.__apifiles = Preferences.getEditorAPI(self.__language) self.__apifiles.sort() if self.__lexer is None: self.__apis = None else: self.__apis = QsciAPIs(self.__lexer) self.__apis.apiPreparationFinished.connect( self.__apiPreparationFinished) self.__apis.apiPreparationCancelled.connect( self.__apiPreparationCancelled) self.__apis.apiPreparationStarted.connect( self.__apiPreparationStarted) self.__loadAPIs()
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)
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 __init__(self): self.lexer = QsciLexerPython() self.apis = QsciAPIs(self.lexer) self.lexer.setAPIs(self.apis) self.apis.apiPreparationFinished.connect( self.slotApiPreparationFinished) for fn in API_FILES: ok = self.apis.load(PATH + '/api_raw/' + fn) self.apis.prepare()
def _init_apis(self): """ 加载自定义智能提示文件 Returns: """ self._apis = QsciAPIs(self._lexer) for path in Path(os.path.dirname(__file__)).rglob('*.api'): self._apis.load(str(path.absolute())) self._apis.prepare()
def __init__(self, parent=None): super(panelCodigo, self).__init__(parent) # Definir propiedades graficas fuente = QFont() fuente.setFamily('Roboto') fuente.setFixedPitch(True) fuente.setPointSize(8) self.setFont(fuente) self.setMarginsFont(fuente) fuenteEditor = QFontMetrics(fuente) self.setMarginsFont(fuente) self.setMarginWidth(0, fuenteEditor.width("00000") - 15) self.setMarginLineNumbers(0, True) self.setMarginsBackgroundColor(QColor("#E0E0E0")) self.setMarginSensitivity(1, True) # Ocultar scrollbar inferior self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) # Dimension del panel donde se edita el codigo self.setMinimumSize(600, 360) # funcion(señal) para la linea marcada self.marginClicked.connect(self.asignarBreak) # Icono que se mostrara en la linea marcada self.markerDefine(QsciScintilla.Rectangle, self.CONTROL_LINEA_MARCADA) self.setMarkerBackgroundColor(QColor("#F90000"), self.CONTROL_LINEA_MARCADA) # Dar valor a los caracteres especiales self.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Identificar la linea donde se encuentra la ejecucion self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#00ff22")) # Funciones para identar las lineas del editor de codigo self.setAutoIndent(True) self.setIndentationGuides(True) self.setIndentationsUseTabs(True) self.setIndentationWidth(4) # Resaltado y fuente del lexer self.lexer = QsciLexerPascal() self.lexer.setDefaultFont(fuente) # Autocompletar haciendo uso de la API Qsci api = QsciAPIs(self.lexer) self.palabrasGramatica(api) #flta api.prepare() # Asignar colores a las keywords de la gramatica self.asignarColores() #falta self.setLexer(self.lexer) self.setAutoCompletionThreshold(1) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) # Lineas marcadas self.controlLineasM = []
def _initCodeEdit(self): # 初始化编辑器的工作 self.codeEdit.setUtf8(True) self.codeEdit.linesChanged.connect(self.onLinesChanged) # 行改变 # 代码高亮 self.codeEdit.setLexer(QsciLexerJavaScript(self)) # 自动折叠 self.codeEdit.setMarginType(3, QsciScintilla.SymbolMargin) self.codeEdit.setMarginLineNumbers(3, False) self.codeEdit.setMarginWidth(3, 15) self.codeEdit.setMarginSensitivity(3, True) # 显示行号 #self.codeEdit.setMarginType(0, QsciScintilla.NumberMargin) self.codeEdit.setMarginLineNumbers(0, True) self.onLinesChanged() # 代码提示 sciApi = QsciAPIs(self.codeEdit.lexer()) sciApi.prepare() self.codeEdit.setAutoCompletionSource(QsciScintilla.AcsAll) # 设置源 self.codeEdit.setAutoCompletionCaseSensitivity(True) # 设置自动补全大小写敏感 self.codeEdit.setAutoCompletionThreshold(1) # 设置每输入一个字符就会出现自动补全的提示 # 设置字体 self.codeEdit.setFont(QFont('Consolas', 16)) self.codeEdit.setMarginsFont(self.codeEdit.font()) # 设置编码 self.codeEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, QsciScintilla.SC_CP_UTF8) self.codeEdit.setBraceMatching(QsciScintilla.StrictBraceMatch) # 设置当前行高亮 self.codeEdit.setCaretLineVisible(True) self.codeEdit.setCaretLineBackgroundColor(Qt.lightGray) self.codeEdit.setCaretForegroundColor(Qt.white) # tab # table relative self.codeEdit.setIndentationsUseTabs(True) self.codeEdit.setIndentationWidth(4) self.codeEdit.setTabIndents(True) self.codeEdit.setAutoIndent(True) self.codeEdit.setBackspaceUnindents(True) self.codeEdit.setTabWidth(4) # indentation guides self.codeEdit.setIndentationGuides(True) # folding margin self.codeEdit.setFolding(QsciScintilla.PlainFoldStyle) self.codeEdit.setMarginWidth(2, 12) # 自动换行 self.codeEdit.setWrapMode(QsciScintilla.WrapWord)
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()
class Main(): def __init__(self): self.lexer = QsciLexerPython() self.apis = QsciAPIs(self.lexer) self.lexer.setAPIs(self.apis) self.apis.apiPreparationFinished.connect( self.slotApiPreparationFinished) for fn in API_FILES: ok = self.apis.load(PATH + '/api_raw/' + fn) self.apis.prepare() def slotApiPreparationFinished(self): self.apis.savePrepared(PATH + '/prepared.api') print('Done.') QApplication.quit()
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__(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 __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 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.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)
def lock(self): """ Sets the default properties for the Python lexer. """ # Lexer Initialization lexer = QsciLexerPython(self.ui.code_editor) lexer.setDefaultFont(self.font) self.ui.code_editor.setLexer(lexer) # Auto Completion api = QsciAPIs(lexer) for var in dir(builtins): if not (var[0] == "_"): api.add(var) api.prepare() self.ui.code_editor.setAutoCompletionThreshold(1) self.ui.code_editor.setAutoCompletionSource(QsciScintilla.AcsAPIs) # Auto Indentation self.ui.code_editor.setAutoIndent(True) self.ui.code_editor.setIndentationGuides(True) self.ui.code_editor.setIndentationsUseTabs(True) self.ui.code_editor.setIndentationWidth(4) # Font Settings font_metrics = QFontMetrics(self.font) self.ui.code_editor.setMinimumSize(int(font_metrics.width("0" * 80)), 0)
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()
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.Indicator) self.setMarkerBackgroundColor(theme.Indicator, 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)
def initialize_editor(self): self.editor = QsciScintilla() # self.editor.cursorPositionChanged.connect(self.e) # self.editor.copyAvailable.connect(self.e) # self.editor.indicatorClicked.connect(self.e) # self.editor.indicatorReleased.connect(self.e) # self.editor.linesChanged.connect(self.e) # self.editor.marginClicked.connect(self.e) # self.editor.modificationAttempted.connect(self.e) # self.editor.modificationChanged.connect(self.e) # self.editor.selectionChanged.connect(self.e) # self.editor.textChanged.connect(self.e) # self.editor.userListActivated.connect(self.e) if self.editor.__class__.__name__ == "LineTextWidget": return # When using PySide without QSciScintilla # define the font to use font = QFont() font.setFamily("Consolas") font.setFixedPitch(True) font.setPointSize(10) # the font metrics here will help # building the margin width later fm = QFontMetrics(font) # set the default font of the self.editor # and take the same font for line numbers self.editor.setFont(font) self.editor.setMarginsFont(font) # Line numbers # conventionnaly, margin 0 is for line numbers self.editor.setMarginWidth(0, fm.width("00000") + 5) self.editor.setMarginLineNumbers(0, True) self.editor.setTabWidth(4) # Folding visual : we will use boxes self.editor.setFolding(QsciScintilla.BoxedTreeFoldStyle) self.editor.setAutoIndent(True) # Braces matching self.editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Editing line color self.editor.setCaretLineVisible(True) self.editor.setCaretLineBackgroundColor(QColor("#CDA869")) # Margins colors # line numbers margin self.editor.setMarginsBackgroundColor(QColor("#333333")) self.editor.setMarginsForegroundColor(QColor("#CCCCCC")) # folding margin colors (foreground,background) self.editor.setFoldMarginColors(QColor("#99CC66"), QColor("#333300")) # Choose a lexer self.lexer = QsciLexerPython() self.lexer.setDefaultFont(font) # Set the length of the string before the editor tries to autocomplete # In practise this would be higher than 1 # But its set lower here to make the autocompletion more obvious self.editor.setAutoCompletionThreshold(1) # Tell the editor we are using a QsciAPI for the autocompletion self.editor.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.editor.setLexer(self.lexer) self.editor.setCallTipsStyle(QsciScintilla.CallTipsContext) # self.editor.setCallTipsVisible(0) # Create an API for us to populate with our autocomplete terms self.api = QsciAPIs(self.lexer) # Compile the api for use in the lexer self.api.prepare()
class ScriptTab(QWidget,): autocomplete_lst = [] def __init__(self, parent, name, script="", filemodified=0): QWidget.__init__(self) self.parent = parent self.tabWidget = self.parent.ui.tabWidget self.gridlayout = QGridLayout(self) self._dirty = False self.initialize_editor() self.gridlayout.addWidget(self.editor) self.setAcceptDrops(True) self.filename = name self.filemodified = filemodified if self.is_file(): self.title = os.path.basename(name) # if self.equals_saved(): # self.filemodified = os.path.getmtime(self.filename) else: self.filename = "" self.title = name # Show this file in the self.editor self.editor.setText(script) self.clean_txt = self.saved() self.update_dirty() self.editor.keyPressEvent = self.key_press_event @property def scriptRunner(self): return self.parent.controller.scriptRunner def wheelEvent(self, event): if event.modifiers() == Qt.ControlModifier: if event.delta() > 0: self.editor.zoomIn() else: self.editor.zoomOut() return QWidget.wheelEvent(self, event) def initialize_editor(self): self.editor = QsciScintilla() # self.editor.cursorPositionChanged.connect(self.e) # self.editor.copyAvailable.connect(self.e) # self.editor.indicatorClicked.connect(self.e) # self.editor.indicatorReleased.connect(self.e) # self.editor.linesChanged.connect(self.e) # self.editor.marginClicked.connect(self.e) # self.editor.modificationAttempted.connect(self.e) # self.editor.modificationChanged.connect(self.e) # self.editor.selectionChanged.connect(self.e) # self.editor.textChanged.connect(self.e) # self.editor.userListActivated.connect(self.e) if self.editor.__class__.__name__ == "LineTextWidget": return # When using PySide without QSciScintilla # define the font to use font = QFont() font.setFamily("Consolas") font.setFixedPitch(True) font.setPointSize(10) # the font metrics here will help # building the margin width later fm = QFontMetrics(font) # set the default font of the self.editor # and take the same font for line numbers self.editor.setFont(font) self.editor.setMarginsFont(font) # Line numbers # conventionnaly, margin 0 is for line numbers self.editor.setMarginWidth(0, fm.width("00000") + 5) self.editor.setMarginLineNumbers(0, True) self.editor.setTabWidth(4) # Folding visual : we will use boxes self.editor.setFolding(QsciScintilla.BoxedTreeFoldStyle) self.editor.setAutoIndent(True) # Braces matching self.editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) # Editing line color self.editor.setCaretLineVisible(True) self.editor.setCaretLineBackgroundColor(QColor("#CDA869")) # Margins colors # line numbers margin self.editor.setMarginsBackgroundColor(QColor("#333333")) self.editor.setMarginsForegroundColor(QColor("#CCCCCC")) # folding margin colors (foreground,background) self.editor.setFoldMarginColors(QColor("#99CC66"), QColor("#333300")) # Choose a lexer self.lexer = QsciLexerPython() self.lexer.setDefaultFont(font) # Set the length of the string before the editor tries to autocomplete # In practise this would be higher than 1 # But its set lower here to make the autocompletion more obvious self.editor.setAutoCompletionThreshold(1) # Tell the editor we are using a QsciAPI for the autocompletion self.editor.setAutoCompletionSource(QsciScintilla.AcsAPIs) self.editor.setLexer(self.lexer) self.editor.setCallTipsStyle(QsciScintilla.CallTipsContext) # self.editor.setCallTipsVisible(0) # Create an API for us to populate with our autocomplete terms self.api = QsciAPIs(self.lexer) # Compile the api for use in the lexer self.api.prepare() # def tefocusInEvent(self, event): # self.parent.focusInEvent(event) # return QTextEdit.focusInEvent(self.textEditScript, event) def is_file(self): return os.path.isfile(self.filename) def is_modified(self): if self.is_file(): if self.equals_saved(): self.filemodified = os.path.getmtime(self.filename) return str(os.path.getmtime(self.filename)) != str(self.filemodified) else: return False def saved(self): if self.is_file(): f = open(self.filename) saved = f.read() f.close() return saved.strip() else: return "" def equals_saved(self): curr_lines = self.get_script().strip().splitlines() saved_lines = self.saved().strip().splitlines() if len(curr_lines) != len(saved_lines): return False for cl, sl in zip(curr_lines, saved_lines): if cl.strip() != sl.strip(): return False return True @property def dirty(self): if not self._dirty: self.dirty = self.clean_txt != self.get_script() return self._dirty @dirty.setter def dirty(self, dirty): if dirty is False: self.clean_txt = self.get_script() if self._dirty != dirty: self._dirty = dirty self.filename_changed() def update_dirty(self): return self.dirty def index(self): return self.tabWidget.indexOf(self) def filename_changed(self): index = self.parent.ui.tabWidget.indexOf(self) if self.dirty: self.tabWidget.setTabText(index, "%s*" % self.title) else: self.tabWidget.setTabText(index, self.title) def reload(self): self.set_script(self.parent._load_script(self.filename)) self.filemodified = os.path.getmtime(self.filename) def close(self): while self.dirty is True: # While avoids data loss, in case save operation is aborted if self.filename == "": text = "Save unsaved changes?" else: text = "Save %s?" % self.filename ans = QMessageBox.question(self, 'Save', text, QMessageBox.Yes, QMessageBox.Cancel, QMessageBox.No) if ans == QMessageBox.Cancel: return elif ans == QMessageBox.Yes: self.parent.actionSave(False) elif ans == QMessageBox.No: break self.tabWidget.removeTab(self.index()) def _save(self,): f = open(self.filename, 'w') f.write(self.get_script()) f.close() self.title = os.path.basename(self.filename) self.dirty = False def key_press_event(self, event): self.update_dirty() if self.editor.__class__.__name__ == "LineTextWidget": return self.editor.edit.keyReleaseEvent(event) # When using PySide without QSciScintilla QsciScintilla.keyPressEvent(self.editor, event) linenr, pos_in_line = self.editor.getCursorPosition() line = str(self.editor.text(linenr)[:pos_in_line]) if event.key() == Qt.Key_F1: try: self.parent.show_documentation(getattr(self.scriptRunner, str(self.editor.selectedText()))) except AttributeError: pass if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return: for tip in self.autocomplete_lst: try: if line.endswith(tip[:tip.index('(') ]): self.editor.insert(tip[tip.index('('):]) parent, residual = self.scriptRunner.split_line(line) self.parent.show_documentation(self.scriptRunner.get_function_dict(parent, residual)[residual]) return except ValueError: pass if event.key() < 100 or event.key() in [Qt.Key_Backspace, Qt.Key_Shift]: linenr, pos_in_line = self.editor.getCursorPosition() line = str(self.editor.text(linenr)[:pos_in_line]) lst = self.scriptRunner.get_autocomplete_list(line) if lst is not None: self.api.clear() list(map(self.api.add, lst)) self.api.prepare() if len(lst) > 0: self.autocomplete_lst = lst self.editor.autoCompleteFromAll() shift_event = QKeyEvent(QEvent.KeyPress, Qt.Key_Shift, Qt.NoModifier) QsciScintilla.keyPressEvent(self.editor, shift_event) # show autocomplete list def set_script(self, script): self.editor.setText(script) def get_script(self): return str(self.editor.text()).replace("\r", "").replace(">>> ", "").replace("\t", " ") def dropHandler(self, dataItemList, ctrl, shift, event): pass
def _setup_editor(self): # set default editor settings self.editor.setFont(DEFAULT_FONT) self.editor.setMarginsFont(LINENO_FONT) self.editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) self.editor.setCaretLineVisible(True) self.editor.setCaretForegroundColor(QColor('#000000')) self.editor.setCaretLineBackgroundColor(QColor('#c4e8fd')) self.editor.setAutoIndent(True) self.editor.setTabIndents(True) # tab width self.editor.setTabWidth(4) # line numbers (margin 0) self.editor.setMarginLineNumbers(0, True) self.editor.setMarginWidth(0, '00000') # hide symbol margin self.editor.setMarginWidth(1, 0) # folding self.editor.setFolding(1) # indentation guides self.editor.setIndentationGuides(True) # wrap mode wrap_lines = False self.editor.setWrapMode(wrap_lines) if wrap_lines: # remove horizontal scrollBar self.editor.SendScintilla(SCI.SCI_SETHSCROLLBAR, 0, 0) lexer = QsciLexerPython(self) # apply default settings to lexer lexer.setDefaultFont(DEFAULT_FONT) lexer.setFont(DEFAULT_FONT) # margins lexer.setPaper(MARGIN_BGCOLOR, SCI.STYLE_LINENUMBER) lexer.setColor(MARGIN_COLOR, SCI.STYLE_LINENUMBER) lexer.setFont(DEFAULT_FONT, SCI.STYLE_LINENUMBER) # assign the lexer self.editor.setLexer(lexer) self.editor.SendScintilla(SCI.SCI_COLOURISE, 0, -1) # margins self.editor.setMarginsBackgroundColor(MARGIN_BGCOLOR) self.editor.setMarginsForegroundColor(MARGIN_COLOR) self.editor.setMarginsFont(LINENO_FONT) # folding self.editor.setFolding(FOLDING) self.editor.SendScintilla(SCI.SCI_SETMARGINWIDTHN, FOLD_MARGIN_NUM, FOLD_MARGIN_WIDTH) # set fold margin colors self.editor.SendScintilla(SCI.SCI_SETFOLDMARGINCOLOUR, True, Main._color_to_bgr_int(FOLD_MARGIN_COLOR)) self.editor.SendScintilla(SCI.SCI_SETFOLDMARGINHICOLOUR, True, Main._color_to_bgr_int(FOLD_MARGIN_HICOLOR)) # create and configure the breakpoint column self.editor.setMarginWidth(self.__symbol_margin_num, 17) self.editor.markerDefine(BREAKPOINT_SYMBOL, self.__breakpoint_marker_num) self.editor.setMarginMarkerMask(self.__symbol_margin_num, self.__breakpoint_marker_mask) self.editor.setMarkerBackgroundColor(BREAKPOINT_COLOR, self.__breakpoint_marker_num) # make breakpoint margin clickable self.editor.setMarginSensitivity(self.__symbol_margin_num, True) # add new callback for breakpoints self.editor.marginClicked.connect(self._slot_margin_clicked) # setup active line marker self.editor.markerDefine(ACTIVE_LINE_SYMBOL, self.__active_line_marker_num) self.editor.setMarkerForegroundColor(ACTIVE_LINE_COLOR, self.__active_line_marker_num) self.editor.setMarkerBackgroundColor(ACTIVE_LINE_COLOR, self.__active_line_marker_num) # connect signals self.editor.textChanged.connect(self._slot_text_changed) self.editor.modificationChanged.connect( self._slot_editor_modification_changed) self.editor.SCN_URIDROPPED.connect(self._slot_file_dropped) self.editor.copyAvailable.connect(self.actionCut.setEnabled) self.editor.copyAvailable.connect(self.actionCopy.setEnabled) self.editor.selectionChanged.connect( lambda: self.actionDelete.setEnabled(self.editor.hasSelectedText() )) self.editor.selectionChanged.connect( lambda: self.actionSelectAll.setEnabled(self.editor. hasSelectedText())) # autocomplete if API_FILE is not None: apis = QsciAPIs(self.editor.lexer()) apis.loadPrepared(API_FILE) self.editor.setAutoCompletionThreshold(3) # The source is any installed APIs. self.editor.setAutoCompletionSource(QsciScintilla.AcsAPIs)
def __init__(self, parent=None): #CONSTRUCTOR DE LA CLASE HEREDADA super(editor, self).__init__(parent) #DEFINICION DE LA FUENTE Y SUS PROPIEDADES font = QFont() #font.setFamily('Courier') font.setFixedPitch(True) font.setPointSize(10) self.setFont(font) self.setMarginsFont(font) self.lineas_marcadas = [] #PROFIEDADES AVANZADAS DE LA FUENTE DEL EDITOR fontmetrics = QFontMetrics(font) self.setMarginsFont(font) #SE CAMBIA EL MARGEN ANCHO DE LA FUNETE self.setMarginWidth(0, fontmetrics.width("00000") - 15) #MARGEN DE LOS NUMEROS DE LINEA self.setMarginLineNumbers(0, True) #COLOR DE FONDO DE LOS NUMEROS DE LINEA self.setMarginsBackgroundColor(QColor("#E0E0E0")) self.setMarginSensitivity(1, True) #CREAMOS LA SEÑA PARA AGREGAR LINEA MARCADA self.marginClicked.connect(self.on_margin_clicked) #SE DEFINE EL ICONO A MOSTRAR EN LA LINEA MARCADA self.markerDefine(QsciScintilla.Circle, self.ARROW_MARKER_NUM) self.setMarkerBackgroundColor(QColor("#FF6C3B"), self.ARROW_MARKER_NUM) #RESALTADO DE PARENTECIS,CORCHETES Y OTROS self.setBraceMatching(QsciScintilla.SloppyBraceMatch) #RESALTADO DE LA LINEA DONDE SE ENCUENTRA EL CURSOR self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#32f24c")) #AUTOIDENTACION self.setAutoIndent(True) self.setIndentationGuides(True) self.setIndentationsUseTabs(True) self.setIndentationWidth(4) #DEFINIMOS EL RESALTADO O LEXER self.lexer = QsciLexerPascal() self.lexer.setDefaultFont(font) #FUENTE DEL LEXER #API PARA EL AUTOCOMPETADO api = QsciAPIs(self.lexer) self.palabraAutocompletar(api) api.prepare() self.cambiarColores() self.setLexer(self.lexer) self.setAutoCompletionThreshold(1) self.setAutoCompletionSource(QsciScintilla.AcsAPIs) #ESCONDER SCROLLBAR HORIZONTAL self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) #TAMAÑO MINIMO DEL EDITOR self.setMinimumSize(600, 360)
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 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 } self.lexer = PythonLexer() self.api = None self.has_annotations = False self.setModified(False) self.breakpoint_handles = 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.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. """ 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.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 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']) 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 clean_line.startswith('#'): # It's a comment line, so handle "# " & "#..." as fallback: if clean_line.startswith('# '): return raw_line.replace('# ', '') else: return raw_line.replace('#', '') elif clean_line: # It's a normal line of code. return '# ' + raw_line else: # It's a whitespace line, so just return it. 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].strip() if last_newline.startswith('#'): index_to += 2 # A newly commented line starts... "# " elif last_oldline.startswith('#'): # Check the original line to see what has been uncommented. if last_oldline.startswith('# '): index_to -= 2 # It was "# ". else: index_to -= 1 # It was "#". 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)
def __init__(self, parent=None, lineNumberOn=False): super(highlightEditor, self).__init__() self.editor = QsciScintilla(parent) font = QFont() font.setFamily("Consolas") font.setPointSize(12) font.setFixedPitch(True) self.editor.setFont(font) self.editor.setObjectName("editor") self.editor.setUtf8(True) self.editor.setMarginsFont(font) if lineNumberOn: self.editor.setMarginWidth( 0, len(str(len(self.editor.text().split('\n')))) * 20) self.editor.setMarginLineNumbers(0, lineNumberOn) self.editor.setBraceMatching(QsciScintilla.StrictBraceMatch) self.editor.setIndentationsUseTabs(True) self.editor.setIndentationWidth(4) self.editor.setTabIndents(True) self.editor.setAutoIndent(True) self.editor.setBackspaceUnindents(True) self.editor.setTabWidth(4) self.editor.setCaretLineVisible(True) self.editor.setCaretLineBackgroundColor(QColor('#DCDCDC')) self.editor.setIndentationGuides(True) self.editor.setFolding(QsciScintilla.PlainFoldStyle) self.editor.setMarginWidth(2, 12) self.editor.markerDefine(QsciScintilla.Minus, QsciScintilla.SC_MARKNUM_FOLDEROPEN) self.editor.markerDefine(QsciScintilla.Plus, QsciScintilla.SC_MARKNUM_FOLDER) self.editor.markerDefine(QsciScintilla.Minus, QsciScintilla.SC_MARKNUM_FOLDEROPENMID) self.editor.markerDefine(QsciScintilla.Plus, QsciScintilla.SC_MARKNUM_FOLDEREND) self.editor.setMarkerBackgroundColor( QColor("#FFFFFF"), QsciScintilla.SC_MARKNUM_FOLDEREND) self.editor.setMarkerForegroundColor( QColor("#272727"), QsciScintilla.SC_MARKNUM_FOLDEREND) self.editor.setMarkerBackgroundColor( QColor("#FFFFFF"), QsciScintilla.SC_MARKNUM_FOLDEROPENMID) self.editor.setMarkerForegroundColor( QColor("#272727"), QsciScintilla.SC_MARKNUM_FOLDEROPENMID) self.editor.setAutoCompletionSource(QsciScintilla.AcsAll) self.editor.setAutoCompletionCaseSensitivity(True) self.editor.setAutoCompletionReplaceWord(False) self.editor.setAutoCompletionThreshold(1) self.editor.setAutoCompletionUseSingle(QsciScintilla.AcusExplicit) self.lexer = highlight(self.editor) self.editor.setLexer(self.lexer) self.__api = QsciAPIs(self.lexer) autocompletions = keyword.kwlist + [] for ac in autocompletions: self.__api.add(ac) self.__api.prepare() self.editor.autoCompleteFromAll() self.editor.textChanged.connect(self.changed)
class EditorPane(QsciScintilla): """ Represents the text editor. """ # Signal fired when a script or hex is droped on this editor. open_file = pyqtSignal(str) # Signal fired when a context menu is requested. context_menu = pyqtSignal() 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 contextMenuEvent(self, event): """ A context menu (right click) has been actioned. """ self.context_menu.emit() 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]) margins = { "xs": 30, "s": 35, "m": 45, "l": 50, "xl": 60, "xxl": 75, "xxxl": 85, } # Make the margin left of line numbers follow zoom level self.setMarginWidth(0, margins[size]) # Make margins around debugger-marker follow zoom level self.setMarginWidth(1, margins[size] * 0.25) self.setMarginWidth(4, margins[size] * 0.1) @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)) # Replace CRLF line endings that we add when run on Windows line_content = self.text(line_number).replace("\r\n", "\n") 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 APIs(QObject): """ Class implementing an API storage entity. @signal apiPreparationFinished() emitted after the API preparation has finished @signal apiPreparationCancelled() emitted after the API preparation has been cancelled @signal apiPreparationStarted() emitted after the API preparation has started """ apiPreparationFinished = pyqtSignal() apiPreparationCancelled = pyqtSignal() apiPreparationStarted = pyqtSignal() def __init__(self, language, projectType="", forPreparation=False, parent=None): """ Constructor @param language language of the APIs object @type str @param projectType type of the project @type str @param forPreparation flag indicating this object is just needed for a preparation process @type bool @param parent reference to the parent object @type QObject """ super(APIs, self).__init__(parent) if projectType: self.setObjectName("APIs_{0}_{1}".format(language, projectType)) else: self.setObjectName("APIs_{0}".format(language)) self.__inPreparation = False self.__language = language self.__projectType = projectType self.__forPreparation = forPreparation self.__lexer = Lexers.getLexer(self.__language) self.__apifiles = Preferences.getEditorAPI(self.__language, self.__projectType) self.__apifiles.sort() if self.__lexer is None: self.__apis = None else: self.__apis = QsciAPIs(self.__lexer) self.__apis.apiPreparationFinished.connect( self.__apiPreparationFinished) self.__apis.apiPreparationCancelled.connect( self.__apiPreparationCancelled) self.__apis.apiPreparationStarted.connect( self.__apiPreparationStarted) self.__loadAPIs() def __loadAPIs(self): """ Private method to load the APIs. """ if self.__apis.isPrepared(): # load a prepared API file if (not self.__forPreparation and Preferences.getEditor("AutoPrepareAPIs")): self.prepareAPIs() self.__apis.loadPrepared(self.__preparedName()) else: # load the raw files and prepare the API file if (not self.__forPreparation and Preferences.getEditor("AutoPrepareAPIs")): self.prepareAPIs(ondemand=True) def reloadAPIs(self): """ Public method to reload the API information. """ if (not self.__forPreparation and Preferences.getEditor("AutoPrepareAPIs")): self.prepareAPIs() self.__loadAPIs() def getQsciAPIs(self): """ Public method to get a reference to QsciAPIs object. @return reference to the QsciAPIs object (QsciAPIs) """ if (not self.__forPreparation and Preferences.getEditor("AutoPrepareAPIs")): self.prepareAPIs() return self.__apis def isEmpty(self): """ Public method to check, if the object has API files configured. @return flag indicating no API files have been configured (boolean) """ return len(self.__apifiles) == 0 def __apiPreparationFinished(self): """ Private method called to save an API, after it has been prepared. """ self.__apis.savePrepared(self.__preparedName()) self.__inPreparation = False self.apiPreparationFinished.emit() def __apiPreparationCancelled(self): """ Private method called, after the API preparation process has been cancelled. """ self.__inPreparation = False self.apiPreparationCancelled.emit() def __apiPreparationStarted(self): """ Private method called, when the API preparation process started. """ self.__inPreparation = True self.apiPreparationStarted.emit() def prepareAPIs(self, ondemand=False, rawList=None): """ Public method to prepare the APIs if necessary. @keyparam ondemand flag indicating a requested preparation (boolean) @keyparam rawList list of raw API files (list of strings) """ if self.__apis is None or self.__inPreparation: return needsPreparation = False if ondemand: needsPreparation = True else: # check, if a new preparation is necessary preparedAPIs = self.__preparedName() if preparedAPIs: preparedAPIsInfo = QFileInfo(preparedAPIs) if not preparedAPIsInfo.exists(): needsPreparation = True else: preparedAPIsTime = preparedAPIsInfo.lastModified() apifiles = sorted( Preferences.getEditorAPI(self.__language, self.__projectType)) if self.__apifiles != apifiles: needsPreparation = True for apifile in apifiles: if (QFileInfo(apifile).lastModified() > preparedAPIsTime): needsPreparation = True break if needsPreparation: # do the preparation self.__apis.clear() if rawList: apifiles = rawList else: apifiles = Preferences.getEditorAPI(self.__language, self.__projectType) for apifile in apifiles: self.__apis.load(apifile) self.__apis.prepare() self.__apifiles = apifiles def cancelPreparation(self): """ Public slot to cancel the APIs preparation. """ self.__apis and self.__apis.cancelPreparation() def installedAPIFiles(self): """ Public method to get a list of installed API files. @return list of installed API files (list of strings) """ if self.__apis is not None: if Globals.isWindowsPlatform(): qsciPath = os.path.join(Globals.getPyQt5ModulesDirectory(), "qsci") if os.path.exists(qsciPath): # it's the installer if self.__lexer.lexerName() is not None: apidir = os.path.join(qsciPath, "api", self.__lexer.lexerName()) fnames = [] filist = QDir(apidir).entryInfoList(["*.api"], QDir.Files, QDir.IgnoreCase) for fi in filist: fnames.append(fi.absoluteFilePath()) return fnames else: return [] return self.__apis.installedAPIFiles() else: return [] def __preparedName(self): """ Private method returning the default name of a prepared API file. @return complete filename for the Prepared APIs file (string) """ apisDir = os.path.join(Globals.getConfigDir(), "APIs") if self.__apis is not None: if self.__projectType: filename = "{0}_{1}.pap".format(self.__language, self.__projectType) else: filename = "{0}.pap".format(self.__language) return os.path.join(apisDir, filename) else: return ""
class APIs(QObject): """ Class implementing an API storage entity. @signal apiPreparationFinished() emitted after the API preparation has finished @signal apiPreparationCancelled() emitted after the API preparation has been cancelled @signal apiPreparationStarted() emitted after the API preparation has started """ apiPreparationFinished = pyqtSignal() apiPreparationCancelled = pyqtSignal() apiPreparationStarted = pyqtSignal() def __init__(self, language, forPreparation=False, parent=None): """ Constructor @param language language of the APIs object (string) @param forPreparation flag indicating this object is just needed for a preparation process (boolean) @param parent reference to the parent object (QObject) """ super(APIs, self).__init__(parent) self.setObjectName("APIs_{0}".format(language)) self.__inPreparation = False self.__language = language self.__forPreparation = forPreparation self.__lexer = Lexers.getLexer(self.__language) self.__apifiles = Preferences.getEditorAPI(self.__language) self.__apifiles.sort() if self.__lexer is None: self.__apis = None else: self.__apis = QsciAPIs(self.__lexer) self.__apis.apiPreparationFinished.connect( self.__apiPreparationFinished) self.__apis.apiPreparationCancelled.connect( self.__apiPreparationCancelled) self.__apis.apiPreparationStarted.connect( self.__apiPreparationStarted) self.__loadAPIs() def __loadAPIs(self): """ Private method to load the APIs. """ if self.__apis.isPrepared(): # load a prepared API file if not self.__forPreparation and \ Preferences.getEditor("AutoPrepareAPIs"): self.prepareAPIs() self.__apis.loadPrepared() else: # load the raw files and prepare the API file if not self.__forPreparation and \ Preferences.getEditor("AutoPrepareAPIs"): self.prepareAPIs(ondemand=True) def reloadAPIs(self): """ Public method to reload the API information. """ if not self.__forPreparation and \ Preferences.getEditor("AutoPrepareAPIs"): self.prepareAPIs() self.__loadAPIs() def getQsciAPIs(self): """ Public method to get a reference to QsciAPIs object. @return reference to the QsciAPIs object (QsciAPIs) """ if not self.__forPreparation and \ Preferences.getEditor("AutoPrepareAPIs"): self.prepareAPIs() return self.__apis def isEmpty(self): """ Public method to check, if the object has API files configured. @return flag indicating no API files have been configured (boolean) """ return len(self.__apifiles) == 0 def __apiPreparationFinished(self): """ Private method called to save an API, after it has been prepared. """ self.__apis.savePrepared() self.__inPreparation = False self.apiPreparationFinished.emit() def __apiPreparationCancelled(self): """ Private method called, after the API preparation process has been cancelled. """ self.__inPreparation = False self.apiPreparationCancelled.emit() def __apiPreparationStarted(self): """ Private method called, when the API preparation process started. """ self.__inPreparation = True self.apiPreparationStarted.emit() def prepareAPIs(self, ondemand=False, rawList=None): """ Public method to prepare the APIs if necessary. @keyparam ondemand flag indicating a requested preparation (boolean) @keyparam rawList list of raw API files (list of strings) """ if self.__apis is None or self.__inPreparation: return needsPreparation = False if ondemand: needsPreparation = True else: # check, if a new preparation is necessary preparedAPIs = self.__defaultPreparedName() if preparedAPIs: preparedAPIsInfo = QFileInfo(preparedAPIs) if not preparedAPIsInfo.exists(): needsPreparation = True else: preparedAPIsTime = preparedAPIsInfo.lastModified() apifiles = sorted( Preferences.getEditorAPI(self.__language)) if self.__apifiles != apifiles: needsPreparation = True for apifile in apifiles: if QFileInfo(apifile).lastModified() > \ preparedAPIsTime: needsPreparation = True break if needsPreparation: # do the preparation self.__apis.clear() if rawList: apifiles = rawList else: apifiles = Preferences.getEditorAPI(self.__language) for apifile in apifiles: self.__apis.load(apifile) self.__apis.prepare() self.__apifiles = apifiles def cancelPreparation(self): """ Public slot to cancel the APIs preparation. """ self.__apis and self.__apis.cancelPreparation() def installedAPIFiles(self): """ Public method to get a list of installed API files. @return list of installed API files (list of strings) """ if self.__apis is not None: if Globals.isWindowsPlatform(): qsciPath = os.path.join( Globals.getPyQt5ModulesDirectory(), "qsci") if os.path.exists(qsciPath): # it's the installer if self.__lexer.lexerName() is not None: apidir = os.path.join(qsciPath, "api", self.__lexer.lexerName()) fnames = [] filist = QDir(apidir).entryInfoList( ["*.api"], QDir.Files, QDir.IgnoreCase) for fi in filist: fnames.append(fi.absoluteFilePath()) return fnames else: return [] return self.__apis.installedAPIFiles() else: return [] def __defaultPreparedName(self): """ Private method returning the default name of a prepared API file. @return complete filename for the Prepared APIs file (string) """ if self.__apis is not None: return self.__apis.defaultPreparedName() else: return ""
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') 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)
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 )
class EditorPane(QsciScintilla): """ Represents the text editor. """ def __init__(self, path, text): super().__init__() self.path = path self.setText(text) 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 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(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()
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()
def updateAutoComplete(self, text=None): self.autocomplete = QsciAPIs(self.lexer) # clear all 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__') 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()
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 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.getFont()) 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() self.font.setFamily(c.getFontFamily()) 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.setMarginType(1, QsciScintilla.SymbolMargin) self.setMarginWidth(1, "00") sym = QsciScintilla.Circle self.markerDefine(sym, 1) self.setMarginMarkerMask(0, 0b1111) handle_01 = self.markerAdd(1, 0) # 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")) # not too small 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) 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(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): pass def checkPath(self, path): if '\\' in path: path = path.replace('\\', '/') return path def undoContext(self): self.undo() def redoContext(self): self.redo() def cutContext(self): self.cut() def copyContext(self): self.copy() def pasteContext(self): self.paste() def getContext(self): self.selectAll() 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() 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() self.font.setFamily(c.getFontFamily()) 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() self.font.setFamily('Mono') 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() self.font.setFamily(c.getFontFamily()) 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 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.discart_event = kwargs.get('discart_event', self.discart_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._discart_button = self._form.discart_button self._save_button.clicked[bool].connect(self.on_save_changes) self._discart_button.clicked[bool].connect(self.on_discart_changes) if self._read_only: self._code_editor.setReadOnly(True) self._save_button.setVisible(False) self._discart_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.discart_button.setIcon( QIcon(conf.PYFORMS_ICON_CODEEDITOR_DISCART)) 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') 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._discart_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._discart_button.setEnabled(False) def on_discart_changes(self): if self.discart_event(): self._code_editor.setModified(False) self._save_button.setEnabled(False) self._discart_button.setEnabled(False) def discart_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._discart_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 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()