class Editor(ttk.Frame): def __init__(self, master): ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) self.notebook = master # type: EditorNotebook # parent of codeview will be workbench so that it can be maximized self._code_view = CodeView( get_workbench(), propose_remove_line_numbers=True, font="EditorFont", text_class=EditorCodeViewText, ) get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget()) self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self) self._code_view.home_widget = self # don't forget home self.maximizable_widget = self._code_view self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._filename = None self._last_known_mtime = None self._asking_about_external_change = False self._code_view.text.bind("<<Modified>>", self._on_text_modified, True) self._code_view.text.bind("<<TextChange>>", self._on_text_change, True) self._code_view.text.bind("<Control-Tab>", self._control_tab, True) get_workbench().bind("DebuggerResponse", self._listen_debugger_progress, True) get_workbench().bind("ToplevelResponse", self._listen_for_toplevel_response, True) self.update_appearance() def get_text_widget(self): return self._code_view.text def get_code_view(self): # TODO: try to get rid of this return self._code_view def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def get_title(self): if self.get_filename() is None: result = "<untitled>" elif is_remote_path(self.get_filename()): path = extract_target_path(self.get_filename()) name = path.split("/")[-1] result = "[ " + name + " ]" else: result = os.path.basename(self.get_filename()) if self.is_modified(): result += " *" return result def check_for_external_changes(self): if self._asking_about_external_change: # otherwise method will be re-entered when focus # changes because of message box return if self._filename is None: return if is_remote_path(self._filename): return try: self._asking_about_external_change = True if self._last_known_mtime is None: return elif not os.path.exists(self._filename): self.master.select(self) if messagebox.askyesno( tr("File is gone"), tr("Looks like '%s' was deleted or moved outside of the editor." ) % self._filename + "\n\n" + tr("Do you want to also close the editor?"), master=self, ): self.master.close_editor(self) else: self.get_text_widget().edit_modified(True) self._last_known_mtime = None elif os.path.getmtime(self._filename) != self._last_known_mtime: self.master.select(self) if messagebox.askyesno( tr("External modification"), tr("Looks like '%s' was modified outside the editor.") % self._filename + "\n\n" + tr("Do you want to discard current editor content and reload the file from disk?" ), master=self, ): cur_line = self.get_text_widget().index("insert") # convert cursor position to line number cur_line = int(float(cur_line)) self._load_file(self._filename, keep_undo=True) self.select_line(cur_line) else: self._last_known_mtime = os.path.getmtime(self._filename) finally: self._asking_about_external_change = False def get_long_description(self): if self._filename is None: result = "<untitled>" else: result = self._filename try: index = self._code_view.text.index("insert") if index and "." in index: line, col = index.split(".") result += " @ {} : {}".format(line, int(col) + 1) except Exception: exception("Finding cursor location") return result def _load_file(self, filename, keep_undo=False): try: if is_remote_path(filename): result = self._load_remote_file(filename) else: result = self._load_local_file(filename, keep_undo) if not result: return False except BinaryFileException: messagebox.showerror("Problem", "%s doesn't look like a text file" % filename, master=self) return False except SyntaxError as e: assert "encoding" in str(e).lower() messagebox.showerror( "Problem loading file", "This file seems to have problems with encoding.\n\n" + "Make sure it is in UTF-8 or contains proper encoding hint.", master=self, ) return False self.update_appearance() return True def _load_local_file(self, filename, keep_undo=False): with open(filename, "rb") as fp: source = fp.read() # Make sure Windows filenames have proper format filename = normpath_with_actual_case(filename) self._filename = filename self.update_file_type() self._last_known_mtime = os.path.getmtime(self._filename) get_workbench().event_generate("Open", editor=self, filename=filename) if not self._code_view.set_content_as_bytes(source, keep_undo): return False self.get_text_widget().edit_modified(False) self._code_view.focus_set() self.master.remember_recent_file(filename) return True def _load_remote_file(self, filename): self._filename = filename self.update_file_type() self._code_view.set_content("") self._code_view.text.set_read_only(True) target_filename = extract_target_path(self._filename) self.update_title() response = get_runner().send_command_and_wait( InlineCommand("read_file", path=target_filename, description=tr("Loading %s") % target_filename), dialog_title=tr("Loading"), ) if response.get("error"): # TODO: make it softer raise RuntimeError(response["error"]) content = response["content_bytes"] self._code_view.text.set_read_only(False) if not self._code_view.set_content_as_bytes(content): return False self.get_text_widget().edit_modified(False) self.update_title() return True def is_modified(self): return bool(self._code_view.text.edit_modified()) def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self, ask_filename=False, save_copy=False, node=None): if self._filename is not None and not ask_filename: save_filename = self._filename get_workbench().event_generate("Save", editor=self, filename=save_filename) else: save_filename = self.ask_new_path(node) if not save_filename: return None if self.notebook.get_editor(save_filename) is not None: messagebox.showerror( "File is open", "This file is already open in Thonny.\n\n" "If you want to save with this name,\n" "close the existing editor first!", master=get_workbench(), ) return None get_workbench().event_generate("SaveAs", editor=self, filename=save_filename, save_copy=save_copy) content_bytes = self._code_view.get_content_as_bytes() if is_remote_path(save_filename): result = self.write_remote_file(save_filename, content_bytes, save_copy) else: result = self.write_local_file(save_filename, content_bytes, save_copy) if not result: return None if not save_copy: self._filename = save_filename self.update_file_type() self.update_title() return save_filename def write_local_file(self, save_filename, content_bytes, save_copy): try: f = open(save_filename, mode="wb") f.write(content_bytes) f.flush() # Force writes on disk, see https://learn.adafruit.com/adafruit-circuit-playground-express/creating-and-editing-code#1-use-an-editor-that-writes-out-the-file-completely-when-you-save-it os.fsync(f) f.close() if not save_copy or save_filename == self._filename: self._last_known_mtime = os.path.getmtime(save_filename) get_workbench().event_generate("LocalFileOperation", path=save_filename, operation="save") except PermissionError: messagebox.showerror( "Permission Error", "Looks like this file or folder is not writable.", master=self) return False if not save_copy or save_filename == self._filename: self.master.remember_recent_file(save_filename) if not save_copy or save_filename == self._filename: self._code_view.text.edit_modified(False) return True def update_file_type(self): if self._filename is None: self._code_view.set_file_type(None) else: ext = self._filename.split(".")[-1].lower() if ext in PYTHON_EXTENSIONS: file_type = "python" elif ext in PYTHONLIKE_EXTENSIONS: file_type = "pythonlike" else: file_type = None self._code_view.set_file_type(file_type) self.update_appearance() def write_remote_file(self, save_filename, content_bytes, save_copy): if get_runner().ready_for_remote_file_operations(show_message=True): target_filename = extract_target_path(save_filename) get_runner().send_command_and_wait( InlineCommand( "write_file", path=target_filename, content_bytes=content_bytes, editor_id=id(self), blocking=True, description=tr("Saving to %s") % target_filename, ), dialog_title=tr("Saving"), ) if not save_copy: self._code_view.text.edit_modified(False) self.update_title() # NB! edit_modified is not falsed yet! get_workbench().event_generate("RemoteFileOperation", path=target_filename, operation="save") get_workbench().event_generate("RemoteFilesChanged") return True else: return False def ask_new_path(self, node=None): if node is None: node = choose_node_for_file_operations(self.winfo_toplevel(), "Where to save to?") if not node: return None if node == "local": return self.ask_new_local_path() else: assert node == "remote" return self.ask_new_remote_path() def ask_new_remote_path(self): target_path = ask_backend_path(self.winfo_toplevel(), "save") if target_path: return make_remote_path(target_path) else: return None def ask_new_local_path(self): if self._filename is None: initialdir = get_workbench().get_local_cwd() initialfile = None else: initialdir = os.path.dirname(self._filename) initialfile = os.path.basename(self._filename) # http://tkinter.unpythonic.net/wiki/tkFileDialog new_filename = asksaveasfilename( filetypes=_dialog_filetypes, defaultextension=".py", initialdir=initialdir, initialfile=initialfile, parent=get_workbench(), ) # Different tkinter versions may return different values if new_filename in ["", (), None]: return None # Seems that in some Python versions defaultextension # acts funny if new_filename.lower().endswith(".py.py"): new_filename = new_filename[:-3] if running_on_windows(): # may have /-s instead of \-s and wrong case new_filename = os.path.join( normpath_with_actual_case(os.path.dirname(new_filename)), os.path.basename(new_filename), ) if new_filename.endswith(".py"): base = os.path.basename(new_filename) mod_name = base[:-3].lower() if running_on_windows(): mod_name = mod_name.lower() if mod_name in [ "math", "turtle", "random", "statistics", "pygame", "matplotlib", "numpy", ]: # More proper name analysis will be performed by ProgramNamingAnalyzer if not tk.messagebox.askyesno( "Potential problem", "If you name your script '%s', " % base + "you won't be able to import the library module named '%s'" % mod_name + ".\n\n" + "Do you still want to use this name for your script?", master=self, ): return self.ask_new_local_path() return new_filename def show(self): self.master.select(self) def update_appearance(self): self._code_view.set_gutter_visibility( get_workbench().get_option("view.show_line_numbers") or get_workbench().in_simple_mode()) self._code_view.set_line_length_margin( get_workbench().get_option("view.recommended_line_length")) self._code_view.text.update_tabs() self._code_view.text.event_generate("<<UpdateAppearance>>") def _listen_debugger_progress(self, event): # Go read-only # TODO: check whether this module is active? self._code_view.text.set_read_only(True) def _listen_for_toplevel_response(self, event: ToplevelResponse) -> None: self._code_view.text.set_read_only(False) def _control_tab(self, event): if event.state & 1: # shift was pressed direction = -1 else: direction = 1 self.master.select_next_prev_editor(direction) return "break" def _shift_control_tab(self, event): self.master.select_next_prev_editor(-1) return "break" def select_range(self, text_range): self._code_view.select_range(text_range) def select_line(self, lineno, col_offset=None): self._code_view.select_range(TextRange(lineno, 0, lineno + 1, 0)) self.see_line(lineno) if col_offset is None: col_offset = 0 self.get_text_widget().mark_set("insert", "%d.%d" % (lineno, col_offset)) def see_line(self, lineno): # first see an earlier line in order to push target line downwards self._code_view.text.see("%d.0" % max(lineno - 4, 1)) self._code_view.text.see("%d.0" % lineno) def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text def _on_text_modified(self, event): self.update_title() def update_title(self): try: self.master.update_editor_title(self) except Exception as e: logger.exception("Could not update editor title", exc_info=e) def _on_text_change(self, event): self.update_title() def destroy(self): get_workbench().unbind("DebuggerResponse", self._listen_debugger_progress) get_workbench().unbind("ToplevelResponse", self._listen_for_toplevel_response) ttk.Frame.destroy(self) get_workbench().event_generate("EditorTextDestroyed", editor=self, text_widget=self.get_text_widget())
class Editor(ttk.Frame): def __init__(self, master, filename=None): ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) # parent of codeview will be workbench so that it can be maximized self._code_view = CodeView(get_workbench(), propose_remove_line_numbers=True, font=get_workbench().get_font("EditorFont")) get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget()) self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self) self._code_view.home_widget = self # don't forget home self.maximizable_widget = self._code_view self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._filename = None if filename is not None: self._load_file(filename) self._code_view.text.edit_modified(False) self._code_view.text.bind("<<Modified>>", self._on_text_modified, True) self._code_view.text.bind("<<TextChange>>", self._on_text_change, True) self._code_view.text.bind("<Control-Tab>", self._control_tab, True) get_workbench().bind("AfterKnownMagicCommand", self._listen_for_execute, True) get_workbench().bind("ToplevelResult", self._listen_for_toplevel_result, True) self.update_appearance() def get_text_widget(self): return self._code_view.text def get_code_view(self): # TODO: try to get rid of this return self._code_view def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def get_long_description(self): if self._filename is None: result = "<untitled>" else: result = self._filename try: index = self._code_view.text.index("insert") if index and "." in index: line, col = index.split(".") result += " @ {} : {}".format(line, int(col) + 1) except: exception("Finding cursor location") return result def _load_file(self, filename): with tokenize.open(filename) as fp: # TODO: support also text files source = fp.read() self._filename = filename get_workbench().event_generate("Open", editor=self, filename=filename) self._code_view.set_content(source) self._code_view.focus_set() self.master.remember_recent_file(filename) def is_modified(self): return self._code_view.text.edit_modified() def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self, ask_filename=False): if self._filename is not None and not ask_filename: filename = self._filename get_workbench().event_generate("Save", editor=self, filename=filename) else: # http://tkinter.unpythonic.net/wiki/tkFileDialog filename = asksaveasfilename( filetypes=_dialog_filetypes, defaultextension=".py", initialdir=get_workbench().get_option("run.working_directory")) if filename in [ "", (), None ]: # Different tkinter versions may return different values return None # Seems that in some Python versions defaultextension # acts funny if filename.lower().endswith(".py.py"): filename = filename[:-3] get_workbench().event_generate("SaveAs", editor=self, filename=filename) content = self._code_view.get_content() encoding = "UTF-8" # TODO: check for marker in the head of the code try: f = open( filename, mode="wb", ) f.write(content.encode(encoding)) f.close() except PermissionError: if askyesno( "Permission Error", "Looks like this file or folder is not writable.\n\n" + "Do you want to save under another folder and/or filename?" ): return self.save_file(True) else: return None self._filename = filename self.master.remember_recent_file(filename) self._code_view.text.edit_modified(False) return self._filename def show(self): self.master.select(self) def update_appearance(self): self._code_view.set_line_numbers( get_workbench().get_option("view.show_line_numbers")) self._code_view.set_line_length_margin( get_workbench().get_option("view.recommended_line_length")) self._code_view.text.event_generate("<<UpdateAppearance>>") def _listen_for_execute(self, event): command, args = parse_shell_command(event.cmd_line) # Go read-only if command.lower() == "debug": if len(args) == 0: return filename = args[0] self_filename = self.get_filename() if self_filename is not None and os.path.basename( self_filename) == filename: # Not that command has only basename # so this solution may make more editors read-only than necessary self._code_view.text.set_read_only(True) def _listen_for_toplevel_result(self, event): self._code_view.text.set_read_only(False) def _control_tab(self, event): if event.state & 1: # shift was pressed direction = -1 else: direction = 1 self.master.select_next_prev_editor(direction) return "break" def _shift_control_tab(self, event): self.master.select_next_prev_editor(-1) return "break" def select_range(self, text_range): self._code_view.select_range(text_range) def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text def _on_text_modified(self, event): self.master.update_editor_title(self) def _on_text_change(self, event): self.master.update_editor_title(self) runner = get_runner() if (runner.get_state() in [ "running", "waiting_input", "waiting_debugger_command" ] and isinstance(runner.get_current_command(), (ToplevelCommand, DebuggerCommand))): # exclude running InlineCommands runner.interrupt_backend() def destroy(self): get_workbench().unbind("AfterKnownMagicCommand", self._listen_for_execute) get_workbench().unbind("ToplevelResult", self._listen_for_toplevel_result) ttk.Frame.destroy(self)
class Editor(ttk.Frame): def __init__(self, master, filename=None): ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) # parent of codeview will be workbench so that it can be maximized self._code_view = CodeView(get_workbench(), propose_remove_line_numbers=True, font=get_workbench().get_font("EditorFont")) get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget()) self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self) self._code_view.home_widget = self # don't forget home self.maximizable_widget = self._code_view self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._filename = None self.file_encoding = None if filename is not None: self._load_file(filename) self._code_view.text.edit_modified(False) self._code_view.text.bind("<<Modified>>", lambda e: master.update_editor_title(self), True) self._code_view.text.bind("<Control-Tab>", self._control_tab, True) get_workbench().bind("AfterKnownMagicCommand", self._listen_for_execute, True) get_workbench().bind("ToplevelResult", self._listen_for_toplevel_result, True) self.update_appearance() def get_text_widget(self): return self._code_view.text def get_code_view(self): # TODO: try to get rid of this return self._code_view def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def _load_file(self, filename): source, self.file_encoding = misc_utils.read_python_file( filename) # TODO: support also text files self._filename = filename get_workbench().event_generate("Open", editor=self, filename=filename) self._code_view.set_content(source) self._code_view.focus_set() self.master.remember_recent_file(filename) def is_modified(self): return self._code_view.text.edit_modified() def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self, ask_filename=False): if self._filename is not None and not ask_filename: filename = self._filename get_workbench().event_generate("Save", editor=self, filename=filename) else: # http://tkinter.unpythonic.net/wiki/tkFileDialog filename = asksaveasfilename( filetypes=_dialog_filetypes, defaultextension=".py", initialdir=get_workbench().get_option("run.working_directory")) if filename in [ "", (), None ]: # Different tkinter versions may return different values return None # Seems that in some Python versions defaultextension # acts funny if filename.lower().endswith(".py.py"): filename = filename[:-3] get_workbench().event_generate("SaveAs", editor=self, filename=filename) content = self._code_view.get_content() encoding = self.file_encoding or "UTF-8" f = open( filename, mode="wb", ) f.write(content.encode(encoding)) f.close() self._filename = filename self.master.remember_recent_file(filename) self._code_view.text.edit_modified(False) return self._filename def show(self): self.master.select(self) def update_appearance(self): self._code_view.set_line_numbers( get_workbench().get_option("view.show_line_numbers")) self._code_view.set_line_length_margin( get_workbench().get_option("view.recommended_line_length")) self._code_view.text.event_generate("<<UpdateAppearance>>") def _listen_for_execute(self, event): command, args = parse_shell_command(event.cmd_line) if command.lower() in ["run", "debug"]: if len(args) == 0: return filename = args[0] self_filename = self.get_filename() if self_filename is not None and os.path.basename( self_filename) == filename: # Not that command has only basename # so this solution may make more editors read-only than necessary self._code_view.text.set_read_only(True) def _listen_for_toplevel_result(self, event): self._code_view.text.set_read_only(False) def _control_tab(self, event): if event.state & 1: # shift was pressed direction = -1 else: direction = 1 self.master.select_next_prev_editor(direction) return "break" def _shift_control_tab(self, event): self.master.select_next_prev_editor(-1) return "break" def select_range(self, text_range): self._code_view.select_range(text_range) def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text def destroy(self): get_workbench().unbind("AfterKnownMagicCommand", self._listen_for_execute) get_workbench().unbind("ToplevelResult", self._listen_for_toplevel_result) ttk.Frame.destroy(self)
class Editor(ttk.Frame): def __init__(self, master, filename=None): ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) # parent of codeview will be workbench so that it can be maximized self._code_view = CodeView(get_workbench(), propose_remove_line_numbers=True, font="EditorFont") get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget()) self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self) self._code_view.home_widget = self # don't forget home self.maximizable_widget = self._code_view self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._filename = None self._last_known_mtime = None self._asking_about_external_change = False if filename is not None: self._load_file(filename) self._code_view.text.edit_modified(False) self._code_view.text.bind("<<Modified>>", self._on_text_modified, True) self._code_view.text.bind("<<TextChange>>", self._on_text_change, True) self._code_view.text.bind("<Control-Tab>", self._control_tab, True) get_workbench().bind("DebuggerResponse", self._listen_debugger_progress, True) get_workbench().bind("ToplevelResponse", self._listen_for_toplevel_response, True) self.update_appearance() def get_text_widget(self): return self._code_view.text def get_code_view(self): # TODO: try to get rid of this return self._code_view def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def get_title(self): if self.get_filename() is None: result = "<untitled>" else: result = os.path.basename(self.get_filename()) if self.is_modified(): result += " *" return result def check_for_external_changes(self): if self._asking_about_external_change: # otherwise method will be re-entered when focus # changes because of message box return if self._filename is None: return try: self._asking_about_external_change = True if self._last_known_mtime is None: return elif not os.path.exists(self._filename): self.master.select(self) if messagebox.askyesno( "File is gone", "Looks like '%s' was deleted or moved outside Thonny.\n\n" % self._filename + "Do you want to also close this editor?", parent=get_workbench()): self.master.close_editor(self) else: self.get_text_widget().edit_modified(True) self._last_known_mtime = None elif os.path.getmtime(self._filename) != self._last_known_mtime: self.master.select(self) if messagebox.askyesno( "External modification", "Looks like '%s' was modified outside Thonny.\n\n" % self._filename + "Do you want to discard current editor content and reload the file from disk?", parent=get_workbench(), ): self._load_file(self._filename, keep_undo=True) else: self._last_known_mtime = os.path.getmtime(self._filename) finally: self._asking_about_external_change = False def get_long_description(self): if self._filename is None: result = "<untitled>" else: result = self._filename try: index = self._code_view.text.index("insert") if index and "." in index: line, col = index.split(".") result += " @ {} : {}".format(line, int(col) + 1) except Exception: exception("Finding cursor location") return result def _load_file(self, filename, keep_undo=False): with tokenize.open(filename) as fp: # TODO: support also text files source = fp.read() # Make sure Windows filenames have proper format filename = normpath_with_actual_case(filename) self._filename = filename self._last_known_mtime = os.path.getmtime(self._filename) get_workbench().event_generate("Open", editor=self, filename=filename) self._code_view.set_content(source, keep_undo) self.get_text_widget().edit_modified(False) self._code_view.focus_set() self.master.remember_recent_file(filename) def is_modified(self): return self._code_view.text.edit_modified() def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self, ask_filename=False): if self._filename is not None and not ask_filename: get_workbench().event_generate("Save", editor=self, filename=self._filename) else: # http://tkinter.unpythonic.net/wiki/tkFileDialog new_filename = asksaveasfilename( master=get_workbench(), filetypes=_dialog_filetypes, defaultextension=".py", initialdir=get_workbench().get_cwd(), ) # Different tkinter versions may return different values if new_filename in [ "", (), None, ]: return None # Seems that in some Python versions defaultextension # acts funny if new_filename.lower().endswith(".py.py"): new_filename = new_filename[:-3] if running_on_windows(): # may have /-s instead of \-s and wrong case new_filename = os.path.join( normpath_with_actual_case(os.path.dirname(new_filename)), os.path.basename(new_filename)) if new_filename.endswith(".py"): base = os.path.basename(new_filename) mod_name = base[:-3].lower() if running_on_windows(): mod_name = mod_name.lower() if mod_name in [ "math", "turtle", "random", "statistics", "pygame", "matplotlib", "numpy", ]: # More proper name analysis will be performed by ProgramNamingAnalyzer if not tk.messagebox.askyesno( "Potential problem", "If you name your script '%s', " % base + "you won't be able to import the library module named '%s'" % mod_name + ".\n\n" + "Do you still want to use this name for your script?", parent=get_workbench(), ): return self.save_file(ask_filename) self._filename = new_filename get_workbench().event_generate("SaveAs", editor=self, filename=new_filename) content = self._code_view.get_content_as_bytes() try: f = open(self._filename, mode="wb") f.write(content) f.flush() # Force writes on disk, see https://learn.adafruit.com/adafruit-circuit-playground-express/creating-and-editing-code#1-use-an-editor-that-writes-out-the-file-completely-when-you-save-it os.fsync(f) f.close() self._last_known_mtime = os.path.getmtime(self._filename) except PermissionError: if askyesno( "Permission Error", "Looks like this file or folder is not writable.\n\n" + "Do you want to save under another folder and/or filename?", parent=get_workbench(), ): return self.save_file(True) else: return None self.master.remember_recent_file(self._filename) self._code_view.text.edit_modified(False) return self._filename def show(self): self.master.select(self) def update_appearance(self): self._code_view.set_gutter_visibility( get_workbench().get_option("view.show_line_numbers") or get_workbench().get_ui_mode() == "simple") self._code_view.set_line_length_margin( get_workbench().get_option("view.recommended_line_length")) self._code_view.text.event_generate("<<UpdateAppearance>>") def _listen_debugger_progress(self, event): # Go read-only # TODO: check whether this module is active? self._code_view.text.set_read_only(True) def _listen_for_toplevel_response(self, event: ToplevelResponse) -> None: self._code_view.text.set_read_only(False) def _control_tab(self, event): if event.state & 1: # shift was pressed direction = -1 else: direction = 1 self.master.select_next_prev_editor(direction) return "break" def _shift_control_tab(self, event): self.master.select_next_prev_editor(-1) return "break" def select_range(self, text_range): self._code_view.select_range(text_range) def select_line(self, lineno, col_offset=None): self._code_view.select_range(TextRange(lineno, 0, lineno + 1, 0)) self.see_line(lineno) if col_offset is None: col_offset = 0 self.get_text_widget().mark_set("insert", "%d.%d" % (lineno, col_offset)) def see_line(self, lineno): # first see an earlier line in order to push target line downwards self._code_view.text.see("%d.0" % max(lineno - 4, 1)) self._code_view.text.see("%d.0" % lineno) def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text def _on_text_modified(self, event): try: self.master.update_editor_title(self) except Exception: traceback.print_exc() def _on_text_change(self, event): self.master.update_editor_title(self) def destroy(self): get_workbench().unbind("DebuggerResponse", self._listen_debugger_progress) get_workbench().unbind("ToplevelResponse", self._listen_for_toplevel_response) ttk.Frame.destroy(self)
class Editor(ttk.Frame): """ Text editor and visual part of module stepper """ def __init__(self, master, workbench, filename=None): self._workbench = workbench ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) self._code_view = CodeView(self, workbench, propose_remove_line_numbers=True) self._code_view.grid(sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._stepper = None self._filename = None self.file_encoding = None if filename is not None: self._load_file(filename) self._code_view.text.edit_modified(False) self._code_view.text.bind("<<Modified>>", lambda _: self.event_generate(EDITOR_STATE_CHANGE), "+") def get_filename(self, try_hard=False): if self._filename is None and try_hard: self._cmd_save_file() return self._filename def _load_file(self, filename): source, self.file_encoding = misc_utils.read_python_file(filename) # TODO: support also text files self._filename = filename self._code_view.modified_since_last_save = False self._workbench.event_generate("Open", editor=self, filename=filename) self._code_view.set_content(source) self._code_view.focus_set() def is_modified(self): return self._code_view.modified_since_last_save def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self): if self._filename is not None: filename = self._filename self._workbench.event_generate("Save", editor=self) else: # http://tkinter.unpythonic.net/wiki/tkFileDialog filename = asksaveasfilename ( filetypes = _dialog_filetypes, defaultextension = ".py", initialdir = self._workbench.get_option("run.working_directory") ) if filename == "": return None self._workbench.event_generate("SaveAs", editor=self, filename=filename) content = self._code_view.get_content() encoding = self.file_encoding or "UTF-8" f = open(filename, mode="wb", ) f.write(content.encode(encoding)) f.close() self._code_view.modified_since_last_save = False self._filename = filename self._code_view.text.edit_modified(False) self.event_generate(EDITOR_STATE_CHANGE) return self._filename def change_font_size(self, delta): self._code_view.change_font_size(delta) def show(self): self.master.select(self) def handle_vm_message(self, msg): assert isinstance(msg, DebuggerResponse) if self.is_modified(): raise RuntimeError ("Can't show debug info in modified editor") """ # actually this check is not sound, as this_frame.source is not guaranteed # to be saved at code compilation time if frame.source != self._code_view.get_content(): print("Editor content>" + self._code_view.get_content() + "<") print("frame.source>" + frame.source + "<") raise RuntimeError ("Editor content doesn't match module source. Did you change it after starting the program?") """ if self._stepper is None: self._stepper = StatementStepper(msg.frame_id, self, self._workbench, self._code_view) self._stepper.handle_vm_message(msg) def select_range(self, text_range): self._code_view.select_range(text_range) def enter_execution_mode(self): self._code_view.enter_execution_mode() def clear_debug_view(self): if self._stepper is not None: self._stepper.clear_debug_view() def exit_execution_mode(self): self.clear_debug_view() self._code_view.exit_execution_mode() self._stepper = None def get_frame_id(self): if self._stepper is None: return None else: return self._stepper.frame_id def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text