From 16a589125a2b47e35fd4a9dcf638a83522dec06a Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Fri, 16 Sep 2022 14:27:06 -0500 Subject: [PATCH] Bring up to date with mainline CQ-Editor 774c3c6 --- cq_editor/editor.py | 303 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 cq_editor/editor.py diff --git a/cq_editor/editor.py b/cq_editor/editor.py new file mode 100644 index 0000000..eb70537 --- /dev/null +++ b/cq_editor/editor.py @@ -0,0 +1,303 @@ +import os +import spyder.utils.encoding +from modulefinder import ModuleFinder + +from spyder.plugins.editor.widgets.codeeditor import CodeEditor +from PyQt5.QtCore import pyqtSignal, QFileSystemWatcher, QTimer +from PyQt5.QtWidgets import QAction, QFileDialog +from PyQt5.QtGui import QFontDatabase +from path import Path + +import sys + +from pyqtgraph.parametertree import Parameter + +from ..mixins import ComponentMixin +from ..utils import get_save_filename, get_open_filename, confirm + +from ..icons import icon + +class Editor(CodeEditor,ComponentMixin): + + name = 'Code Editor' + + # This signal is emitted whenever the currently-open file changes and + # autoreload is enabled. + triggerRerender = pyqtSignal(bool) + sigFilenameChanged = pyqtSignal(str) + + preferences = Parameter.create(name='Preferences',children=[ + {'name': 'Font size', 'type': 'int', 'value': 12}, + {'name': 'Autoreload', 'type': 'bool', 'value': False}, + {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, + {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False}, + {'name': 'Line wrap', 'type': 'bool', 'value': False}, + {'name': 'Color scheme', 'type': 'list', + 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) + + EXTENSIONS = 'py' + + def __init__(self,parent=None): + + self._watched_file = None + + super(Editor,self).__init__(parent) + ComponentMixin.__init__(self) + + self.setup_editor(linenumbers=True, + markers=True, + edge_line=False, + tab_mode=False, + show_blanks=True, + font=QFontDatabase.systemFont(QFontDatabase.FixedFont), + language='Python', + filename='') + + self._actions = \ + {'File' : [QAction(icon('new'), + 'New', + self, + shortcut='ctrl+N', + triggered=self.new), + QAction(icon('open'), + 'Open', + self, + shortcut='ctrl+O', + triggered=self.open), + QAction(icon('save'), + 'Save', + self, + shortcut='ctrl+S', + triggered=self.save), + QAction(icon('save_as'), + 'Save as', + self, + shortcut='ctrl+shift+S', + triggered=self.save_as), + QAction(icon('autoreload'), + 'Automatic reload and preview', + self,triggered=self.autoreload, + checkable=True, + checked=False, + objectName='autoreload'), + ]} + + for a in self._actions.values(): + self.addActions(a) + + + self._fixContextMenu() + + # autoreload support + self._file_watcher = QFileSystemWatcher(self) + # we wait for 50ms after a file change for the file to be written completely + self._file_watch_timer = QTimer(self) + self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + self._file_watch_timer.setSingleShot(True) + self._file_watcher.fileChanged.connect( + lambda val: self._file_watch_timer.start()) + self._file_watch_timer.timeout.connect(self._file_changed) + + self.updatePreferences() + + def _fixContextMenu(self): + + menu = self.menu + + menu.removeAction(self.run_cell_action) + menu.removeAction(self.run_cell_and_advance_action) + menu.removeAction(self.run_selection_action) + menu.removeAction(self.re_run_last_cell_action) + + def updatePreferences(self,*args): + + self.set_color_scheme(self.preferences['Color scheme']) + + font = self.font() + font.setPointSize(self.preferences['Font size']) + self.set_font(font) + + self.findChild(QAction, 'autoreload') \ + .setChecked(self.preferences['Autoreload']) + + self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) + + self.toggle_wrap_mode(self.preferences['Line wrap']) + + self._clear_watched_paths() + self._watch_paths() + + def confirm_discard(self): + + if self.modified: + rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?') + else: + rv = True + + return rv + + def new(self): + + if not self.confirm_discard(): return + + self.set_text('') + self.filename = '' + self.reset_modified() + + def open(self): + + if not self.confirm_discard(): return + + curr_dir = Path(self.filename).abspath().dirname() + fname = get_open_filename(self.EXTENSIONS, curr_dir) + if fname != '': + self.load_from_file(fname) + + def load_from_file(self,fname): + + self.set_text_from_file(fname) + self.filename = fname + self.reset_modified() + + def determine_encoding(self, fname): + if os.path.exists(fname): + # this function returns the encoding spyder used to read the file + _, encoding = spyder.utils.encoding.read(fname) + # spyder returns a -guessed suffix in some cases + return encoding.replace('-guessed', '') + else: + return 'utf-8' + + def save(self): + + if self._filename != '': + + if self.preferences['Autoreload']: + self._file_watcher.blockSignals(True) + self._file_watch_timer.stop() + + encoding = self.determine_encoding(self._filename) + encoded = self.toPlainText().encode(encoding) + with open(self._filename, 'wb') as f: + f.write(encoded) + + if self.preferences['Autoreload']: + self._file_watcher.blockSignals(False) + self.triggerRerender.emit(True) + + self.reset_modified() + + else: + self.save_as() + + def save_as(self): + + fname = get_save_filename(self.EXTENSIONS) + if fname != '': + encoded = self.toPlainText().encode('utf-8') + with open(fname, 'wb') as f: + f.write(encoded) + self.filename = fname + + self.reset_modified() + + def _update_filewatcher(self): + if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): + self._clear_watched_paths() + self._watched_file = None + if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: + self._watched_file = self._filename + self._watch_paths() + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, fname): + self._filename = fname + self._update_filewatcher() + self.sigFilenameChanged.emit(fname) + + def _clear_watched_paths(self): + paths = self._file_watcher.files() + if paths: + self._file_watcher.removePaths(paths) + + def _watch_paths(self): + if Path(self._filename).exists(): + self._file_watcher.addPath(self._filename) + if self.preferences['Autoreload: watch imported modules']: + module_paths = self.get_imported_module_paths(self._filename) + if module_paths: + self._file_watcher.addPaths(module_paths) + + # callback triggered by QFileSystemWatcher + def _file_changed(self): + # neovim writes a file by removing it first so must re-add each time + self._watch_paths() + self.set_text_from_file(self._filename) + self.triggerRerender.emit(True) + + # Turn autoreload on/off. + def autoreload(self, enabled): + self.preferences['Autoreload'] = enabled + self._update_filewatcher() + + def reset_modified(self): + + self.document().setModified(False) + + @property + def modified(self): + + return self.document().isModified() + + def saveComponentState(self,store): + + if self.filename != '': + store.setValue(self.name+'/state',self.filename) + + def restoreComponentState(self,store): + + filename = store.value(self.name+'/state') + + if filename and self.filename == '': + try: + self.load_from_file(filename) + except IOError: + self._logger.warning(f'could not open {filename}') + + + def get_imported_module_paths(self, module_path): + + finder = ModuleFinder([os.path.dirname(module_path)]) + imported_modules = [] + + try: + finder.run_script(module_path) + except SyntaxError as err: + self._logger.warning(f'Syntax error in {module_path}: {err}') + except Exception as err: + self._logger.warning( + f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}' + ) + else: + for module_name, module in finder.modules.items(): + if module_name != '__main__': + path = getattr(module, '__file__', None) + if path is not None and os.path.isfile(path): + imported_modules.append(path) + + return imported_modules + + +if __name__ == "__main__": + + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + editor = Editor() + editor.show() + + sys.exit(app.exec_())