Esempio n. 1
0
    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)
Esempio n. 2
0
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()
Esempio n. 3
0
class ControlCodeEditor(ControlBase):
    """
	Control that offers a code editor with pretty-print and line numbers and a save button
	"""

    ARROW_MARKER_NUM = 8

    def __init__(self, *args, **kwargs):
        """
		
		:param label: 
		:param default: 
		:param helptext: 
		"""
        self._read_only = kwargs.get('readonly', False)
        self._changed_func = None
        super(ControlCodeEditor, self).__init__(*args, **kwargs)

        self.discard_event = kwargs.get('discard_event', self.discard_event)

    def init_form(self):
        """
		
		"""

        control_path = tools.getFileInSameDirectory(__file__, "code_editor.ui")
        self._form = uic.loadUi(control_path)

        self._code_editor = self._form.code_editor
        self._save_button = self._form.save_button
        self._discard_button = self._form.discard_button

        self._save_button.clicked[bool].connect(self.on_save_changes)
        self._discard_button.clicked[bool].connect(self.on_discard_changes)

        if self._read_only:
            self._code_editor.setReadOnly(True)
            self._save_button.setVisible(False)
            self._discard_button.setVisible(False)

        self.form.font_size.addItem('9')
        self.form.font_size.addItem('10')
        self.form.font_size.addItem('11')
        self.form.font_size.addItem('12')
        self.form.font_size.addItem('14')
        self.form.font_size.addItem('18')
        self.form.font_size.addItem('24')

        # Set the default font size
        index = self.form.font_size.findText(
            conf.PYFORMS_CONTROL_CODE_EDITOR_DEFAULT_FONT_SIZE)
        self.form.font_size.setCurrentIndex(index)

        self.form.font_size.currentIndexChanged.connect(
            self.__font_size_index_changed)

        self.form.save_button.setIcon(QIcon(conf.PYFORMS_ICON_CODEEDITOR_SAVE))
        self.form.discard_button.setIcon(
            QIcon(conf.PYFORMS_ICON_CODEEDITOR_DISCARD))

        self.lexer = QsciLexerPython

        self._code_editor.keyPressEvent = self._key_pressed
        self._changed_func = None

        self.value = self._value
        super(ControlCodeEditor, self).init_form()

    def __font_size_index_changed(self, index):
        item = self.form.font_size.currentText()
        if len(item) >= 1:
            self._load_code_editor_settings()

    def _load_code_editor_settings(self):
        """
		Load settings on the code editor like, font style, margins, scroll, etc.
		Based on the example from http://eli.thegreenplace.net/2011/04/01/sample-using-qscintilla-with-pyqt/
		"""

        item = self.form.font_size.currentText()
        size = int(item)

        # Set the default font
        font = QFont()
        font.setFamily('Courier New')
        font.setFixedPitch(True)
        font.setPointSize(size)
        self._code_editor.setFont(font)
        self._code_editor.setMarginsFont(font)

        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(font)
        self._code_editor.setMarginsFont(font)
        self._code_editor.setMarginWidth(0, fontmetrics.width("00000") + 6)
        self._code_editor.setMarginLineNumbers(0, True)
        self._code_editor.setMarginsBackgroundColor(QColor("#cccccc"))

        # Clickable margin 1 for showing markers
        self._code_editor.setMarginSensitivity(1, True)
        self._code_editor.marginClicked.connect(self.on_margin_clicked)
        self._code_editor.markerDefine(QsciScintilla.RightArrow,
                                       self.ARROW_MARKER_NUM)
        self._code_editor.setMarkerBackgroundColor(QColor("#ee1111"),
                                                   self.ARROW_MARKER_NUM)

        # Detect changes to text
        self._code_editor.modificationChanged.connect(
            self.on_modification_changed)

        # Brace matching: enable for a brace immediately before or after the current position
        self._code_editor.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        # Current line visible with special background color
        self._code_editor.setCaretLineVisible(True)
        self._code_editor.setCaretLineBackgroundColor(QColor("#ffe4e4"))

        # Set Python lexer
        # Set style for Python comments (style number 1) to a fixed-width Courier.
        lexer = self.lexer()
        lexer.setDefaultFont(font)
        self._code_editor.setLexer(lexer)
        self._code_editor.setIndentationWidth(4)
        # self._code_editor.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, 'Courier')
        self._code_editor.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1)

        # Don't want to see the horizontal scrollbar at all
        # Use raw message to Scintilla here (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
        self._code_editor.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)

        self._lexer_obj = lexer
        self.qsci_api = QsciAPIs(self._lexer_obj)
        ## Add autocompletion strings
        self.qsci_api.add("aLongString")
        self.qsci_api.add("aLongerString")
        self.qsci_api.add("aDifferentString")
        self.qsci_api.add("sOmethingElse")
        ## Compile the api for use in the lexer
        self.qsci_api.prepare()
        self._code_editor.setAutoCompletionThreshold(1)
        self._code_editor.setAutoCompletionSource(QsciScintilla.AcsAll)

    # not too small
    # self._code_editor.setMinimumSize(600, 450)

    ###################################################################
    ############ Events ###############################################
    ###################################################################

    def on_margin_clicked(self, nmargin, nline, modifiers):  # pylint: disable=unused-argument
        """
		On margin clicked, toggle marker for the line the margin was clicked on
		:param nmargin:
		:type nmargin:
		:param nline:
		:type nline:
		:param modifiers:
		:type modifiers:
		"""
        if self._code_editor.markersAtLine(nline) != 0:
            self._code_editor.markerDelete(nline, self.ARROW_MARKER_NUM)
        else:
            self._code_editor.markerAdd(nline, self.ARROW_MARKER_NUM)

    def on_modification_changed(self):
        """
		On modification change, re-enable save button
		"""
        if self._changed_func:
            self._save_button.setEnabled(True)
            self._discard_button.setEnabled(True)

    def on_save_changes(self):
        """
		On button save clicked, save changes made on the code editor to file
		"""
        if self.changed_event():
            self._code_editor.setModified(False)
            self._save_button.setEnabled(False)
            self._discard_button.setEnabled(False)

    def on_discard_changes(self):

        if self.discard_event():
            self._code_editor.setModified(False)
            self._save_button.setEnabled(False)
            self._discard_button.setEnabled(False)

    def discard_event(self):

        return True

    def _key_pressed(self, event):
        """
		Handle KeyPressed event
		We only care about CTRL-S in order to save changes
		:param event: key event
		"""
        QsciScintilla.keyPressEvent(self._code_editor, event)
        if event.key() in [QtCore.Qt.Key_S, QtCore.Qt.Key_Save]:
            modifiers = QApplication.keyboardModifiers()
            if modifiers == QtCore.Qt.ControlModifier and self.is_modified:
                logger.debug("Saving...")
                self.on_save_changes()

        self.key_pressed_event(event)

    def key_pressed_event(self, event):
        """
		Override KeyPressed event as you like
		:param event: key event
		"""
        pass

    @property
    def is_modified(self):
        return self._code_editor.isModified()

    ###################################################################
    ############ Properties ###########################################
    ###################################################################

    @property
    def lexer(self):
        return self._lexer

    @lexer.setter
    def lexer(self, value):
        self._lexer = value
        self._load_code_editor_settings()

    @property
    def value(self):
        return self._code_editor.text()

    @value.setter
    def value(self, value):
        if value is not None:
            self._code_editor.setText(str(value))
            self._code_editor.setModified(False)
            self._save_button.setEnabled(False)
            self._discard_button.setEnabled(False)

    @property
    def changed_event(self):
        return self._changed_func if self._changed_func else (lambda: 0)

    @changed_event.setter
    def changed_event(self, value):
        self._changed_func = value
Esempio n. 4
0
class IDEeditor(QsciScintilla):
    r"""
        文本框类
    """
    ARROW_MARKER_NUM = 8
    newFileSignal = pyqtSignal()

    def __init__(self,
                 name,
                 parent=None,
                 parent_tabWidget=None,
                 language='txt',
                 font_content=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        self.setObjectName(name)
        # self.document().setModified(False)
        # self.setWindowModified(False)
        self.setUtf8(True)
        self.setModified(False)
        self.setText('')
        self.filepath = None
        self.language = language
        self.parent_tabw = parent_tabWidget
        self.font_content = font_content if font_content else {
            'font': 'Andale Mono',
            'size': '12'
        }
        self.lxr = None
        self.api = None

        self.setFontSize(font_content)
        #self.SendScintilla()
        #self.replaceSelectedText()

        # IDE settings
        # Brace matching: enable for a brace immediately before or after
        # the current position
        #
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        # Current line visible with special background color
        self.setCaretLineVisible(True)
        self.setCaretLineBackgroundColor(QColor("#ffe4e4"))
        # 自动缩进
        self.setAutoIndent(True)
        self.setTabWidth(4)

        self.setAutoCompletionThreshold(1)
        self.setAutoCompletionSource(self.AcsAll)
        # self.cursorPositionChanged.connect(self.testEvent)
        # Hotspot
        # 5:关键词
        # 8:类名
        # 9:函数名
        # 10:标点符号
        # 11:变量名
        self.SendScintilla(self.SCI_STYLESETHOTSPOT, 8, True)
        self.SendScintilla(self.SCI_STYLESETHOTSPOT, 9, True)
        self.SendScintilla(self.SCI_STYLESETHOTSPOT, 11, True)
        self.SCN_HOTSPOTCLICK.connect(self.hotspot_clicked)

    def hotspot_clicked(self, position, modifiers):
        r"""
        点击hotspot触发事件
        :return:
        """
        print(position)
        QTimer.singleShot(
            100,
            functools.partial(self.hotspot_clicked_delayed, position,
                              modifiers))

    def hotspot_clicked_delayed(self, position, modifiers):
        start = self.SendScintilla(self.SCI_WORDSTARTPOSITION, position)
        end = self.SendScintilla(self.SCI_WORDENDPOSITION, position)
        text = self.text(start, end)  # [start:end]
        click_line = self.SendScintilla(self.SCI_LINEFROMPOSITION,
                                        position) + 1
        click_column = self.SendScintilla(self.SCI_GETCOLUMN, position) + 1
        new_row, new_col = goto_definition(text,
                                           click_line,
                                           click_column,
                                           contents=self.text())
        if new_row is not None and new_col is not None:
            self.setCursorPosition(new_row - 1, new_col - 1)
        print(text)

    def keyPressEvent(self, e):
        r"""
            监测文件内容是否修改,若修改则在tab中文件名末尾
            添加一个 '*'
        :param e:
        :return:
        """
        super().keyPressEvent(e)
        index = self.parent_tabw.currentIndex()
        tabtext = self.parent_tabw.tabText(index)
        if not tabtext.startswith('*') and self.isModified():
            self.parent_tabw.setTabText(index, '*' + tabtext)
        if not self.isModified() and tabtext.startswith('*'):
            self.parent_tabw.setTabText(index, tabtext[1:])

    def setlanguage(self, language):
        r"""
            改变语言
        :param language:
        :return:
        """
        self.set_lexer(language)
        self.language = language

    def set_lexer(self, language):
        r"""
            多语法代码高亮
        :param language:
        :return:
        """
        font = self.font_content['font']
        size = int(self.font_content['size'])
        lexer_font = QFont(font, size)
        if language == 'py':
            self.lxr = QsciLexerPython()
            self.lxr.setFont(lexer_font)
            self.setLexer(self.lxr)
            self.__pythonCompletion()

        elif language == 'c':
            self.lxr = QsciLexerCPP()
            self.lxr.setFont(lexer_font)
            self.setLexer(self.lxr)
            self.__cCompletion()
        elif language == 'md':
            self.lxr = QsciLexerMarkdown()
            self.lxr.setFont(lexer_font)
            self.setLexer(self.lxr)
        else:
            self.setLexer(None)
            self.setText(self.text())

    def __pythonCompletion(self):
        r"""
            python 自动补全
        :return:
        """
        python_keywords = [
            "False", "None", "True", "and", "as", "assert", "break", "class",
            "continue", "def", "del", "elif", "else", "except", "finally",
            "for", "from", "global", "if", "import", "in", "is", "isinstance",
            "print", "len", "range", "enumerate", "input", "int", "float",
            "bool", "lambda", "nonlocal", "not", "or", "pass", "raise",
            "return", "try", "while", "with", "yield", "next", "iter"
        ]
        try:
            if isinstance(self.api, QsciAPIs):
                del self.api
        except:
            pass
        self.api = QsciAPIs(self.lxr)
        for kw in python_keywords:
            self.api.add(kw)
        self.api.prepare()
        # self.api.add('class')
        # import PyQt5
        # pyqt_path = os.path.dirname(PyQt5.__file__)
        # self.api.load(os.path.join(pyqt_path, "Qt/qsci/api/python/Python-3.6.api"))

        # self.api.prepare()
        # print('OK')

    def __cCompletion(self):
        r"""
            C自动补全
        :return:
        """
        c_keywords = [
            "char", "double", "enum", "float", "int", "long", "short",
            "signed", "struct", "union", "unsigned", "void", "for", "do",
            "while", "break", "continue", "if", "else", "goto", "switch",
            "case", "default", "return", "auto", "extern", "register",
            "static", "const", "sizeof", "typedef", "volatile"
        ]
        try:
            if isinstance(self.api, QsciAPIs):
                del self.api
        except:
            pass
        self.api = QsciAPIs(self.lxr)
        for kw in c_keywords:
            self.api.add(kw)
        self.api.prepare()

    def setFontSize(self, font_content):
        r"""
            修改字体大小和样式
        :param fontSize:
        :return:
        """
        # self.setStyleSheet(f"font: {fontSize}pt'.AppleSystemUIFont';")
        self.font_content = font_content
        font = font_content['font']
        size = int(font_content['size'])
        qfont = QFont(font, size)

        self.setFont(qfont)
        self.set_lexer(self.language)
        self.codeRow()

    def codeRow(self):
        r"""
            显示代码行数
        :return:
        """
        font_categoty = self.font_content['font']
        size = int(self.font_content['size'])
        font = QFont(font_categoty, size)

        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(font)
        self.setMarginsFont(font)
        self.setMarginWidth(0, fontmetrics.width("00000") + 6)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#cccccc"))
        # Clickable margin 1 for showing markers
        self.setMarginSensitivity(1, True)
        self.marginClicked.connect(self.on_margin_clicked)
        self.markerDefine(QsciScintilla.RightArrow, self.ARROW_MARKER_NUM)
        self.setMarkerBackgroundColor(QColor("#ee1111"), self.ARROW_MARKER_NUM)
        # 取消显示横向bar
        # self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)

    def on_margin_clicked(self, nmargin, nline, modifiers):
        # Toggle marker for the line the margin was clicked on
        if self.markersAtLine(nline) != 0:
            self.markerDelete(nline, self.ARROW_MARKER_NUM)
        else:
            self.markerAdd(nline, self.ARROW_MARKER_NUM)

    def load(self, file_path, mapping=None):
        r"""
        读取文件
        :param file_path: 文件路径
        :return: None
        """
        text = ''
        try:
            """读文件"""
            with open(file_path, 'r', encoding='utf-8') as f:
                for line in f.readlines():
                    text += line
                if mapping is not None or file_path.startswith('.tmp'):
                    self.filepath = mapping
                else:
                    self.filepath = file_path
            # self.setPlainText(text)
            self.setText(text)
            # 设置当前文件名
            _, tmpfilename = os.path.split(file_path)
            self.setObjectName(tmpfilename)
            # 设置语言
            _, prefix = os.path.splitext(tmpfilename)
            self.setlanguage(prefix[1:])
            # 是否清除改变
            if tmpfilename.startswith('*'):
                self.setModified(True)
            else:
                self.setModified(False)
        except FileNotFoundError:
            """弹出窗口,提示文件不存在"""
            QMessageBox.warning(self, 'Warning', 'Text does not exist!')

    def save(self, file_path=None):
        r"""
        保存
        :return:
        """
        if self.filepath is not None or file_path:
            if file_path:
                save_path = file_path
            else:
                save_path = self.filepath
            with open(save_path, 'w', encoding='utf-8') as f:
                text = self.text()
                f.writelines(text)
            # self.document().setModified(False)
            # 把 '*' 去掉
            index = self.parent_tabw.currentIndex()
            tabtext = self.parent_tabw.tabText(index)
            if tabtext.startswith('*'):
                self.parent_tabw.setTabText(index, tabtext[1:])
            self.setModified(False)
            return False
        else:
            self.saveas()
            return True

    def saveas(self):
        r"""
        另存为
        :return:
        """
        file_path, _ = QFileDialog.getSaveFileName(self, 'Save As')
        if len(file_path):
            """如果路径不为空,则保存"""
            self.filepath = file_path
            with open(file_path, 'w', encoding='utf-8') as f:
                text = self.text()
                f.writelines(text)
            # self.document().setModified(False)
            # 路径
            self.filepath = file_path
            # 设置当前文件名
            _, tmpfilename = os.path.split(file_path)
            self.setObjectName(tmpfilename)
            # 设置语言
            _, prefix = os.path.splitext(tmpfilename)
            self.setlanguage(prefix[1:])
            self.setModified(False)
            index = self.parent_tabw.currentIndex()
            self.parent_tabw.setTabText(index, tmpfilename)
            self.newFileSignal.emit()
            return True
        else:
            # QMessageBox.warning(self, 'Warning', 'File name should not be empty')
            return False

    def closeText(self):
        self.close()
Esempio n. 5
0
 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()
Esempio n. 6
0
class PMBaseEditor(QWidget, Ui_FormEditor):
    def __init__(self, parent=None, comment_string: str = '//'):
        super(PMBaseEditor, self).__init__(parent=parent)
        self._parent = self.parent()
        self.setupUi(self)

        self._lexer = None
        self._apis = None
        self._path = ''
        self._extension_names: typing.List[str] = []
        self._encoding = 'utf-8'
        self._action_format = None  # 格式化
        self._action_run_sel_code = None  # 运行选中代码
        self._action_run_code = None  # 运行代码
        self._shortcut_format = None
        self._shortcut_run = None
        self._shortcut_run_sel = None
        self._shortcut_goto = None
        self._indicator_error = -1
        self._indicator_error2 = -1
        self._indicator_warn = -1
        self._indicator_info = -1
        self._indicator_dict = {}  # 指示器记录
        self._smart_autocomp_on = True
        # 自定义属性用于控制QSS设置
        self._theme = 'tomorrow'

        # 代码检测后详情提示颜色
        self.fc_red = QColor(255, 23, 23)
        self.bc_red = QColor(255, 240, 240)
        self.fc_yellow = QColor(191, 153, 36)
        self.bc_yellow = QColor(255, 255, 240)
        self.fc_black = QColor(0, 0, 0)
        self.bc_black = QColor(239, 239, 239)
        self.fc_purple = QColor(197, 67, 153)
        self.bc_purple = QColor(255, 240, 255)

        self.extension_lib = None
        self.find_dialog: 'FindDialog' = None

        self.commenter = Commenter(self.textEdit, comment_string=comment_string)

    def update_settings(self, settings: typing.Dict[str, object]):
        wrap = settings['wrap']
        if wrap:
            self.textEdit.setWrapMode(QsciScintilla.WrapWord)
        else:
            self.textEdit.setWrapMode(QsciScintilla.WrapNone)
        self.set_smart_autocomp_stat(settings['smart_autocomp_on'])

    def set_smart_autocomp_stat(self, autocomp_on: bool) -> None:
        self._smart_autocomp_on = autocomp_on
        # self.textEdit.setAutoCompletionThreshold(0)

    def search_word(self, text_to_find: str, wrap: bool, regex: bool, case_sensitive: bool, whole_word: bool,
                    forward: bool, index=-1, line=-1, **kwargs):

        return self.textEdit.findFirst(text_to_find, regex, case_sensitive, whole_word, wrap, forward, line, index)

    def slot_cursor_position_changed(self, line: int, column: int) -> None:
        """
        光标变化槽函数

        :param line: 行
        :param column: 列
        :type line: int
        :type column: int
        :return: None
        """
        self.label_status_ln_col.setText(
            self.tr('Ln:{0}  Col:{1}').format(format(line + 1, ','), format(column + 1, ',')))

    def on_textedit_focusin(self, e):
        QsciScintilla.focusInEvent(self.textEdit, e)
        self.extension_lib.UI.switch_toolbar('code_editor_toolbar', switch_only=True)

    def _init_apis(self) -> None:
        """
        加载自定义智能提示文件

        :return: None
        """
        self._apis = QsciAPIs(self._lexer)
        # for path in Path(os.path.join(os.path.dirname(__file__), 'api')).rglob('*.api'):
        #     logger.info('load %s' % str(path.absolute()))
        #     self._apis.load(str(path.absolute()))
        try:
            # 添加额外关键词
            for word in self._parent.keywords():
                self._apis.add(word)
        except Exception as e:
            logger.warning(str(e))
        self._apis.prepare()

    def _init_editor(self) -> None:
        """
        初始化编辑器设置

        :return: None
        """
        self.label_status_ln_col.setText(self.tr('Ln:1  Col:1'))
        self.label_status_length.setText(self.tr('Length:0  Lines:1'))
        self.label_status_sel.setText(self.tr('Sel:0 | 0'))
        self.textEdit.setContextMenuPolicy(Qt.CustomContextMenu)
        # 设置字体
        self.textEdit.setFont(QFont('Source Code Pro', 12))  # Consolas
        self.textEdit.setMarginsFont(self.textEdit.font())
        # 自动换行
        self.textEdit.setEolMode(QsciScintilla.EolUnix)  # \n换行
        self.textEdit.setWrapMode(QsciScintilla.WrapWord)  # 自动换行
        self.textEdit.setWrapVisualFlags(QsciScintilla.WrapFlagNone)
        self.textEdit.setWrapIndentMode(QsciScintilla.WrapIndentFixed)
        # 编码
        self.textEdit.setUtf8(True)
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, QsciScintilla.SC_CP_UTF8)
        # 自动提示
        self.textEdit.setAnnotationDisplay(QsciScintilla.AnnotationBoxed)  # 提示显示方式
        self.textEdit.setAutoCompletionSource(QsciScintilla.AcsAll)  # 自动补全。对于所有Ascii字符
        self.textEdit.setAutoCompletionReplaceWord(True)
        self.textEdit.setAutoCompletionCaseSensitivity(False)  # 忽略大小写

        # self.textEdit.setAutoCompletionFillupsEnabled(True)
        self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusNever)
        # self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusAlways)
        # self.textEdit.setAutoCompletionUseSingle(QsciScintilla.AcusExplicit)
        self.textEdit.setAutoCompletionThreshold(1)  # 输入多少个字符才弹出补全提示
        # QsciScintilla.setAutoCompletionUseSingle()
        self.textEdit.setCallTipsPosition(QsciScintilla.CallTipsBelowText)  # 设置提示位置
        self.textEdit.setCallTipsStyle(QsciScintilla.CallTipsNoContext)  # 设置提示样式
        # 设置折叠样式
        self.textEdit.setFolding(QsciScintilla.FoldStyle.BoxedTreeFoldStyle)  # 代码折叠
        # self.textEdit.setFoldMarginColors(QColor(233, 233, 233), Qt.white)
        # 折叠标签颜色
        # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERSUB,
        #                             QColor('0xa0a0a0'))
        # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERMIDTAIL,
        #                             QColor('0xa0a0a0'))
        # self.textEdit.SendScintilla(QsciScintilla.SCI_MARKERSETBACK, QsciScintilla.SC_MARKNUM_FOLDERTAIL,
        #                             QColor('0xa0a0a0'))
        # 设置当前行背景
        self.textEdit.setCaretLineVisible(True)
        # self.textEdit.setCaretLineBackgroundColor(QColor(232, 232, 255))

        # 设置选中文本颜色
        # self.textEdit.setSelectionForegroundColor(QColor(192, 192, 192))
        # self.textEdit.setSelectionBackgroundColor(QColor(192, 192, 192))

        # 括号匹配
        self.textEdit.setBraceMatching(QsciScintilla.StrictBraceMatch)  # 大括号严格匹配
        # self.textEdit.setMatchedBraceBackgroundColor(Qt.blue)
        # self.textEdit.setMatchedBraceForegroundColor(Qt.white)
        # self.textEdit.setUnmatchedBraceBackgroundColor(Qt.red)
        # self.textEdit.setUnmatchedBraceForegroundColor(Qt.white)

        # 启用活动热点区域的下划线
        self.textEdit.setHotspotUnderline(True)
        self.textEdit.setHotspotWrap(True)

        # 缩进
        self.textEdit.setAutoIndent(True)  # 换行后自动缩进
        self.textEdit.setTabWidth(4)
        self.textEdit.setIndentationWidth(4)
        self.textEdit.setTabIndents(True)

        # 缩进指南
        self.textEdit.setIndentationGuides(True)
        self.textEdit.setIndentationsUseTabs(False)  # 不使用Tab
        self.textEdit.setBackspaceUnindents(True)  # 当一行没有其它字符时删除前面的缩进
        # self.textEdit.setIndentationGuidesForegroundColor(QColor(192, 192, 192))
        # self.textEdit.setIndentationGuidesBackgroundColor(Qt.white)

        # 显示行号
        self.textEdit.setMarginLineNumbers(0, True)
        self.textEdit.setMarginWidth(0, 50)
        self.textEdit.setMarginWidth(1, 0)  # 行号
        #  self.textEdit.setMarginWidth(2, 0)  # 折叠
        self.textEdit.setMarginWidth(3, 0)
        self.textEdit.setMarginWidth(4, 0)
        #  # 折叠区域
        #  self.textEdit.setMarginType(3, QsciScintilla.SymbolMargin)
        #  self.textEdit.setMarginLineNumbers(3, False)
        #  self.textEdit.setMarginWidth(3, 15)
        #  self.textEdit.setMarginSensitivity(3, True)

        # 设置空白字符显示
        self.textEdit.setWhitespaceSize(1)  # 可见的空白点的尺寸
        self.textEdit.setWhitespaceVisibility(QsciScintilla.WsVisible)  # 空白的可见性。默认的是空格是无形的
        # self.textEdit.setWhitespaceForegroundColor(QColor(255, 181, 106))

        # 设置右边边界线
        self.textEdit.setEdgeColumn(120)
        self.textEdit.setEdgeMode(QsciScintilla.EdgeLine)

        # 设置代码检测后波浪线
        self._indicator_error = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator)
        self._indicator_error2 = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator)
        self._indicator_warn = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator)
        self._indicator_info = self.textEdit.indicatorDefine(QsciScintilla.SquigglePixmapIndicator)
        self.textEdit.setIndicatorForegroundColor(QColor(Qt.red), self._indicator_error)
        self.textEdit.setIndicatorForegroundColor(QColor(Qt.red), self._indicator_error2)
        self.textEdit.setIndicatorForegroundColor(QColor(244, 152, 16), self._indicator_warn)
        self.textEdit.setIndicatorForegroundColor(QColor(Qt.green), self._indicator_info)

        # 鼠标跟踪
        # self.textEdit.viewport().setMouseTracking(True)
        # # 安装键盘过滤器
        # self.textEdit.installEventFilter(self)
        # 安装鼠标移动过滤器
        self.textEdit.viewport().installEventFilter(self)

    def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool:
        if event.type() == QEvent.ToolTip:
            # 如果有错误则显示详情
            line = self.textEdit.lineAt(event.pos())
            if line >= 0 and line in self._indicator_dict:
                text = self._indicator_dict.get(line, '')
                if text:
                    color = self.textEdit.lexer().paper(0)
                    QToolTip.showText(QCursor.pos(),
                                      '<html><head/><body><div style="background:{0};">{1}</div></body></html>'.format(
                                          color.name(), text), self)
        return False

    def indent(self):

        sel = self.textEdit.getSelection()
        if sel[0] == sel[3]:
            row = self.textEdit.getCursorPosition()[0]
            self.textEdit.indent(row)
        else:
            ke = QKeyEvent(QEvent.KeyPress, Qt.Key_Tab, Qt.NoModifier)
            self.textEdit.keyPressEvent(ke)

    def unindent(self):
        """
        取消缩进。
        方式就是注入一个tab快捷键。
        :return:
        """
        sel = self.textEdit.getSelection()
        if sel[0] == sel[3]:
            row = self.textEdit.getCursorPosition()[0]
            self.textEdit.unindent(row)
        else:
            ke = QKeyEvent(QEvent.KeyPress, Qt.Key_Backtab, Qt.NoModifier)
            self.textEdit.keyPressEvent(ke)

    def _init_lexer(self, lexer: 'QsciLexer') -> None:
        """
        初始化语法解析器

        :return: None
        """
        self._lexer = lexer
        self._lexer.setFont(self.textEdit.font())
        self.textEdit.setLexer(self._lexer)

    def _init_signals(self) -> None:
        """
        初始化信号绑定

        :return: None
        """

        # 绑定获得焦点信号
        self.textEdit.focusInEvent = self.on_textedit_focusin
        # 绑定光标变化信号
        self.textEdit.cursorPositionChanged.connect(self.slot_cursor_position_changed)
        # 绑定内容改变信号
        self.textEdit.textChanged.connect(self.slot_text_changed)
        # 绑定选中变化信号
        self.textEdit.selectionChanged.connect(self.slot_selection_changed)
        # 绑定是否被修改信号
        self.textEdit.modificationChanged.connect(self.slot_modification_changed)
        # 绑定右键菜单信号
        self.textEdit.customContextMenuRequested.connect(self.slot_custom_context_menu_requested)
        # 绑定快捷键信号
        self._action_format.triggered.connect(self.slot_code_format)
        self._shortcut_format.activated.connect(self.slot_code_format)
        self._action_run_code.triggered.connect(self.slot_code_run)
        self._shortcut_run.activated.connect(self.slot_code_run)
        self._action_run_sel_code.triggered.connect(self.slot_code_sel_run)
        self._shortcut_run_sel.activated.connect(self.slot_code_sel_run)

        self._shortcut_save.activated.connect(self.slot_save)
        self._action_save.triggered.connect(self.slot_save)

        self._action_find_replace.triggered.connect(self.slot_find_or_replace)
        self._shortcut_find_replace.activated.connect(self.slot_find_or_replace)

        self._action_autocomp.triggered.connect(self.autocomp)
        self._shortcut_autocomp.activated.connect(self.autocomp)

        self._shortcut_goto.activated.connect(self.slot_goto_line)

    def autocomp(self):
        logger.warning('Manual Autocompletion Triggered!')

    def get_word_under_cursor(self):
        pos = self.textEdit.getCursorPosition()
        text = self.textEdit.text(pos[0])
        try:
            line = text[:pos[1] + 1]
        except Exception as e:
            logger.debug(e)
            line = ''
        word: str = re.split(r'[;,:/ .\\!&\|\*\+-=\s\(\)\{\}\[\]]', line)[-1].strip()
        col = pos[1]
        while (1):
            col += 1
            if col > len(text) - 1:
                break
            char = text[col]
            if char in ' \n()[]{}\'\";:\t!+-*/\\=.':
                break
            word += char
        return word

    def current_line_text(self):
        current_row = self.textEdit.getCursorPosition()[0]
        current_len = self.textEdit.lineLength(current_row)
        self.textEdit.setSelection(current_row, 0, current_row, current_len)
        return self.text(True)

    def text(self, selected: bool = False) -> str:
        """
        返回编辑器选中或者全部内容

        :rtype: str
        :return: 返回编辑器选中或者全部内容
        """
        if selected:
            return self.textEdit.selectedText()
        return self.textEdit.text()

    def set_text(self, text: str) -> None:
        """
        设置编辑器内容

        :type text: str
        :param text: 文本内容
        :return: None
        """
        # self.textEdit.setText(text)  # 该方法会重置撤销历史
        try:
            text = text.encode(self._encoding)
        except Exception as e:
            logger.warning(str(e))
            text = text.encode('utf-8', errors='ignore')
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETTEXT, text)

    def filename(self) -> str:
        """
        返回当前文件名

        :rtype: str
        :return: 返回当前文件名
        """
        return os.path.basename(self._path)

    def path(self) -> str:
        """
        返回当前文件路径

        :rtype: str
        :return: 返回当前文件路径
        """
        return self._path

    def set_path(self, path: str) -> None:
        """
        设置文件路径

        :param path: 设置文件路径
        :type path: str
        :return: None
        """
        self._path = path

    def modified(self) -> bool:
        """
        返回内容是否被修改

        :rtype: bool
        :return: 返回内容是否被修改
        """
        return self.textEdit.isModified()

    def set_modified(self, modified: bool) -> None:
        """
        设置内容是否被修改

        :param modified: 是否被修改 True or False
        :type: bool
        :return: None
        """
        self.textEdit.setModified(modified)

    def load_file(self, path: str) -> None:
        """
        加载文件

        :param path: 文件路径
        :type path: str
        :return: None
        """
        self._path = ''
        try:
            # 读取文件内容并加载
            with open(path, 'rb') as fp:
                text = fp.read()
                text, coding = Utilities.decode(text)
                self.set_encoding(coding)
                self.set_text(text)
                self.set_modified(False)
                self.set_eol_status()
        except Exception as e:
            logger.warning(str(e))
            self.extension_lib.show_log('error', 'CodeEditor', str(e))
        self._path = path
        self.setWindowTitle(self.filename())

    def set_encoding(self, encoding: str):
        """
        设置文本编码,仅支持 ASCII 和 UTF-8

        :param encoding: ascii or gbk or utf-8
        :type: str
        :return:
        """
        encoding = encoding.lower()
        self._encoding = encoding
        self.label_status_encoding.setText(encoding.upper())
        if encoding.startswith('utf'):
            self.textEdit.setUtf8(True)
            self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, QsciScintilla.SC_CP_UTF8)
        else:
            self.textEdit.setUtf8(False)
            self.textEdit.SendScintilla(QsciScintilla.SCI_SETCODEPAGE, 936)

    def slot_find_or_replace(self):
        if self.find_dialog is None:
            self.find_dialog = FindDialog(parent=self, text_edit=self)
        self.find_dialog.show()
        return
        # match_regex = False
        # case_sensitive = False
        # match_whole_word = False
        # wrap_find = False
        #
        # first = self.textEdit.findFirst('def', False, False, False, False)
        # self.textEdit.replace('ggg')

    def slot_about_close(self, save_all=False) -> QMessageBox.StandardButton:
        """
        是否需要关闭以及保存

        :param save_all: 当整个窗口关闭时增加是否全部关闭
        :return:QMessageBox.StandardButton
        """
        if not self.modified():
            return QMessageBox.Discard
        buttons = QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
        if save_all:
            buttons |= QMessageBox.SaveAll  # 保存全部
            buttons |= QMessageBox.NoToAll  # 放弃所有
        ret = QMessageBox.question(self, self.tr('Save'), self.tr('Save file "{0}"?').format(self.filename()), buttons,
                                   QMessageBox.Save)
        if ret == QMessageBox.Save or ret == QMessageBox.SaveAll:
            if not self.slot_save():
                return QMessageBox.Cancel
        return ret

    def slot_modification_changed(self, modified: bool) -> None:
        """
        内容被修改槽函数

        :param modified: 是否被修改
        :type modified: bool
        :return:
        """
        title = self.windowTitle()
        if modified:
            if not title.startswith('*'):
                self.setWindowTitle('*' + title)
        else:
            if title.startswith('*'):
                self.setWindowTitle(title[1:])

    def create_context_menu(self) -> 'QMenu':
        menu = self.textEdit.createStandardContextMenu()

        # 遍历本身已有的菜单项做翻译处理
        # 前提是要加载了Qt自带的翻译文件
        for action in menu.actions():
            action.setText(QCoreApplication.translate('QTextControl', action.text()))
        # 添加额外菜单
        menu.addSeparator()
        menu.addAction(self._action_format)
        menu.addAction(self._action_run_code)
        menu.addAction(self._action_run_sel_code)
        menu.addAction(self._action_save)
        menu.addAction(self._action_find_replace)
        # menu.addAction(self)
        return menu

    def slot_custom_context_menu_requested(self, pos: QPoint) -> None:
        """
        右键菜单修改

        :param pos:
        :type pos: QPoint
        :return: None
        """
        menu = self.create_context_menu()
        # 根据条件决定菜单是否可用
        enabled = len(self.text().strip()) > 0
        self._action_format.setEnabled(enabled)
        self._action_run_code.setEnabled(enabled)
        # self._action_run_sel_code.setEnabled(self.textEdit.hasSelectedText())
        self._action_run_sel_code.setEnabled(enabled)
        menu.exec_(self.textEdit.mapToGlobal(pos))
        del menu

    def slot_save(self) -> bool:
        """
        保存时触发的事件。
        :return:
        """
        return self.save()

    def slot_text_changed(self) -> None:
        self.label_status_length.setText(self.tr('Length:{0}  Lines:{1}').format(format(self.textEdit.length(), ','),
                                                                                 format(self.textEdit.lines(), ',')))

        self.slot_modification_changed(True)
        self.set_modified(True)

    def save(self):
        """
        保存文件时调用的方法
        :param ext_name:
        :return:
        """
        path = self._path.replace(os.sep, '/')
        if path.startswith(QDir.tempPath().replace(os.sep, '/')):
            # 弹出对话框要求选择真实路径保存
            path, ext = QFileDialog.getSaveFileName(self, self.tr('Save file'),
                                                    self.extension_lib.Program.get_work_dir(),
                                                    filter='*.py')

            if not path:
                return False
            if not path.endswith('.py'):
                path += '.py'
            self._path = path
        try:
            with open(self._path, 'wb') as fp:
                fp.write(self.text().encode('utf-8', errors='ignore'))

            self.setWindowTitle(os.path.basename(path))
            self.slot_modification_changed(False)
            self.set_modified(False)
            return True
        except Exception as e:
            # 保存失败
            logger.warning(str(e))
        return False

    def set_eol_status(self):
        """
        根据文件内容中的换行符设置底部状态

        :return:
        """
        eols = re.findall(r'\r\n|\r|\n', self.text())
        if not eols:
            self.label_status_eol.setText('Unix(LF)')
            self.textEdit.setEolMode(QsciScintilla.EolUnix)  # \n换行
            return
        grouped = [(len(list(group)), key) for key, group in groupby(sorted(eols))]
        eol = sorted(grouped, reverse=True)[0][1]
        if eol == '\r\n':
            self.label_status_eol.setText('Windows(CR LF)')
            self.textEdit.setEolMode(QsciScintilla.EolWindows)  # \r\n换行
            return QsciScintilla.EolWindows
        if eol == '\r':
            self.label_status_eol.setText('Mac(CR)')
            self.textEdit.setEolMode(QsciScintilla.EolMac)  # \r换行
            return
        self.label_status_eol.setText('Unix(LF)')
        self.textEdit.setEolMode(QsciScintilla.EolUnix)  # \n换行

    def _init_actions(self) -> None:
        """
        初始化额外菜单项

        :return:
        """
        self._action_format = QAction(self.tr('Format Code'), self.textEdit)
        self._action_run_code = QAction(self.tr('Run Code'), self.textEdit)
        self._action_run_sel_code = QAction(self.tr('Run Selected Code'), self.textEdit)
        self._action_save = QAction(self.tr('Save'), self.textEdit)
        self._action_find_replace = QAction(self.tr('Find/Replace'), self.textEdit)
        self._action_autocomp = QAction(self.tr('AutoComp'), self.textEdit)

        # 设置快捷键
        self._shortcut_format = QShortcut(QKeySequence('Ctrl+Alt+F'), self.textEdit)
        self._action_format.setShortcut(QKeySequence('Ctrl+Alt+F'))

        self._shortcut_autocomp = QShortcut(QKeySequence('Ctrl+P'), self.textEdit)
        self._action_autocomp.setShortcut(QKeySequence("Ctrl+P"))

        self._shortcut_run = QShortcut(QKeySequence('Ctrl+R'), self.textEdit)
        self._action_run_code.setShortcut(QKeySequence('Ctrl+R'))

        self._shortcut_run_sel = QShortcut(Qt.Key_F9, self.textEdit)
        self._action_run_sel_code.setShortcut(Qt.Key_F9)

        self._action_save.setShortcut(QKeySequence('Ctrl+S'))
        self._shortcut_save = QShortcut(QKeySequence('Ctrl+S'), self.textEdit)

        self._action_find_replace.setShortcut(QKeySequence('Ctrl+F'))
        self._shortcut_find_replace = QShortcut(QKeySequence('Ctrl+F'), self.textEdit)

        self._shortcut_goto = QShortcut(QKeySequence('Ctrl+G'), self.textEdit)

    def slot_selection_changed(self) -> None:
        """
        选中内容变化槽函数

        :return: None
        """
        line_from, index_from, line_to, index_to = self.textEdit.getSelection()
        lines = 0 if line_from == line_to == -1 else line_to - line_from + 1
        self.label_status_sel.setText(
            self.tr('Sel:{0} | {1}').format(format(len(self.textEdit.selectedText()), ','), format(lines, ',')))

    def slot_run_in_terminal(self):
        logger.warning('不支持在终端运行!')
        pass

    def slot_code_sel_run(self):
        """
        运行选中代码

        :return:
        """
        logger.warning('不支持在ipython运行!')

    def slot_code_run(self):
        """
        运行代码

        :return:
        """
        logger.warning('不支持在ipython运行!')

    def slot_code_format(self):
        pass

    def slot_goto_line(self):
        """
        跳转到指定行列
        :return:
        """
        GotoLineDialog(self.textEdit, self).exec_()
        self.textEdit.setFocus()

    def slot_set_theme(self, name: str, language=None):
        """设置编辑器主题

        :param name:
        :param language:
        :return:
        """
        if not name.endswith('.xml'):
            name += '.xml'
        path = os.path.join(os.path.dirname(__file__), 'themes', name)
        if not os.path.exists(path):
            return

        # 默认样式
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETSELBACK, 1, QColor(128, 128, 128))
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor(Qt.black))
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETEDGECOLOUR, QColor(192, 192, 192))
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINCOLOUR, True, QColor(128, 128, 128))
        self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINHICOLOUR, True, QColor(Qt.white))
        # self.textEdit.SendScintilla(QsciScintilla.SCI_INDICSETHOVERFORE, 8, QColor(128, 128, 128))

        background = QColor('#FFFFFF')
        try:
            style = etree.parse(path)
            # 全局样式
            for c in style.xpath('/NotepadPlus/GlobalStyles/WidgetStyle'):
                name, styleID, fgColor, bgColor = c.get('name'), int(c.get('styleID', 0)), '#' + str(
                    c.get('fgColor', '')), '#' + str(c.get('bgColor', ''))
                logger.debug('name:%s, styleID:%s, fgColor:%s, bgColor:%s', name, styleID, fgColor, bgColor)
                if name == 'Default Style':
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE,
                                                    QsciScintilla.STYLE_DEFAULT, QColor(fgColor))
                        logger.debug('SCI_STYLESETFORE STYLE_DEFAULT %s', fgColor)
                    if bgColor != '#':
                        background = QColor(bgColor)
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK,
                                                    QsciScintilla.STYLE_DEFAULT, QColor(bgColor))
                        logger.debug('SCI_STYLESETBACK STYLE_DEFAULT %s', bgColor)
                elif name == 'Current line background colour':
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETLINEBACK, QColor(bgColor))
                        logger.debug('SCI_SETCARETLINEBACK %s', bgColor)
                elif name == 'Selected text colour':
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETSELBACK, 1, QColor(bgColor))
                        logger.debug('SCI_SETSELBACK %s', bgColor)
                elif styleID == QsciScintilla.SCI_SETCARETFORE:
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor(fgColor))
                        logger.debug('SCI_SETCARETFORE %s', fgColor)
                elif name == 'Edge colour':
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETEDGECOLOUR, QColor(fgColor))
                        logger.debug('SCI_SETEDGECOLOUR %s', fgColor)
                elif name == 'Fold margin':
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINHICOLOUR, True, QColor(fgColor))
                        logger.debug('SCI_SETFOLDMARGINHICOLOUR %s', fgColor)
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETFOLDMARGINCOLOUR, True, QColor(bgColor))
                        logger.debug('SCI_SETFOLDMARGINCOLOUR %s', bgColor)
                # elif name == 'URL hovered':
                #     if fgColor != '#':
                #         self.textEdit.SendScintilla(QsciScintilla.SCI_INDICSETHOVERFORE, 8, QColor(fgColor))
                #         logger.debug('SCI_INDICSETHOVERFORE %s', fgColor)
                elif name == 'White space symbol':
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_SETWHITESPACEFORE, True, QColor(fgColor))
                        logger.debug('SCI_SETWHITESPACEFORE %s', fgColor)
                elif styleID == QsciScintilla.STYLE_INDENTGUIDE:
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_INDENTGUIDE,
                                                    QColor(fgColor))
                        logger.debug('SCI_STYLESETFORE STYLE_INDENTGUIDE %s', fgColor)
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_INDENTGUIDE,
                                                    QColor(bgColor))
                        logger.debug('SCI_STYLESETBACK STYLE_INDENTGUIDE %s', bgColor)
                elif styleID == QsciScintilla.STYLE_BRACELIGHT:
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_BRACELIGHT,
                                                    QColor(fgColor))
                        logger.debug('SCI_STYLESETFORE STYLE_BRACELIGHT %s', fgColor)
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_BRACELIGHT,
                                                    QColor(bgColor))
                        logger.debug('SCI_STYLESETBACK STYLE_BRACELIGHT %s', bgColor)
                elif styleID == QsciScintilla.STYLE_BRACEBAD:
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_BRACEBAD,
                                                    QColor(fgColor))
                        logger.debug('SCI_STYLESETFORE STYLE_BRACEBAD %s', fgColor)
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_BRACEBAD,
                                                    QColor(bgColor))
                        logger.debug('SCI_STYLESETBACK STYLE_BRACEBAD %s', bgColor)
                elif styleID == QsciScintilla.STYLE_LINENUMBER:
                    if fgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciScintilla.STYLE_LINENUMBER,
                                                    QColor(fgColor))
                        logger.debug('SCI_STYLESETFORE STYLE_LINENUMBER %s', fgColor)
                    if bgColor != '#':
                        self.textEdit.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_LINENUMBER,
                                                    QColor(bgColor))
                        logger.debug('SCI_STYLESETBACK STYLE_LINENUMBER %s', bgColor)

            if not self._lexer:
                return
            self._lexer.setPaper(background)
            # 关键词高亮
            logger.debug('lexer language: %s', self._lexer.language())
            # print(self._lexer.lexer())
            for w in style.xpath('/NotepadPlus/LexerStyles/LexerType[@name="{0}"]/WordsStyle'.format(
                    language if language else self._lexer.lexer().lower())):
                name, styleID, fgColor, bgColor = w.get('name'), int(w.get('styleID', 0)), '#' + str(
                    w.get('fgColor', '')), '#' + str(w.get('bgColor', ''))
                logger.debug('name:%s, styleID:%s, fgColor:%s, bgColor:%s', name, styleID, fgColor, bgColor)
                self._lexer.setColor(QColor(fgColor), styleID)
        except Exception as e:
            logger.warning(str(e), exc_info=1)

    @pyqtProperty(str)
    def theme(self) -> str:
        """返回编辑器主题

        :return:
        """
        return self._theme

    @theme.setter
    def theme(self, name):
        """设置编辑器主题

        :param name:
        :return:
        """
        if name == self._theme:
            return
        self._theme = name
        self.slot_set_theme(name)
Esempio n. 7
0
class CodeEditor(QsciScintilla):

    def __init__(self, parent=None):
        super().__init__(parent)
        
        self.filename = None
        self.fileBrowser = None
        self.mainWindow = parent
        self.debugging = False
        
        c = Configuration()
        self.pointSize = int(c.getFontSize())
        self.tabWidth = int(c.getTab())
        
        # Scrollbars
        self.verticalScrollBar().setStyleSheet(
            """border: 20px solid black;
            background-color: darkgreen;
            alternate-background-color: #FFFFFF;""")
    
        self.horizontalScrollBar().setStyleSheet(
            """border: 20px solid black;
            background-color: darkgreen;
            alternate-background-color: #FFFFFF;""")
        
        # matched / unmatched brace color ...
        self.setMatchedBraceBackgroundColor(QColor('#000000'))
        self.setMatchedBraceForegroundColor(QColor('cyan'))
        self.setUnmatchedBraceBackgroundColor(QColor('#000000'))
        self.setUnmatchedBraceForegroundColor(QColor('red'))

        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
        
        # edge mode ... line at 79 characters 
        self.setEdgeColumn(79)
        self.setEdgeMode(1)
        self.setEdgeColor(QColor('dark green'))

        # Set the default font
        self.font = QFont()
        
        system = platform.system().lower()
        if system == 'windows':
            self.font.setFamily('Consolas')
        else:
            self.font.setFamily('Monospace')
        
        self.font.setFixedPitch(True)
        self.font.setPointSize(self.pointSize)
        self.setFont(self.font)
        self.setMarginsFont(self.font)

        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(self.font)
        self.setMarginsFont(self.font)
        self.setMarginWidth(0, fontmetrics.width("00000") + 5)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#000000"))
        self.setMarginsForegroundColor(QColor("#FFFFFF"))
        
        # Margin 1 for breakpoints
        self.setMarginSensitivity(1, True)
        self.markerDefine(QsciScintilla.RightArrow, 8)
        self.setMarkerBackgroundColor(QColor('#FF0000'), 8)
        
        # variable for breakpoint
        self.breakpoint = False
        self.breakpointLine = None
        

        # FoldingBox
        self.setFoldMarginColors(QColor('dark green'), QColor('dark green'))
        
        # CallTipBox
        self.setCallTipsForegroundColor(QColor('#FFFFFF'))
        self.setCallTipsBackgroundColor(QColor('#282828'))
        self.setCallTipsHighlightColor(QColor('#3b5784'))
        self.setCallTipsStyle(QsciScintilla.CallTipsContext)
        self.setCallTipsPosition(QsciScintilla.CallTipsBelowText)
        self.setCallTipsVisible(-1)
        
        # change caret's color
        self.SendScintilla(QsciScintilla.SCI_SETCARETFORE, QColor('#98fb98'))
        self.setCaretWidth(4)

        # tab Width
        self.setIndentationsUseTabs(False)
        self.setTabWidth(self.tabWidth)
        # use Whitespaces instead tabs
        self.SendScintilla(QsciScintilla.SCI_SETUSETABS, False)
        self.setAutoIndent(True)
        self.setTabIndents(True)

        # BackTab
        self.setBackspaceUnindents(True)

        # Current line visible with special background color or not :)
        #self.setCaretLineVisible(False)
        #self.setCaretLineVisible(True)
        #self.setCaretLineBackgroundColor(QColor("#020202"))               
        self.setMinimumSize(300, 300)
        
        # get style
        self.style = None
        
        # Call the Color-Function: ...
        self.setPythonStyle()
        
        #self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)

        # Contextmenu
        self.setContextMenuPolicy(Qt.ActionsContextMenu)
        undoAction = QAction("Undo", self)
        undoAction.triggered.connect(self.undoContext)
        redoAction = QAction("Redo", self)
        redoAction.triggered.connect(self.redoContext)
        sepAction1 = QAction("", self)
        sepAction1.setSeparator(True)
        cutAction = QAction("Cut", self)
        cutAction.triggered.connect(self.cutContext)
        copyAction = QAction("Copy", self)
        copyAction.triggered.connect(self.copyContext)
        pasteAction = QAction("Paste", self)
        pasteAction.triggered.connect(self.pasteContext)
        sepAction2 = QAction("", self)
        sepAction2.setSeparator(True)
        sepAction3 = QAction("", self)
        sepAction3.setSeparator(True)
        selectAllAction = QAction("Select All", self)
        selectAllAction.triggered.connect(self.getContext)
        sepAction4 = QAction("", self)
        sepAction4.setSeparator(True)
        breakpointAction = QAction("Run until Breakpoint", self)
        breakpointAction.triggered.connect(self.breakpointContext)
        terminalAction = QAction("Open Terminal", self)
        terminalAction.triggered.connect(self.termContext)
        
        self.addAction(undoAction)
        self.addAction(redoAction)
        self.addAction(sepAction1)
        self.addAction(cutAction)
        self.addAction(copyAction)
        self.addAction(pasteAction)
        self.addAction(sepAction2)
        self.addAction(selectAllAction)
        self.addAction(sepAction3)
        self.addAction(breakpointAction)
        self.addAction(sepAction4)
        self.addAction(terminalAction)

        # signals
        self.SCN_FOCUSIN.connect(self.onFocusIn)
        self.textChanged.connect(self.onTextChanged)
        self.marginClicked.connect(self.onMarginClicked)

    def onFocusIn(self):
        self.mainWindow.refresh(self)

    def onTextChanged(self):
        notebook = self.mainWindow.notebook
        textPad = notebook.currentWidget()
        index = notebook.currentIndex()
        
        if self.debugging is True:
            self.mainWindow.statusBar.showMessage('remember to update CodeView if you delete or change lines in CodeEditor !', 3000)
        
        if textPad == None:
            return
        
        if textPad.filename:
            if not '*' in notebook.tabText(index):
                fname = os.path.basename(textPad.filename)
                fname += '*'
                notebook.setTabText(index, fname)
        
        else:
            fname = notebook.tabText(index)
            fname += '*'
            
            if not '*' in notebook.tabText(index):
                notebook.setTabText(index, fname)
        
    
    def onMarginClicked(self, margin, line, modifiers):
        
        if self.markersAtLine(line) != 0:
            self.markerDelete(line, 8)
            self.breakpoint = False
            self.breakpointLine = None
            self.mainWindow.statusBar.showMessage('Breakpoint removed', 3000)
        else:
            if self.breakpoint == False:
                self.markerAdd(line, 8)
                self.breakpoint = True
                self.breakpointLine = line + 1
                self.mainWindow.statusBar.showMessage('Breakpoint set on line ' + \
                                                      str(self.breakpointLine), 3000)
        

    def checkPath(self, path):
        if '\\' in path:
            path = path.replace('\\', '/')
        return path
    

    def undoContext(self):
        self.resetBreakpoint()
        self.undo()
    
    def redoContext(self):
        self.resetBreakpoint()
        self.redo()

    def cutContext(self):
        self.resetBreakpoint()
        self.cut()
    
    def copyContext(self):
        self.resetBreakpoint()
        self.copy()

    def pasteContext(self):
        self.resetBreakpoint()
        self.paste()

    def getContext(self):
        self.selectAll()
    
    def breakpointContext(self):        
        code = ''
        lines = self.lines()
        
        c = Configuration()
        system = c.getSystem()
        
        if self.breakpointLine:
            for i in range(lines):
                if i < self.breakpointLine:
                    code += self.text(i)
        
        randomNumber = random.SystemRandom()
        number = randomNumber.randint(0, sys.maxsize)
        filename = 'temp_file_' + str(number) + '.py'
        
        try:
            with open(filename, 'w') as f:
                f.write(code)
                command = c.getRun(system).format(filename)
                thread = RunThread(command)
                thread.start()
            
        except Exception as e:
            print(str(e))  
        
        finally:
            time.sleep(2)
            os.remove(filename)
        
    def termContext(self):
        c = Configuration()
        system = c.getSystem()
        command = c.getTerminal(system)
        
        thread = RunThread(command)
        thread.start()
 
    def getLexer(self):
        return self.lexer
    
    def setPythonStyle(self):
        self.style = 'Python'
        
        # Set Python lexer
        self.setAutoIndent(True)
        
        #self.lexer = QsciLexerPython()
        self.lexer = PythonLexer()
        self.lexer.setFont(self.font)
        self.lexer.setFoldComments(True)
        
        # set Lexer
        self.setLexer(self.lexer)
        
        self.setCaretLineBackgroundColor(QColor("#344c4c"))
        self.lexer.setDefaultPaper(QColor("black"))
        self.lexer.setDefaultColor(QColor("white"))
        self.lexer.setColor(QColor('white'), 0) # default
        self.lexer.setPaper(QColor('black'), -1) # default -1 vor all styles
        self.lexer.setColor(QColor('gray'), PythonLexer.Comment)  # = 1
        self.lexer.setColor(QColor('orange'), 2)   # Number = 2
        self.lexer.setColor(QColor('lightblue'), 3)   # DoubleQuotedString 
        self.lexer.setColor(QColor('lightblue'), 4)   # SingleQuotedString 
        self.lexer.setColor(QColor('#33cccc'), 5)   # Keyword 
        self.lexer.setColor(QColor('lightblue'), 6)   # TripleSingleQuotedString 
        self.lexer.setColor(QColor('lightblue'), 7)   # TripleDoubleQuotedString 
        self.lexer.setColor(QColor('#ffff00'), 8)   # ClassName 
        self.lexer.setColor(QColor('#ffff66'), 9)   # FunctionMethodName 
        self.lexer.setColor(QColor('magenta'), 10)   # Operator 
        self.lexer.setColor(QColor('white'), 11)   # Identifier 
        self.lexer.setColor(QColor('gray'), 12)   # CommentBlock 
        self.lexer.setColor(QColor('#ff471a'), 13)   # UnclosedString 
        self.lexer.setColor(QColor('gray'), 14)   # HighlightedIdentifier 
        self.lexer.setColor(QColor('#5DD3AF'), 15)   # Decorator 
        self.setPythonAutocomplete()
        self.setFold()


    def setPythonAutocomplete(self):
        
        self.autocomplete = QsciAPIs(self.lexer)
        self.keywords = self.lexer.keywords(1)
        
        self.keywords = self.keywords.split(' ')
        
        for word in self.keywords:
            self.autocomplete.add(word)
        
        self.autocomplete.add('super')
        self.autocomplete.add('self')
        self.autocomplete.add('__name__')
        self.autocomplete.add('__main__')
        self.autocomplete.add('__init__')
        self.autocomplete.add('__str__')
        self.autocomplete.add('__repr__')
        
        self.autocomplete.prepare()
        
        ## Set the length of the string before the editor tries to autocomplete
        self.setAutoCompletionThreshold(3)
        
        ## Tell the editor we are using a QsciAPI for the autocompletion
        self.setAutoCompletionSource(QsciScintilla.AcsAPIs)
        
        self.updateAutoComplete()
    

    def setFold(self):
        # setup Fold Styles for classes and functions ...
        x = self.FoldStyle(self.FoldStyle(5)) 
        #self.textPad.folding()
        if not x:
            self.foldAll(False)
        
        self.setFolding(x)
        #self.textPad.folding()  
        
    
    def unsetFold(self):
        self.setFolding(0)

    def keyReleaseEvent(self, e):
        # feed the autocomplete with the words from editor
        # simple algorithm to do this ... everytime after Enter

        # refresh CodeView
        text = self.text()
        self.updateCodeView(text)

        # if ENTER was hit ... :
        
        if e.key() == Qt.Key_Return:
        
            self.updateAutoComplete()
        
        if e.key() == Qt.Key_Backspace:
            self.resetBreakpoint()
    
    def resetBreakpoint(self):
        self.markerDeleteAll()
        self.breakpoint = False
        self.breakpointLine = None
    
    def updateCodeView(self, text=''):
        codeView = self.mainWindow.codeView
        codeViewDict = codeView.makeDictForCodeView(text)
        codeView.updateCodeView(codeViewDict)
       
    
    def updateAutoComplete(self, text=None):

        if not text:
            
            firstList = []     # list to edit
            secondList = []    # collect all items for autocomplete
            
            text = self.text()

            # parse complete text ....
            firstList = text.splitlines()
            
            for line in firstList:
                if 'def' in line:
                    item = line.strip()
                    item = item.strip('def')
                    item = item.replace(':', '')
                    if not item in secondList:
                        secondList.append(item)
                elif 'class' in line:
                    item = line.strip()
                    item = item.strip('class')
                    item = item.replace(':', '')
                    if not item in secondList:
                        secondList.append(item)
            
            
            text = text.replace('"', " ").replace("'", " ").replace("(", " ").replace\
                                (")", " ").replace("[", " ").replace("]", " ").replace\
                                (':', " ").replace(',', " ").replace("<", " ").replace\
                                (">", " ").replace("/", " ").replace("=", " ").replace\
                                (";", " ")
                
            firstList = text.split('\n')
            
            for row in firstList:
                
                if (row.strip().startswith('#')) or (row.strip().startswith('//')):
                    continue
                
                else:
                    wordList = row.split() 
                    
                    for word in wordList:
                        
                        if re.match("(^[0-9])", word):
                            continue
                        
                        elif '#' in word or '//' in word:
                            continue
                        
                        elif word in self.keywords:
                            continue
                        
                        elif (word == '__init__') or (word == '__main__') or \
                             (word == '__name__') or (word == '__str__') or \
                             (word == '__repr__'):
                            continue
                        
                        elif word in secondList:
                            continue
                        
                        elif len(word) > 15:
                            continue
                        
                        elif not len(word) < 3:
                            w = re.sub("{}<>;,:]", '', word)
                            #print(w)
                            secondList.append(w)
            
            # delete doubled entries
            x = set(secondList)
            secondList = list(x)
            
            # debugging ...
            #print(secondList)

            for item in secondList:
                self.autocomplete.add(item)
            
            self.autocomplete.prepare()

    def setPythonPrintStyle(self):
        # Set None lexer
        self.font = QFont()
        
        system = platform.system().lower()
        if system == 'windows':
            self.font.setFamily('Consolas')
        else:
            self.font.setFamily('Monospace')

        self.font.setFixedPitch(True)
        self.font.setPointSize(10)
        self.setFont(self.font)

        self.lexer = PythonLexer()
        self.lexer.setFont(self.font)
        # set Lexer
        self.setLexer(self.lexer)

        self.setCaretLineBackgroundColor(QColor("#344c4c"))
        self.lexer.setDefaultPaper(QColor("white"))
        self.lexer.setDefaultColor(QColor("black"))
        self.lexer.setColor(QColor('black'), -1) # default
        self.lexer.setPaper(QColor('white'), -1) # default
        self.lexer.setColor(QColor('gray'), PythonLexer.Comment)  # entspricht 1
        self.lexer.setColor(QColor('orange'), 2)   # Number entspricht 2
        self.lexer.setColor(QColor('darkgreen'), 3)   # DoubleQuotedString entspricht 3
        self.lexer.setColor(QColor('darkgreen'), 4)   # SingleQuotedString entspricht 4
        self.lexer.setColor(QColor('darkblue'), 5)   # Keyword entspricht 5
        self.lexer.setColor(QColor('darkgreen'), 6)   # TripleSingleQuotedString entspricht 6
        self.lexer.setColor(QColor('darkgreen'), 7)   # TripleDoubleQuotedString entspricht 7
        self.lexer.setColor(QColor('red'), 8)   # ClassName entspricht 8
        self.lexer.setColor(QColor('crimson'), 9)   # FunctionMethodName entspricht 9
        self.lexer.setColor(QColor('green'), 10)   # Operator entspricht 10
        self.lexer.setColor(QColor('black'), 11)   # Identifier entspricht 11 ### alle Wörter
        self.lexer.setColor(QColor('gray'), 12)   # CommentBlock entspricht 12
        self.lexer.setColor(QColor('#ff471a'), 13)   # UnclosedString entspricht 13
        self.lexer.setColor(QColor('gray'), 14)   # HighlightedIdentifier entspricht 14
        self.lexer.setColor(QColor('#5DD3AF'), 15)   # Decorator entspricht 15
    
        self.setNoneAutocomplete()
        self.unsetFold()
        
        self.font = QFont()

        system = platform.system().lower()
        if system == 'windows':
            self.font.setFamily('Consolas')
        else:
            self.font.setFamily('Monospace')

        self.font.setFixedPitch(True)
        self.font.setPointSize(self.pointSize)



    def setNoneAutocomplete(self):
        #AutoCompletion
        self.autocomplete = Qsci.QsciAPIs(self.lexer)
        self.autocomplete.clear()

        self.autocomplete.prepare()

        self.setAutoCompletionThreshold(3)
        self.setAutoCompletionSource(QsciScintilla.AcsAPIs)
    

    def resetPythonPrintStyle(self, lexer):
        
        self.font = QFont()

        system = platform.system().lower()
        if system == 'windows':
            self.font.setFamily('Consolas')
        else:
            self.font.setFamily('Monospace')

        self.font.setFixedPitch(True)
        self.font.setPointSize(self.pointSize)
        self.setFont(self.font)
        
        lexer.setFont(self.font)
        # set Lexer
        self.setLexer(lexer)
        
        # margins reset
        
        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(self.font)
        self.setMarginsFont(self.font)
        self.setMarginWidth(0, fontmetrics.width("00000") + 5)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#000000"))
        self.setMarginsForegroundColor(QColor("#FFFFFF"))

        # FoldingBox
        self.setFoldMarginColors(QColor('dark green'), QColor('dark green'))
Esempio n. 8
0
class EditorPane(QsciScintilla):
    """
    Represents the text editor.
    """

    # Signal fired when a script or hex is droped on this editor
    open_file = pyqtSignal(str)

    def __init__(self, path, text, newline=NEWLINE):
        super().__init__()
        self.setUtf8(True)
        self.path = path
        self.setText(text)
        self.newline = newline
        self.check_indicators = {  # IDs are arbitrary
            'error': {
                'id': 19,
                'markers': {}
            },
            'style': {
                'id': 20,
                'markers': {}
            }
        }
        self.search_indicators = {'selection': {'id': 21, 'positions': []}}
        self.DEBUG_INDICATOR = 22  # Arbitrary
        self.BREAKPOINT_MARKER = 23  # Arbitrary
        self.previous_selection = {
            'line_start': 0,
            'col_start': 0,
            'line_end': 0,
            'col_end': 0
        }
        if self.path:
            if self.path.endswith(".css"):
                self.lexer = CssLexer()
            elif self.path.endswith(".html") or self.path.endswith(".htm"):
                self.lexer = QsciLexerHTML()
                self.lexer.setDjangoTemplates(True)
            else:
                self.lexer = PythonLexer()
        else:
            self.lexer = PythonLexer()
        self.api = None
        self.has_annotations = False
        self.setModified(False)
        self.breakpoint_handles = set()
        self.configure()

    def wheelEvent(self, event):
        """
        Stops QScintilla from doing the wrong sort of zoom handling.
        """
        if not QApplication.keyboardModifiers():
            super().wheelEvent(event)

    def dropEvent(self, event):
        """
        Run by Qt when *something* is dropped on this editor
        """

        # Does the drag event have any urls?
        # Files are transfered as a url (by path not value)
        if event.mimeData().hasUrls():
            # Qt doesn't seem to have an 'open' action,
            # this seems the most appropriate
            event.setDropAction(Qt.CopyAction)
            # Valid links
            links = []
            # Iterate over each of the urls attached to the event
            for url in event.mimeData().urls():
                # Check the url is to a local file
                # (not a webpage for example)
                if url.isLocalFile():
                    # Grab a 'real' path from the url
                    path = url.toLocalFile()
                    # Add it to the list of valid links
                    links.append(path)

            # Did we get any?
            if len(links) > 0:
                # Only accept now we actually know we can do
                # something with the drop event
                event.accept()
                for link in links:
                    # Start bubbling an open file request
                    self.open_file.emit(link)

        # If the event wasn't handled let QsciScintilla have a go
        if not event.isAccepted():
            super().dropEvent(event)

    def configure(self):
        """
        Set up the editor component.
        """
        # Font information
        font = Font().load()
        self.setFont(font)
        # Generic editor settings
        self.setUtf8(True)
        self.setAutoIndent(True)
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setIndentationGuides(True)
        self.setBackspaceUnindents(True)
        self.setTabWidth(4)
        self.setEdgeColumn(79)
        self.setMarginLineNumbers(0, True)
        self.setMarginWidth(0, 50)
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
        self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
        self.set_theme()
        # Markers and indicators
        self.setMarginSensitivity(0, True)
        self.markerDefine(self.Circle, self.BREAKPOINT_MARKER)
        self.setMarginSensitivity(1, True)
        # Additional dummy margin to prevent accidental breakpoint toggles when
        # trying to position the edit cursor to the left of the first column,
        # using the mouse and not being 100% accurate. This margin needs to be
        # set with "sensitivity on": otherwise clicking it would select the
        # whole text line, per QsciScintilla's behaviour. It is up to the
        # click handler to ignore clicks on this margin: self.connect_margin.
        self.setMarginWidth(4, 8)
        self.setMarginSensitivity(4, True)
        # Indicators
        self.setIndicatorDrawUnder(True)
        for type_ in self.check_indicators:
            self.indicatorDefine(self.SquiggleIndicator,
                                 self.check_indicators[type_]['id'])
        for type_ in self.search_indicators:
            self.indicatorDefine(self.StraightBoxIndicator,
                                 self.search_indicators[type_]['id'])
        self.indicatorDefine(self.FullBoxIndicator, self.DEBUG_INDICATOR)
        self.setAnnotationDisplay(self.AnnotationBoxed)
        self.selectionChanged.connect(self.selection_change_listener)
        self.set_zoom()

    def connect_margin(self, func):
        """
        Connect clicking the margin to the passed in handler function, via a
        filtering handler that ignores clicks on margin 4.
        """

        # Margin 4 motivation in self.configure comments.
        def func_ignoring_margin_4(margin, line, modifiers):
            if margin != 4:
                func(margin, line, modifiers)

        self.marginClicked.connect(func_ignoring_margin_4)

    def set_theme(self, theme=DayTheme):
        """
        Connect the theme to a lexer and return the lexer for the editor to
        apply to the script text.
        """
        theme.apply_to(self.lexer)
        self.lexer.setDefaultPaper(theme.Paper)
        self.setCaretForegroundColor(theme.Caret)
        self.setIndicatorForegroundColor(theme.IndicatorError,
                                         self.check_indicators['error']['id'])
        self.setIndicatorForegroundColor(theme.IndicatorStyle,
                                         self.check_indicators['style']['id'])
        self.setIndicatorForegroundColor(theme.DebugStyle,
                                         self.DEBUG_INDICATOR)
        for type_ in self.search_indicators:
            self.setIndicatorForegroundColor(
                theme.IndicatorWordMatch, self.search_indicators[type_]['id'])
        self.setMarkerBackgroundColor(theme.BreakpointMarker,
                                      self.BREAKPOINT_MARKER)
        self.setAutoCompletionThreshold(2)
        self.setAutoCompletionSource(QsciScintilla.AcsAll)
        self.setLexer(self.lexer)
        self.setMarginsBackgroundColor(theme.Margin)
        self.setMarginsForegroundColor(theme.Caret)
        self.setMatchedBraceBackgroundColor(theme.BraceBackground)
        self.setMatchedBraceForegroundColor(theme.BraceForeground)
        self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground)
        self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground)

    def set_api(self, api_definitions):
        """
        Sets the API entries for tooltips, calltips and the like.
        """
        self.api = QsciAPIs(self.lexer)
        for entry in api_definitions:
            self.api.add(entry)
        self.api.prepare()

    def set_zoom(self, size='m'):
        """
        Sets the font zoom to the specified base point size for all fonts given
        a t-shirt size.
        """
        sizes = {
            'xs': -4,
            's': -2,
            'm': 1,
            'l': 4,
            'xl': 8,
            'xxl': 16,
            'xxxl': 48,
        }
        self.zoomTo(sizes[size])

    @property
    def label(self):
        """
        The label associated with this editor widget (usually the filename of
        the script we're editing).
        """
        if self.path:
            label = os.path.basename(self.path)
        else:
            label = _('untitled')
        return label

    @property
    def title(self):
        """
        The title associated with this editor widget (usually the filename of
        the script we're editing).

        If the script has been modified since it was last saved, the label will
        end with an asterisk.
        """
        if self.isModified():
            return self.label + ' •'
        return self.label

    def reset_annotations(self):
        """
        Clears all the assets (indicators, annotations and markers).
        """
        self.clearAnnotations()
        self.markerDeleteAll()
        self.reset_search_indicators()
        self.reset_check_indicators()

    def reset_check_indicators(self):
        """
        Clears all the text indicators related to the check code functionality.
        """
        for indicator in self.check_indicators:
            for _, markers in \
                    self.check_indicators[indicator]['markers'].items():
                line_no = markers[0]['line_no']  # All markers on same line.
                self.clearIndicatorRange(
                    line_no, 0, line_no, 999999,
                    self.check_indicators[indicator]['id'])
            self.check_indicators[indicator]['markers'] = {}

    def reset_search_indicators(self):
        """
        Clears all the text indicators from the search functionality.
        """
        for indicator in self.search_indicators:
            for position in self.search_indicators[indicator]['positions']:
                self.clearIndicatorRange(
                    position['line_start'], position['col_start'],
                    position['line_end'], position['col_end'],
                    self.search_indicators[indicator]['id'])
            self.search_indicators[indicator]['positions'] = []

    def annotate_code(self, feedback, annotation_type='error'):
        """
        Given a list of annotations add them to the editor pane so the user can
        act upon them.
        """
        indicator = self.check_indicators[annotation_type]
        for line_no, messages in feedback.items():
            indicator['markers'][line_no] = messages
            for message in messages:
                col = message.get('column', 0)
                if col:
                    col_start = col - 1
                    col_end = col + 1
                    self.fillIndicatorRange(line_no, col_start, line_no,
                                            col_end, indicator['id'])
        if feedback:
            # Ensure the first line with a problem is visible.
            first_problem_line = sorted(feedback.keys())[0]
            self.ensureLineVisible(first_problem_line)

    def debugger_at_line(self, line):
        """
        Set the line to be highlighted with the DEBUG_INDICATOR.
        """
        self.reset_debugger_highlight()
        # Calculate the line length & account for \r\n giving ObOE.
        line_length = len(self.text(line).rstrip())
        self.fillIndicatorRange(line, 0, line, line_length,
                                self.DEBUG_INDICATOR)
        self.ensureLineVisible(line)

    def reset_debugger_highlight(self):
        """
        Reset all the lines so the DEBUG_INDICATOR is no longer displayed.

        We need to check each line since there's no way to tell what the
        currently highlighted line is. This approach also has the advantage of
        resetting the *whole* editor pane.
        """
        for i in range(self.lines()):
            line_length = len(self.text(i))
            self.clearIndicatorRange(i, 0, i, line_length,
                                     self.DEBUG_INDICATOR)

    def show_annotations(self):
        """
        Display all the messages to be annotated to the code.
        """
        lines = defaultdict(list)
        for indicator in self.check_indicators:
            markers = self.check_indicators[indicator]['markers']
            for k, marker_list in markers.items():
                for m in marker_list:
                    lines[m['line_no']].append('\u2191 ' + m['message'])
        for line, messages in lines.items():
            text = '\n'.join(messages).strip()
            if text:
                self.annotate(line, text, self.annotationDisplay())

    def find_next_match(self,
                        text,
                        from_line=-1,
                        from_col=-1,
                        case_sensitive=True,
                        wrap_around=True):
        """
        Finds the next text match from the current cursor, or the given
        position, and selects it (the automatic selection is the only available
        QsciScintilla behaviour).
        Returns True if match found, False otherwise.
        """
        return self.findFirst(
            text,  # Text to find,
            False,  # Treat as regular expression
            case_sensitive,  # Case sensitive search
            True,  # Whole word matches only
            wrap_around,  # Wrap search
            forward=True,  # Forward search
            line=from_line,  # -1 starts at current position
            index=from_col,  # -1 starts at current position
            show=False,  # Unfolds found text
            posix=False)  # More POSIX compatible RegEx

    def range_from_positions(self, start_position, end_position):
        """Given a start-end pair, such as are provided by a regex match,
        return the corresponding Scintilla line-offset pairs which are
        used for searches, indicators etc.

        NOTE: Arguments must be byte offsets into the underlying text bytes.
        """
        start_line, start_offset = self.lineIndexFromPosition(start_position)
        end_line, end_offset = self.lineIndexFromPosition(end_position)
        return start_line, start_offset, end_line, end_offset

    def highlight_selected_matches(self):
        """
        Checks the current selection, if it is a single word it then searches
        and highlights all matches.

        Since we're interested in exactly one word:
        * Ignore an empty selection
        * Ignore anything which spans more than one line
        * Ignore more than one word
        * Ignore anything less than one word
        """
        selected_range = line0, col0, line1, col1 = self.getSelection()
        #
        # If there's no selection, do nothing
        #
        if selected_range == (-1, -1, -1, -1):
            return

        #
        # Ignore anything which spans two or more lines
        #
        if line0 != line1:
            return

        #
        # Ignore if no text is selected or the selected text is not at most one
        # valid identifier-type word.
        #
        selected_text = self.selectedText()
        if not RE_VALID_WORD.match(selected_text):
            return

        #
        # Ignore anything which is not a whole word.
        # NB Although Scintilla defines a SCI_ISRANGEWORD message,
        # it's not exposed by QSciScintilla. Instead, we
        # ask Scintilla for the start end end position of
        # the word we're in and test whether our range end points match
        # those or not.
        #
        pos0 = self.positionFromLineIndex(line0, col0)
        word_start_pos = self.SendScintilla(
            QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1)
        _, start_offset = self.lineIndexFromPosition(word_start_pos)
        if col0 != start_offset:
            return

        pos1 = self.positionFromLineIndex(line1, col1)
        word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION,
                                          pos1, 1)
        _, end_offset = self.lineIndexFromPosition(word_end_pos)
        if col1 != end_offset:
            return

        #
        # For each matching word within the editor text, add it to
        # the list of highlighted indicators and fill it according
        # to the current theme.
        #
        indicators = self.search_indicators['selection']
        encoding = 'utf8' if self.isUtf8() else 'latin1'
        text_bytes = self.text().encode(encoding)
        selected_text_bytes = selected_text.encode(encoding)
        for match in re.finditer(selected_text_bytes, text_bytes):
            range = self.range_from_positions(*match.span())
            #
            # Don't highlight the text we've selected
            #
            if range == selected_range:
                continue

            line_start, col_start, line_end, col_end = range
            indicators['positions'].append({
                'line_start': line_start,
                'col_start': col_start,
                'line_end': line_end,
                'col_end': col_end
            })
            self.fillIndicatorRange(line_start, col_start, line_end, col_end,
                                    indicators['id'])

    def selection_change_listener(self):
        """
        Runs every time the text selection changes. This could get triggered
        multiple times while the mouse click is down, even if selection has not
        changed in itself.
        If there is a new selection is passes control to
        highlight_selected_matches.
        """
        # Get the current selection, exit if it has not changed
        line_from, index_from, line_to, index_to = self.getSelection()
        if self.previous_selection['col_end'] != index_to or \
                self.previous_selection['col_start'] != index_from or \
                self.previous_selection['line_start'] != line_from or \
                self.previous_selection['line_end'] != line_to:
            self.previous_selection['line_start'] = line_from
            self.previous_selection['col_start'] = index_from
            self.previous_selection['line_end'] = line_to
            self.previous_selection['col_end'] = index_to
            # Highlight matches
            self.reset_search_indicators()
            self.highlight_selected_matches()

    def toggle_line(self, raw_line):
        """
        Given a raw_line, will return the toggled version of it.
        """
        clean_line = raw_line.strip()
        if not clean_line or clean_line.startswith('##'):
            # Ignore whitespace-only lines and compact multi-commented lines
            return raw_line

        if clean_line.startswith('#'):
            # It's a comment line, so replace only the first "# " or "#":
            if clean_line.startswith('# '):
                return raw_line.replace('# ', '', 1)
            else:
                return raw_line.replace('#', '', 1)
        else:
            # It's a normal line of code.
            return '# ' + raw_line

    def toggle_comments(self):
        """
        Iterate through the selected lines and toggle their comment/uncomment
        state. So, lines that are not comments become comments and vice versa.
        """
        if self.hasSelectedText():
            # Toggle currently selected text.
            logger.info("Toggling comments")
            line_from, index_from, line_to, index_to = self.getSelection()
            selected_text = self.selectedText()
            lines = selected_text.split('\n')
            toggled_lines = []
            for line in lines:
                toggled_lines.append(self.toggle_line(line))
            new_text = '\n'.join(toggled_lines)
            self.replaceSelectedText(new_text)
            # Ensure the new text is also selected.
            last_newline = toggled_lines[-1]
            last_oldline = lines[-1]

            # Adjust the selection based on whether the last line got
            # longer, shorter, or stayed the same
            delta = len(last_newline) - len(last_oldline)
            index_to += delta
            self.setSelection(line_from, index_from, line_to, index_to)
        else:
            # Toggle the line currently containing the cursor.
            line_number, column = self.getCursorPosition()
            logger.info('Toggling line {}'.format(line_number))
            line_content = self.text(line_number)
            new_line = self.toggle_line(line_content)
            self.setSelection(line_number, 0, line_number, len(line_content))
            self.replaceSelectedText(new_line)
            self.setSelection(line_number, 0, line_number, len(new_line) - 1)
Esempio n. 9
0
class TkPyTextEdit(QsciScintilla):
    key_pressed = pyqtSignal()
    format_code = pyqtSignal()
    sort_imports = pyqtSignal()

    def __init__(self, parent=None):
        QsciScintilla.__init__(self, parent)
        self.complete = AutoComplete()
        self.complete.prepare.connect(self.update_completes)
        self.lexer = text_lexer(self)
        self.setLexer(self.lexer)
        self.api = QsciAPIs(self.lexer)
        self.setAutoIndent(True)
        self.setMarginLineNumbers(0, True)
        self.setEdgeMode(QsciScintilla.EdgeLine)
        self.setEdgeColumn(79)
        self.setEdgeColor(QColor(0, 0, 0))
        """
        self.setIndentationsUseTabs(True)
        self.setIndentationWidth(get_configs()['tab_width'])
        self.setTabWidth(get_configs()['tab_width'])
        self.setTabIndents(get_configs()['indent_with_tabs'])
        self.setBackspaceUnindents(True)
        self.setCaretLineVisible(True)
        self.setIndentationGuides(True)
        self.setCaretForegroundColor(QColor(get_configs()['cursor_color']))
        self.setCaretLineBackgroundColor(QColor(get_configs()['line_background_color']))
        self.setCaretWidth(6)
        self.setWrapMode(QsciScintilla.WrapNone if not get_configs()['text_wrap'] else QsciScintilla.WrapWhitespace)
        self.setEolMode(get_eol_mode(get_configs()['eol_mode']))
        # self.setMarginsForegroundColor(QColor("#ff888888"))
        """
        self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20)
        self.setFolding(QsciScintilla.PlainFoldStyle)
        self.setAutoCompletionSource(QsciScintilla.AcsAll)
        self.setAutoCompletionCaseSensitivity(True)
        self.setAutoCompletionReplaceWord(True)
        self.autoCompleteFromAll()
        self.setAutoCompletionThreshold(1)
        self.setAutoCompletionSource(QsciScintilla.AcsAll)
        self.setUtf8(True)
        self.setBraceMatching(QsciScintilla.StrictBraceMatch)
        self.setMatchedBraceForegroundColor(
            QColor(self.lexer.styles[Name.Decorator]))
        self.setMatchedBraceBackgroundColor(RGB(0, 255, 0).to_pyqt_color())
        self.setCallTipsVisible(-1)

    def goto_html_or_define(self, position):
        print(position)

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Space:
            if QApplication.keyboardModifiers() == Qt.ControlModifier:
                self.autoCompleteFromAll()
                return
        QsciScintilla.keyPressEvent(self, event)
        self.complete.line, self.complete.column = self.getCursorPosition()
        self.complete.text = self.text()
        self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20)
        # if event.key() not in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
        #    if not self.complete.started:
        #        self.complete.start()
        self.key_pressed.emit()

    def contextMenuEvent(self, event: QContextMenuEvent) -> None:
        menu = QMenu(self)
        complete_code = menu.addAction('代码提示')
        complete_code.triggered.connect(self.autoCompleteFromAll)
        complete_code.setShortcut('Ctrl+Space')
        menu.addSeparator()
        copy = menu.addAction('复制')
        copy.setShortcut(get_event('Ctrl+C'))
        copy.triggered.connect(self.copy)
        paste = menu.addAction('粘贴')
        paste.setShortcut(get_event('Ctrl+V'))
        paste.triggered.connect(self.paste)
        cut = menu.addAction('剪切')
        cut.setShortcut(get_event('Ctrl+X'))
        cut.triggered.connect(self.cut)
        menu.addSeparator()
        undo = menu.addAction('撤销')
        undo.setShortcut(get_event('Ctrl+Z'))
        undo.triggered.connect(self.undo)
        redo = menu.addAction('撤回')
        redo.setShortcut('Ctrl+Y')
        redo.triggered.connect(self.redo)
        menu.addSeparator()
        format_code = menu.addAction('格式化代码')
        format_code.triggered.connect(self.format_code.emit)
        format_code.setShortcut('Ctrl+Alt+L')
        sort_imports = menu.addAction('整理Import语句')
        sort_imports.triggered.connect(self.sort_imports.emit)
        menu.exec_(event.globalPos())

    def paste(self):
        QsciScintilla.paste(self)
        self.key_pressed.emit()
        self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20)

    def cut(self):
        QsciScintilla.cut(self)
        self.key_pressed.emit()
        self.setMarginWidth(0, len(str(len(self.text().split('\n')))) * 20)

    def update_completes(self, completes):
        for complete in completes:
            self.api.add(complete)
        self.api.prepare()

    def goto_line(self, lineno: int, column: int = 0):
        self.setCursorPosition(lineno, column)
Esempio n. 10
0
class EditorPane(QsciScintilla):
    """
    Represents the text editor.
    """

    # Signal fired when a script or hex is droped on this editor
    open_file = pyqtSignal(str)

    def __init__(self, path, text, newline=NEWLINE):
        super().__init__()
        self.setUtf8(True)
        self.path = path
        self.setText(text)
        self.newline = newline
        self.check_indicators = {  # IDs are arbitrary
            'error': {
                'id': 19,
                'markers': {}
            },
            'style': {
                'id': 20,
                'markers': {}
            }
        }
        self.BREAKPOINT_MARKER = 23  # Arbitrary
        self.search_indicators = {'selection': {'id': 21, 'positions': []}}
        self.previous_selection = {
            'line_start': 0,
            'col_start': 0,
            'line_end': 0,
            'col_end': 0
        }
        self.lexer = PythonLexer()
        self.api = None
        self.has_annotations = False
        self.setModified(False)
        self.breakpoint_lines = set()
        self.configure()

    def dropEvent(self, event):
        """
        Run by Qt when *something* is dropped on this editor
        """

        # Does the drag event have any urls?
        # Files are transfered as a url (by path not value)
        if event.mimeData().hasUrls():
            # Qt doesn't seem to have an 'open' action,
            # this seems the most appropriate
            event.setDropAction(Qt.CopyAction)
            # Valid links
            links = []
            # Iterate over each of the urls attached to the event
            for url in event.mimeData().urls():
                # Check the url is to a local file
                # (not a webpage for example)
                if url.isLocalFile():
                    # Grab a 'real' path from the url
                    path = url.toLocalFile()
                    # Add it to the list of valid links
                    links.append(path)

            # Did we get any?
            if len(links) > 0:
                # Only accept now we actually know we can do
                # something with the drop event
                event.accept()
                for link in links:
                    # Start bubbling an open file request
                    self.open_file.emit(link)

        # If the event wasn't handled let QsciScintilla have a go
        if not event.isAccepted():
            super().dropEvent(event)

    def configure(self):
        """
        Set up the editor component.
        """
        # Font information
        font = Font().load()
        self.setFont(font)
        # Generic editor settings
        self.setUtf8(True)
        self.setAutoIndent(True)
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setIndentationGuides(True)
        self.setBackspaceUnindents(True)
        self.setTabWidth(4)
        self.setEdgeColumn(79)
        self.setMarginLineNumbers(0, True)
        self.setMarginWidth(0, 50)
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
        self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
        self.set_theme()
        # Markers and indicators
        self.setMarginSensitivity(0, True)
        self.markerDefine(self.Circle, self.BREAKPOINT_MARKER)
        self.setMarginSensitivity(1, True)
        self.setIndicatorDrawUnder(True)
        for type_ in self.check_indicators:
            self.indicatorDefine(self.SquiggleIndicator,
                                 self.check_indicators[type_]['id'])
        for type_ in self.search_indicators:
            self.indicatorDefine(self.StraightBoxIndicator,
                                 self.search_indicators[type_]['id'])
        self.setAnnotationDisplay(self.AnnotationBoxed)
        self.selectionChanged.connect(self.selection_change_listener)

    def connect_margin(self, func):
        """
        Connect clicking the margin to the passed in handler function.
        """
        self.marginClicked.connect(func)

    def set_theme(self, theme=DayTheme):
        """
        Connect the theme to a lexer and return the lexer for the editor to
        apply to the script text.
        """
        theme.apply_to(self.lexer)
        self.lexer.setDefaultPaper(theme.Paper)
        self.setCaretForegroundColor(theme.Caret)
        self.setMarginsBackgroundColor(theme.Margin)
        self.setMarginsForegroundColor(theme.Caret)
        self.setIndicatorForegroundColor(theme.IndicatorError,
                                         self.check_indicators['error']['id'])
        self.setIndicatorForegroundColor(theme.IndicatorStyle,
                                         self.check_indicators['style']['id'])
        for type_ in self.search_indicators:
            self.setIndicatorForegroundColor(
                theme.IndicatorWordMatch, self.search_indicators[type_]['id'])
        self.setMarkerBackgroundColor(theme.BreakpointMarker,
                                      self.BREAKPOINT_MARKER)
        self.setAutoCompletionThreshold(2)
        self.setAutoCompletionSource(QsciScintilla.AcsAll)
        self.setLexer(self.lexer)
        self.setMatchedBraceBackgroundColor(theme.BraceBackground)
        self.setMatchedBraceForegroundColor(theme.BraceForeground)
        self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground)
        self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground)

    def set_api(self, api_definitions):
        """
        Sets the API entries for tooltips, calltips and the like.
        """
        self.api = QsciAPIs(self.lexer)
        for entry in api_definitions:
            self.api.add(entry)
        self.api.prepare()

    @property
    def label(self):
        """
        The label associated with this editor widget (usually the filename of
        the script we're editing).

        If the script has been modified since it was last saved, the label will
        end with an asterisk.
        """
        if self.path:
            label = os.path.basename(self.path)
        else:
            label = 'untitled'
        # Add an asterisk to indicate that the file remains unsaved.
        if self.isModified():
            return label + ' *'
        else:
            return label

    def reset_annotations(self):
        """
        Clears all the assets (indicators, annotations and markers).
        """
        self.clearAnnotations()
        self.markerDeleteAll()
        self.reset_search_indicators()
        self.reset_check_indicators()

    def reset_check_indicators(self):
        """
        Clears all the text indicators related to the check code functionality.
        """
        for indicator in self.check_indicators:
            for _, markers in \
                    self.check_indicators[indicator]['markers'].items():
                line_no = markers[0]['line_no']  # All markers on same line.
                self.clearIndicatorRange(
                    line_no, 0, line_no, 999999,
                    self.check_indicators[indicator]['id'])
            self.check_indicators[indicator]['markers'] = {}

    def reset_search_indicators(self):
        """
        Clears all the text indicators from the search functionality.
        """
        for indicator in self.search_indicators:
            for position in self.search_indicators[indicator]['positions']:
                self.clearIndicatorRange(
                    position['line_start'], position['col_start'],
                    position['line_end'], position['col_end'],
                    self.search_indicators[indicator]['id'])
            self.search_indicators[indicator]['positions'] = []

    def annotate_code(self, feedback, annotation_type='error'):
        """
        Given a list of annotations add them to the editor pane so the user can
        act upon them.
        """
        indicator = self.check_indicators[annotation_type]
        for line_no, messages in feedback.items():
            indicator['markers'][line_no] = messages
            for message in messages:
                col = message.get('column', 0)
                if col:
                    col_start = col - 1
                    col_end = col + 1
                    self.fillIndicatorRange(line_no, col_start, line_no,
                                            col_end, indicator['id'])

    def show_annotations(self):
        """
        Display all the messages to be annotated to the code.
        """
        lines = defaultdict(list)
        for indicator in self.check_indicators:
            markers = self.check_indicators[indicator]['markers']
            for k, marker_list in markers.items():
                for m in marker_list:
                    lines[m['line_no']].append('\u2191 ' + m['message'])
        for line, messages in lines.items():
            text = '\n'.join(messages).strip()
            if text:
                self.annotate(line, text, self.annotationDisplay())

    def find_next_match(self,
                        text,
                        from_line=-1,
                        from_col=-1,
                        case_sensitive=True,
                        wrap_around=True):
        """
        Finds the next text match from the current cursor, or the given
        position, and selects it (the automatic selection is the only available
        QsciScintilla behaviour).
        Returns True if match found, False otherwise.
        """
        return self.findFirst(
            text,  # Text to find,
            False,  # Treat as regular expression
            case_sensitive,  # Case sensitive search
            True,  # Whole word matches only
            wrap_around,  # Wrap search
            forward=True,  # Forward search
            line=from_line,  # -1 starts at current position
            index=from_col,  # -1 starts at current position
            show=False,  # Unfolds found text
            posix=False)  # More POSIX compatible RegEx

    def range_from_positions(self, start_position, end_position):
        """Given a start-end pair, such as are provided by a regex match,
        return the corresponding Scintilla line-offset pairs which are
        used for searches, indicators etc.

        FIXME: Not clear whether the Scintilla conversions are expecting
        bytes or characters (ie codepoints)
        """
        start_line, start_offset = self.lineIndexFromPosition(start_position)
        end_line, end_offset = self.lineIndexFromPosition(end_position)
        return start_line, start_offset, end_line, end_offset

    def highlight_selected_matches(self):
        """
        Checks the current selection, if it is a single word it then searches
        and highlights all matches.

        Since we're interested in exactly one word:
        * Ignore an empty selection
        * Ignore anything which spans more than one line
        * Ignore more than one word
        * Ignore anything less than one word
        """
        selected_range = line0, col0, line1, col1 = self.getSelection()
        #
        # If there's no selection, do nothing
        #
        if selected_range == (-1, -1, -1, -1):
            return

        #
        # Ignore anything which spans two or more lines
        #
        if line0 != line1:
            return

        #
        # Ignore if no text is selected or the selected text is not at most one
        # valid identifier-type word.
        #
        selected_text = self.selectedText()
        if not RE_VALID_WORD.match(selected_text):
            return

        #
        # Ignore anything which is not a whole word.
        # NB Although Scintilla defines a SCI_ISRANGEWORD message,
        # it's not exposed by QSciScintilla. Instead, we
        # ask Scintilla for the start end end position of
        # the word we're in and test whether our range end points match
        # those or not.
        #
        pos0 = self.positionFromLineIndex(line0, col0)
        word_start_pos = self.SendScintilla(
            QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1)
        _, start_offset = self.lineIndexFromPosition(word_start_pos)
        if col0 != start_offset:
            return

        pos1 = self.positionFromLineIndex(line1, col1)
        word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION,
                                          pos1, 1)
        _, end_offset = self.lineIndexFromPosition(word_end_pos)
        if col1 != end_offset:
            return

        #
        # For each matching word within the editor text, add it to
        # the list of highlighted indicators and fill it according
        # to the current theme.
        #
        indicators = self.search_indicators['selection']
        text = self.text()
        for match in re.finditer(selected_text, text):
            range = self.range_from_positions(*match.span())
            #
            # Don't highlight the text we've selected
            #
            if range == selected_range:
                continue

            line_start, col_start, line_end, col_end = range
            indicators['positions'].append({
                'line_start': line_start,
                'col_start': col_start,
                'line_end': line_end,
                'col_end': col_end
            })
            self.fillIndicatorRange(line_start, col_start, line_end, col_end,
                                    indicators['id'])

    def selection_change_listener(self):
        """
        Runs every time the text selection changes. This could get triggered
        multiple times while the mouse click is down, even if selection has not
        changed in itself.
        If there is a new selection is passes control to
        highlight_selected_matches.
        """
        # Get the current selection, exit if it has not changed
        line_from, index_from, line_to, index_to = self.getSelection()
        if self.previous_selection['col_end'] != index_to or \
                self.previous_selection['col_start'] != index_from or \
                self.previous_selection['line_start'] != line_from or \
                self.previous_selection['line_end'] != line_to:
            self.previous_selection['line_start'] = line_from
            self.previous_selection['col_start'] = index_from
            self.previous_selection['line_end'] = line_to
            self.previous_selection['col_end'] = index_to
            # Highlight matches
            self.reset_search_indicators()
            self.highlight_selected_matches()
Esempio n. 11
0
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)
Esempio n. 12
0
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()