<filename>thonny/plugins/backend_config_page.py
import tkinter as tk
from tkinter import ttk

from thonny import get_runner, get_workbench
from thonny.config_ui import ConfigurationPage
from thonny.ui_utils import create_string_var


class BackendDetailsConfigPage(ConfigurationPage):
    def should_restart(self):
        raise NotImplementedError()

    def _add_text_field(self, label_text, variable_name, row, show=None):
        entry_label = ttk.Label(self, text=label_text)
        entry_label.grid(row=row, column=0, sticky="w")

        variable = create_string_var(get_workbench().get_option(variable_name), self._on_change)
        entry = ttk.Entry(self, textvariable=variable, show=show)
        entry.grid(row=row + 1, column=0, sticky="we")
        return variable


class OnlyTextConfigurationPage(BackendDetailsConfigPage):
    def __init__(self, master, text):
        super().__init__(master)
        label = ttk.Label(self, text=text)
        label.grid()

    def should_restart(self):
        return False


class BackendConfigurationPage(ConfigurationPage):
    def __init__(self, master):
        ConfigurationPage.__init__(self, master)

        self._backend_specs_by_desc = {
            spec.description: spec for spec in get_workbench().get_backends().values()
        }
        self._conf_pages = {}
        self._current_page = None

        current_backend_name = get_workbench().get_option("run.backend_name")
        try:
            current_backend_desc = get_workbench().get_backends()[current_backend_name].description
        except KeyError:
            current_backend_desc = ""

        self._combo_variable = create_string_var(current_backend_desc)

        label = ttk.Label(
            self, text=_("Which interpreter or device should Thonny use for running your code?")
        )
        label.grid(row=0, column=0, columnspan=2, sticky=tk.W)

        sorted_backend_specs = sorted(
            self._backend_specs_by_desc.values(), key=lambda x: x.sort_key
        )

        self._combo = ttk.Combobox(
            self,
            exportselection=False,
            textvariable=self._combo_variable,
            values=[spec.description for spec in sorted_backend_specs],
            height=25,
        )

        self._combo.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW, pady=(0, 10))
        self._combo.state(["!disabled", "readonly"])

        self.labelframe = ttk.LabelFrame(self, text=" " + _("Details") + " ")
        self.labelframe.grid(row=2, column=0, sticky="nsew")
        self.labelframe.columnconfigure(0, weight=1)
        self.labelframe.rowconfigure(0, weight=1)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)

        self._combo_variable.trace("w", self._backend_changed)
        self._backend_changed()

    def _backend_changed(self, *args):
        backend_desc = self._combo_variable.get()

        if backend_desc == "":
            if self._current_page is not None:
                self._current_page.grid_forget()
            return

        page = self._get_conf_page(backend_desc)

        if page != self._current_page:

            if self._current_page is not None:
                self._current_page.grid_forget()

            page.grid(sticky="nsew", padx=10, pady=5)
            self._current_page = page

    def _get_conf_page(self, backend_desc):
        if backend_desc not in self._conf_pages:
            cp_constructor = self._backend_specs_by_desc[backend_desc].config_page_constructor
            if isinstance(cp_constructor, str):
                self._conf_pages[backend_desc] = OnlyTextConfigurationPage(
                    self.labelframe, cp_constructor
                )
            else:
                assert issubclass(cp_constructor, ConfigurationPage)
                self._conf_pages[backend_desc] = cp_constructor(self.labelframe)

        return self._conf_pages[backend_desc]

    def apply(self):
        if self._current_page is None:
            return None

        result = self._current_page.apply()

        if result is False:
            return False

        backend_desc = self._combo_variable.get()
        backend_name = self._backend_specs_by_desc[backend_desc].name
        get_workbench().set_option("run.backend_name", backend_name)

        if getattr(self._combo_variable, "modified") or self._current_page.should_restart():
            get_runner().restart_backend(False)

        return None


class BaseSshProxyConfigPage(BackendDetailsConfigPage):
    backend_name = None  # Will be overwritten on Workbench.add_backend

    def __init__(self, master, conf_group):
        super().__init__(master)
        self._changed = False
        self._conf_group = conf_group

        self._host_var = self._add_text_field("Host", self._conf_group + ".host", 1)
        self._user_var = self._add_text_field("Username", self._conf_group + ".user", 3)
        self._password_var = self._add_text_field(
            "Password", self._conf_group + ".password", 5, show="•"
        )
        self._executable_var = self._add_text_field(
            "Interpreter", self._conf_group + ".executable", 30
        )

    def _on_change(self):
        self._changed = True

    def apply(self):
        get_workbench().set_option(self._conf_group + ".host", self._host_var.get())
        get_workbench().set_option(self._conf_group + ".user", self._user_var.get())
        get_workbench().set_option(self._conf_group + ".password", self._password_var.get())
        get_workbench().set_option(self._conf_group + ".executable", self._executable_var.get())

        if self._changed:
            # reset cwd setting to default
            get_workbench().set_option(self._conf_group + ".cwd", "")

    def should_restart(self):
        return self._changed


def load_plugin() -> None:
    def select_device():
        get_workbench().show_options("interpreter")

    get_workbench().add_configuration_page(
        "interpreter", _("Interpreter"), BackendConfigurationPage, 20
    )
    get_workbench().add_command(
        "select_interpreter", "run", _("Select interpreter") + "...", select_device, group=1
    )

<filename>thonny/plugins/backend_config_page.py
import tkinter as tk
from tkinter import ttk

from thonny import get_runner, get_workbench
from thonny.config_ui import ConfigurationPage
from thonny.ui_utils import create_string_var


class BackendDetailsConfigPage(ConfigurationPage):
    def should_restart(self):
        raise NotImplementedError()

    def _add_text_field(self, label_text, variable_name, row, show=None):
        entry_label = ttk.Label(self, text=label_text)
        entry_label.grid(row=row, column=0, sticky="w")

        variable = create_string_var(get_workbench().get_option(variable_name), self._on_change)
        entry = ttk.Entry(self, textvariable=variable, show=show)
        entry.grid(row=row + 1, column=0, sticky="we")
        return variable


class OnlyTextConfigurationPage(BackendDetailsConfigPage):
    def __init__(self, master, text):
        super().__init__(master)
        label = ttk.Label(self, text=text)
        label.grid()

    def should_restart(self):
        return False


class BackendConfigurationPage(ConfigurationPage):
    def __init__(self, master):
        ConfigurationPage.__init__(self, master)

        self._backend_specs_by_desc = {
            spec.description: spec for spec in get_workbench().get_backends().values()
        }
        self._conf_pages = {}
        self._current_page = None

        current_backend_name = get_workbench().get_option("run.backend_name")
        try:
            current_backend_desc = get_workbench().get_backends()[current_backend_name].description
        except KeyError:
            current_backend_desc = ""

        self._combo_variable = create_string_var(current_backend_desc)

        label = ttk.Label(
            self, text=_("Which interpreter or device should Thonny use for running your code?")
        )
        label.grid(row=0, column=0, columnspan=2, sticky=tk.W)

        sorted_backend_specs = sorted(
            self._backend_specs_by_desc.values(), key=lambda x: x.sort_key
        )

        self._combo = ttk.Combobox(
            self,
            exportselection=False,
            textvariable=self._combo_variable,
            values=[spec.description for spec in sorted_backend_specs],
            height=25,
        )

        self._combo.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW, pady=(0, 10))
        self._combo.state(["!disabled", "readonly"])

        self.labelframe = ttk.LabelFrame(self, text=" " + _("Details") + " ")
        self.labelframe.grid(row=2, column=0, sticky="nsew")
        self.labelframe.columnconfigure(0, weight=1)
        self.labelframe.rowconfigure(0, weight=1)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)

        self._combo_variable.trace("w", self._backend_changed)
        self._backend_changed()

    def _backend_changed(self, *args):
        backend_desc = self._combo_variable.get()

        if backend_desc == "":
            if self._current_page is not None:
                self._current_page.grid_forget()
            return

        page = self._get_conf_page(backend_desc)

        if page != self._current_page:

            if self._current_page is not None:
                self._current_page.grid_forget()

            page.grid(sticky="nsew", padx=10, pady=5)
            self._current_page = page

    def _get_conf_page(self, backend_desc):
        if backend_desc not in self._conf_pages:
            cp_constructor = self._backend_specs_by_desc[backend_desc].config_page_constructor
            if isinstance(cp_constructor, str):
                self._conf_pages[backend_desc] = OnlyTextConfigurationPage(
                    self.labelframe, cp_constructor
                )
            else:
                assert issubclass(cp_constructor, ConfigurationPage)
                self._conf_pages[backend_desc] = cp_constructor(self.labelframe)

        return self._conf_pages[backend_desc]

    def apply(self):
        if self._current_page is None:
            return None

        result = self._current_page.apply()

        if result is False:
            return False

        backend_desc = self._combo_variable.get()
        backend_name = self._backend_specs_by_desc[backend_desc].name
        get_workbench().set_option("run.backend_name", backend_name)

        if getattr(self._combo_variable, "modified") or self._current_page.should_restart():
            get_runner().restart_backend(False)

        return None


class BaseSshProxyConfigPage(BackendDetailsConfigPage):
    backend_name = None  # Will be overwritten on Workbench.add_backend

    def __init__(self, master, conf_group):
        super().__init__(master)
        self._changed = False
        self._conf_group = conf_group

        self._host_var = self._add_text_field("Host", self._conf_group + ".host", 1)
        self._user_var = self._add_text_field("Username", self._conf_group + ".user", 3)
        self._password_var = self._add_text_field(
            "Password", self._conf_group + ".password", 5, show="•"
        )
        self._executable_var = self._add_text_field(
            "Interpreter", self._conf_group + ".executable", 30
        )

    def _on_change(self):
        self._changed = True

    def apply(self):
        get_workbench().set_option(self._conf_group + ".host", self._host_var.get())
        get_workbench().set_option(self._conf_group + ".user", self._user_var.get())
        get_workbench().set_option(self._conf_group + ".password", self._password_var.get())
        get_workbench().set_option(self._conf_group + ".executable", self._executable_var.get())

        if self._changed:
            # reset cwd setting to default
            get_workbench().set_option(self._conf_group + ".cwd", "")

    def should_restart(self):
        return self._changed


def load_plugin() -> None:
    def select_device():
        get_workbench().show_options("interpreter")

    get_workbench().add_configuration_page(
        "interpreter", _("Interpreter"), BackendConfigurationPage, 20
    )
    get_workbench().add_command(
        "select_interpreter", "run", _("Select interpreter") + "...", select_device, group=1
    )

# Will be overwritten on Workbench.add_backend # reset cwd setting to default