Пример #1
0
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())
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
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