class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) sig_progress = Signal(int) sig_exec_error = Signal(object) sig_exec_success = Signal(object) def __init__(self, font=None, content=None, filename=None, parent=None): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) self.parent = parent # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(self.editor)) self.code_commenter = CodeCommenter(self.editor) self.code_completer = CodeCompleter(self.editor, self._presenter.model.globals_ns) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success) # Re-populate the completion API after execution success self._presenter.model.sig_exec_success.connect(self.code_completer.update_completion_api) def closeEvent(self, event): self.deleteLater() if self.find_replace_dialog: self.find_replace_dialog.close() super(PythonFileInterpreter, self).closeEvent(event) def show_find_replace_dialog(self): if self.find_replace_dialog is None: self.find_replace_dialog = EmbeddedFindReplaceDialog(self, self.editor) self.layout.insertWidget(0, self.find_replace_dialog.view) self.find_replace_dialog.show() def hide_find_replace_dialog(self): if self.find_replace_dialog is not None: self.find_replace_dialog.hide() @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(prompt_for_confirmation=self.parent.confirm_on_save) def abort(self): self._presenter.req_abort() def execute_async(self, ignore_selection=False): return self._presenter.req_execute_async(ignore_selection) def execute_async_blocking(self): self._presenter.req_execute_async_blocking() def save(self, prompt_for_confirmation=False, force_save=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(prompt_for_confirmation, force_save) else: return True def save_as(self): io = EditorIO(self.editor) new_filename = io.ask_for_filename() if new_filename: return io.write(save_as=new_filename), new_filename else: return False, None def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def replace_tabs_with_spaces(self): self.replace_text(TAB_CHAR, SPACE_CHAR * TAB_WIDTH) def replace_text(self, match_text, replace_text): if self.editor.selectedText() == '': self.editor.selectAll() new_text = self.editor.selectedText().replace(match_text, replace_text) self.editor.replaceSelectedText(new_text) def replace_spaces_with_tabs(self): self.replace_text(SPACE_CHAR * TAB_WIDTH, TAB_CHAR) def set_whitespace_visible(self): self.editor.setEolVisibility(True) self.editor.setWhitespaceVisibility(CodeEditor.WsVisible) def set_whitespace_invisible(self): self.editor.setEolVisibility(False) self.editor.setWhitespaceVisibility(CodeEditor.WsInvisible) def toggle_comment(self): self.code_commenter.toggle_comment() def _setup_editor(self, default_content, filename): editor = self.editor # Clear default QsciScintilla key bindings that we want to allow # to be users of this class self.clear_key_binding("Ctrl+/") # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str)
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) def __init__(self, content=None, filename=None, parent=None): """ :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) # layout self.editor = CodeEditor("AlternateCSPythonLexer", self) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(confirm=True) def abort(self): self._presenter.req_abort() def execute_async(self): self._presenter.req_execute_async() def save(self, confirm=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(confirm) else: return True def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def _setup_editor(self, default_content, filename): editor = self.editor # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) editor.enableAutoCompletion(CodeEditor.AcsAll)
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) def __init__(self, content=None, filename=None, parent=None): """ :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) # layout self.editor = CodeEditor("AlternateCSPythonLexer", self) # Clear QsciScintilla key bindings that may override PyQt's bindings self.clear_key_binding("Ctrl+/") self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self.layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self.setAttribute(Qt.WA_DeleteOnClose, True) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.find_replace_dialog = None self.find_replace_dialog_shown = False def closeEvent(self, event): self.deleteLater() if self.find_replace_dialog: self.find_replace_dialog.close() super(PythonFileInterpreter, self).closeEvent(event) def show_find_replace_dialog(self): if self.find_replace_dialog is None: self.find_replace_dialog = EmbeddedFindReplaceDialog( self, self.editor) self.layout.insertWidget(0, self.find_replace_dialog.view) self.find_replace_dialog.show() def hide_find_replace_dialog(self): if self.find_replace_dialog is not None: self.find_replace_dialog.hide() @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(confirm=True) def abort(self): self._presenter.req_abort() def execute_async(self): self._presenter.req_execute_async() def save(self, confirm=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(confirm) else: return True def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def replace_tabs_with_spaces(self): self.replace_text(TAB_CHAR, SPACE_CHAR * TAB_WIDTH) def replace_text(self, match_text, replace_text): if self.editor.selectedText() == '': self.editor.selectAll() new_text = self.editor.selectedText().replace(match_text, replace_text) self.editor.replaceSelectedText(new_text) def replace_spaces_with_tabs(self): self.replace_text(SPACE_CHAR * TAB_WIDTH, TAB_CHAR) def set_whitespace_visible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsVisible) def set_whitespace_invisible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsInvisible) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str) def toggle_comment(self): if self.editor.selectedText() == '': # If nothing selected, do nothing return # Note selection indices to restore highlighting later selection_idxs = list(self.editor.getSelection()) # Expand selection from first character on start line to end char on last line line_end_pos = len( self.editor.text().split('\n')[selection_idxs[2]].rstrip()) line_selection_idxs = [ selection_idxs[0], 0, selection_idxs[2], line_end_pos ] self.editor.setSelection(*line_selection_idxs) selected_lines = self.editor.selectedText().split('\n') if self._are_comments(selected_lines) is True: toggled_lines = self._uncomment_lines(selected_lines) # Track deleted characters to keep highlighting consistent selection_idxs[1] -= 2 selection_idxs[-1] -= 2 else: toggled_lines = self._comment_lines(selected_lines) selection_idxs[1] += 2 selection_idxs[-1] += 2 # Replace lines with commented/uncommented lines self.editor.replaceSelectedText('\n'.join(toggled_lines)) # Restore highlighting self.editor.setSelection(*selection_idxs) def _comment_lines(self, lines): for i in range(len(lines)): lines[i] = '# ' + lines[i] return lines def _uncomment_lines(self, lines): for i in range(len(lines)): uncommented_line = lines[i].replace('# ', '', 1) if uncommented_line == lines[i]: uncommented_line = lines[i].replace('#', '', 1) lines[i] = uncommented_line return lines def _are_comments(self, code_lines): for line in code_lines: if line.strip(): if not line.strip().startswith('#'): return False return True def _setup_editor(self, default_content, filename): editor = self.editor # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) editor.enableAutoCompletion(CodeEditor.AcsAll)
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) def __init__(self, content=None, filename=None, parent=None): """ :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) # layout self.editor = CodeEditor("AlternateCSPythonLexer", self) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(confirm=True) def abort(self): self._presenter.req_abort() def execute_async(self): self._presenter.req_execute_async() def save(self, confirm=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(confirm) else: return True def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def _setup_editor(self, default_content, filename): editor = self.editor # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) editor.enableAutoCompletion(CodeEditor.AcsAll)
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) sig_progress = Signal(int) sig_exec_error = Signal(object) sig_exec_success = Signal(object) def __init__(self, font=None, content=None, filename=None, parent=None): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) self.parent = parent # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.code_commenter = CodeCommenter(self.editor) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_progress.connect(self.sig_progress) self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success) def closeEvent(self, event): self.deleteLater() if self.find_replace_dialog: self.find_replace_dialog.close() super(PythonFileInterpreter, self).closeEvent(event) def show_find_replace_dialog(self): if self.find_replace_dialog is None: self.find_replace_dialog = EmbeddedFindReplaceDialog(self, self.editor) self.layout.insertWidget(0, self.find_replace_dialog.view) self.find_replace_dialog.show() def hide_find_replace_dialog(self): if self.find_replace_dialog is not None: self.find_replace_dialog.hide() @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(prompt_for_confirmation=self.parent.confirm_on_save) def abort(self): self._presenter.req_abort() def execute_async(self, ignore_selection=False): return self._presenter.req_execute_async(ignore_selection) def execute_async_blocking(self): self._presenter.req_execute_async_blocking() def save(self, prompt_for_confirmation=False, force_save=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(prompt_for_confirmation, force_save) else: return True def save_as(self): io = EditorIO(self.editor) new_filename = io.ask_for_filename() if new_filename: return io.write(save_as=new_filename), new_filename else: return False, None def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def replace_tabs_with_spaces(self): self.replace_text(TAB_CHAR, SPACE_CHAR * TAB_WIDTH) def replace_text(self, match_text, replace_text): if self.editor.selectedText() == '': self.editor.selectAll() new_text = self.editor.selectedText().replace(match_text, replace_text) self.editor.replaceSelectedText(new_text) def replace_spaces_with_tabs(self): self.replace_text(SPACE_CHAR * TAB_WIDTH, TAB_CHAR) def set_whitespace_visible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsVisible) def set_whitespace_invisible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsInvisible) def toggle_comment(self): self.code_commenter.toggle_comment() def _setup_editor(self, default_content, filename): editor = self.editor # Clear default QsciScintilla key bindings that we want to allow # to be users of this class self.clear_key_binding("Ctrl+/") # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) editor.enableAutoCompletion(CodeEditor.AcsAll) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str)