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): 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, cursor=get_beam_cursor(), ) 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_content(self) -> str: return self._code_view.get_content() def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def get_identifier(self): if self._filename: return self._filename else: return str(self.winfo_id()) def get_title(self): if self.get_filename() is None: result = tr("<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 = tr("<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) -> str: 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: target_path = self._check_add_py_extension(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) # https://tcl.tk/man/tcl8.6/TkCmd/getOpenFile.htm type_var = tk.StringVar(value="") new_filename = asksaveasfilename( filetypes=_dialog_filetypes, defaultextension=None, initialdir=initialdir, initialfile=initialfile, parent=get_workbench(), typevariable=type_var, ) logger.info("Save dialog returned %r with typevariable %r", new_filename, type_var.get()) # Different tkinter versions may return different values if new_filename in ["", (), None]: return None 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 type_var.get() == PYTHON_FILES_STR or type_var.get() == "": new_filename = self._check_add_py_extension( new_filename, without_asking=type_var.get() == PYTHON_FILES_STR ) 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() ) def _check_add_py_extension(self, path: str, without_asking: bool = False) -> str: assert path parts = re.split(r"[/\\]", path) name = parts[-1] if "." not in name: if without_asking or messagebox.askyesno( title=tr("Confirmation"), message=tr("Python files usually have .py extension.") + "\n\n" + tr("Did you mean '%s'?" % (name + ".py")), parent=self.winfo_toplevel(), ): return path + ".py" else: return path return path
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