From ccb5d7960b76c46466cd4d11ae4eb06c4d5c2d65 Mon Sep 17 00:00:00 2001 From: Akuli Date: Fri, 30 Jun 2023 00:54:39 +0300 Subject: [PATCH] Rewrite ask_encoding() (#1301) --- porcupine/plugins/statusbar.py | 79 +--------- porcupine/utils.py | 264 ++++++++++++++++++--------------- 2 files changed, 144 insertions(+), 199 deletions(-) diff --git a/porcupine/plugins/statusbar.py b/porcupine/plugins/statusbar.py index 08d8cc280..7d214fefa 100644 --- a/porcupine/plugins/statusbar.py +++ b/porcupine/plugins/statusbar.py @@ -9,83 +9,6 @@ from porcupine.textutils import count -# Must be in a function because lambdas and local variables are ... inconvenient -def _connect_label_to_radiobutton(label: ttk.Label, radio: ttk.Radiobutton) -> None: - label.bind("", lambda e: radio.event_generate(""), add=True) - label.bind("", lambda e: radio.event_generate(""), add=True) - label.bind("", lambda e: radio.invoke(), add=True) - - -def ask_line_ending(old_line_ending: settings.LineEnding) -> settings.LineEnding: - top = tkinter.Toplevel(name="choose_line_ending") - top.resizable(False, False) - top.transient(get_main_window()) - top.title("Choose a line ending") - - big_frame = ttk.Frame(top) - big_frame.pack(fill="both", expand=True) - ttk.Label(big_frame, text="Choose how line endings should be saved:").pack( - fill="x", padx=5, pady=5 - ) - - options: list[tuple[str, str, str]] = [ - ( - "LF", - "LF line endings (Unix)", - "Newline characters will be saved to the file as the LF byte (\\n)." - " This is the line ending used by most Unix-like operating systems," - " such as Linux and MacOS," - " and usually the preferred line ending in projects that use Git.", - ), - ( - "CRLF", - "CRLF line endings (Windows)", - "Each newline will be saved to the file as two bytes," - " CR (\\r) followed by LF (\\n)." - " CRLF is Porcupine's default line ending on Windows," - " and the only line ending supported by many Windows programs." - "\n\nCommitting files to Git with CRLF is usually considered bad style." - " If you use CRLF in projects that use Git," - " make sure to configure Git to convert the line endings" - " so that your CRLF line endings appear as LF line endings" - " for other people working on the project.", - ), - ( - "CR", - "CR line endings (???)", - "I don't know when this option could be useful," - " but it is provided in case you have some use case that I didn't think of.", - ), - ] - - var = tkinter.StringVar(value=old_line_ending.name) - for line_ending_name, short_text, long_text in options: - radio = ttk.Radiobutton(big_frame, variable=var, value=line_ending_name, text=short_text) - radio.pack(fill="x", padx=(10, 0), pady=(10, 0)) - label = ttk.Label(big_frame, wraplength=450, text=long_text) - label.pack(fill="x", padx=(50, 10), pady=(0, 10)) - _connect_label_to_radiobutton(label, radio) - - if line_ending_name == old_line_ending.name: - radio.focus() - - ttk.Label( - big_frame, - text=( - "Consider setting the line ending in a project-specific .editorconfig file" - " if your project uses an unusual choice of line endings." - ), - ) - - ttk.Button(big_frame, text="OK", command=top.destroy, width=15).pack( - side="right", padx=10, pady=10 - ) - top.bind("", (lambda e: top.destroy()), add=True) - - top.wait_window() - return settings.LineEnding[var.get()] - - class StatusBar(ttk.Frame): def __init__(self, tab: tabs.FileTab): super().__init__(tab.bottom_frame, name="statusbar") @@ -170,7 +93,7 @@ def _choose_line_ending(self) -> None: return old_value = self._tab.settings.get("line_ending", settings.LineEnding) - self._tab.settings.set("line_ending", ask_line_ending(old_value)) + self._tab.settings.set("line_ending", utils.ask_line_ending(old_value)) def on_new_filetab(tab: tabs.FileTab) -> None: diff --git a/porcupine/utils.py b/porcupine/utils.py index 2b747bb35..5d416f047 100644 --- a/porcupine/utils.py +++ b/porcupine/utils.py @@ -558,107 +558,86 @@ def callback(shifted: bool, event: tkinter.Event[tkinter.Misc]) -> Any: widget.bind(shift_tab, functools.partial(callback, True), **bind_kwargs) # noqa: TK231 -# list of encodings supported by python 3.7 https://stackoverflow.com/a/25584253 -_list_of_encodings = [ - "ascii", - "big5", - "big5hkscs", - "cp037", - "cp273", - "cp424", - "cp437", - "cp500", - "cp720", - "cp737", - "cp775", - "cp850", - "cp852", - "cp855", - "cp856", - "cp857", - "cp858", - "cp860", - "cp861", - "cp862", - "cp863", - "cp864", - "cp865", - "cp866", - "cp869", - "cp874", - "cp875", - "cp932", - "cp949", - "cp950", - "cp1006", - "cp1026", - "cp1125", - "cp1140", - "cp1250", - "cp1251", - "cp1252", - "cp1253", - "cp1254", - "cp1255", - "cp1256", - "cp1257", - "cp1258", - "cp65001", - "euc-jis-2004", - "euc-jisx0213", - "euc-jp", - "euc-kr", - "gb2312", - "gb18030", - "gbk", - "hz", - "iso2022-jp", - "iso2022-jp-1", - "iso2022-jp-2", - "iso2022-jp-3", - "iso2022-jp-2004", - "iso2022-jp-ext", - "iso2022-kr", - "iso8859-2", - "iso8859-3", - "iso8859-4", - "iso8859-5", - "iso8859-6", - "iso8859-7", - "iso8859-8", - "iso8859-9", - "iso8859-10", - "iso8859-11", - "iso8859-13", - "iso8859-14", - "iso8859-15", - "iso8859-16", - "johab", - "koi8-r", - "koi8-t", - "koi8-u", - "kz1048", - "latin-1", - "mac-cyrillic", - "mac-greek", - "mac-iceland", - "mac-latin2", - "mac-roman", - "mac-turkish", - "ptcp154", - "shift-jis", - "shift-jis-2004", - "shift-jisx0213", - "utf-7", - "utf-8", - "utf-8-sig", - "utf-16", - "utf-16-be", - "utf-16-le", - "utf-32", - "utf-32-be", - "utf-32-le", -] +# Must be in a function because lambdas and local variables are ... inconvenient +def _associate_another_widget_with_a_radiobutton( + other: ttk.Label | ttk.Entry, radio: ttk.Radiobutton +) -> None: + other.bind("", lambda e: radio.event_generate(""), add=True) + other.bind("", lambda e: radio.event_generate(""), add=True) + other.bind("", lambda e: radio.invoke(), add=True) + + +# TODO: document this? +def ask_line_ending( + old_line_ending: porcupine.settings.LineEnding, +) -> porcupine.settings.LineEnding: + top = tkinter.Toplevel(name="choose_line_ending") + top.resizable(False, False) + top.transient(porcupine.get_main_window()) + top.title("Choose a line ending") + + big_frame = ttk.Frame(top) + big_frame.pack(fill="both", expand=True) + ttk.Label(big_frame, text="Choose how line endings should be saved:").pack( + fill="x", padx=5, pady=5 + ) + + options: list[tuple[str, str, str]] = [ + ( + "LF", + "LF line endings (Unix)", + "Newline characters will be saved to the file as the LF byte (\\n)." + " This is the line ending used by most Unix-like operating systems," + " such as Linux and MacOS," + " and usually the preferred line ending in projects that use Git.", + ), + ( + "CRLF", + "CRLF line endings (Windows)", + "Each newline will be saved to the file as two bytes," + " CR (\\r) followed by LF (\\n)." + " CRLF is Porcupine's default line ending on Windows," + " and the only line ending supported by many Windows programs." + "\n\nCommitting files to Git with CRLF is usually considered bad style." + " If you use CRLF in projects that use Git," + " make sure to configure Git to convert the line endings" + " so that your CRLF line endings appear as LF line endings" + " for other people working on the project.", + ), + ( + "CR", + "CR line endings (???)", + "I don't know when this option could be useful," + " but it is provided in case you have some use case that I didn't think of.", + ), + ] + + var = tkinter.StringVar(value=old_line_ending.name) + for line_ending_name, short_text, long_text in options: + radio = ttk.Radiobutton(big_frame, variable=var, value=line_ending_name, text=short_text) + radio.pack(fill="x", padx=(10, 0), pady=(10, 0)) + label = ttk.Label(big_frame, wraplength=450, text=long_text) + label.pack(fill="x", padx=(50, 10), pady=(0, 10)) + _associate_another_widget_with_a_radiobutton(label, radio) + + if line_ending_name == old_line_ending.name: + radio.focus() + + ttk.Label( + big_frame, + text=( + "Consider setting the line ending in a project-specific .editorconfig file" + " if your project uses an unusual choice of line endings." + ), + ) + + ttk.Button(big_frame, text="OK", command=top.destroy, width=15).pack( + side="right", padx=10, pady=10 + ) + top.bind("", (lambda e: top.destroy()), add=True) + + top.wait_window() + return porcupine.settings.LineEnding[var.get()] # TODO: document this? @@ -678,19 +657,50 @@ def ask_encoding(text: str, old_encoding: str) -> str | None: big_frame.pack(fill="both", expand=True) ttk.Label(big_frame, text=text, wraplength=label_width).pack(fill="x", padx=10, pady=10) - var = tkinter.StringVar() - combobox = ttk.Combobox(big_frame, values=_list_of_encodings, textvariable=var) - combobox.pack(pady=40) - combobox.set(old_encoding) + options: list[tuple[str, str]] = [ + ("UTF-8", "By far the most commonly used encoding. Supports all Unicode characters."), + ( + "Latin-1", + "Supports only 256 different characters, but never fails to open a file. Also known as ISO 8859-1.", + ), + ] + + radio_var = tkinter.StringVar(value="other") # which item selected: "UTF-8", "Latin-1", "other" + entry_var = tkinter.StringVar(value=old_encoding) # text of entry + + for name, description in options: + radio = ttk.Radiobutton(big_frame, variable=radio_var, value=name, text=name) + radio.pack(fill="x", padx=(10, 0), pady=(10, 0)) + label = ttk.Label(big_frame, wraplength=label_width - (50 + 10), text=description) + label.pack(fill="x", padx=(50, 10), pady=(0, 10)) + _associate_another_widget_with_a_radiobutton(label, radio) + + other_frame = ttk.Frame(big_frame) + other_frame.pack(side="top", fill="x", padx=10, pady=10) + other_radio = ttk.Radiobutton( + other_frame, variable=radio_var, value="other", text="Other encoding:" + ) + other_radio.pack(side="left") + entry = ttk.Entry(other_frame, textvariable=entry_var) + entry.pack(side="left", padx=5) + _associate_another_widget_with_a_radiobutton(entry, other_radio) + + # Set UI to old encoding. Treat e.g. "utf8" as meaning "UTF-8". + for name, description in options: + if codecs.lookup(name) == codecs.lookup(old_encoding): + radio_var.set(name) + break + else: + radio_var.set("other") ttk.Label( big_frame, text=( - "You can create a project-specific .editorconfig file to change the encoding" - " permanently." + "You can create a project-specific .editorconfig file to change the encoding permanently." ), wraplength=label_width, - ).pack(fill="x", padx=10, pady=10) + ).pack(fill="x", padx=10, pady=(30, 10)) + button_frame = ttk.Frame(big_frame) button_frame.pack(fill="x", pady=10) @@ -698,7 +708,10 @@ def ask_encoding(text: str, old_encoding: str) -> str | None: def select_encoding() -> None: nonlocal selected_encoding - selected_encoding = combobox.get() + if radio_var.get() == "other": + selected_encoding = entry_var.get() + else: + selected_encoding = radio_var.get() dialog.destroy() cancel_button = ttk.Button(button_frame, text="Cancel", command=dialog.destroy, width=1) @@ -706,20 +719,29 @@ def select_encoding() -> None: ok_button = ttk.Button(button_frame, text="OK", command=select_encoding, width=1) ok_button.pack(side="right", expand=True, fill="x", padx=10) - def validate_encoding(*junk: object) -> None: - encoding = combobox.get() - try: - codecs.lookup(encoding) - except LookupError: - ok_button.config(state="disabled") + def update_ui(*junk: object) -> None: + if radio_var.get() == "other": + entry.config(state="normal") else: - ok_button.config(state="normal") + entry.config(state="disabled") + + valid = True + if radio_var.get() == "other": + try: + codecs.lookup(entry_var.get()) + except LookupError: + valid = False + + ok_button.config(state=("normal" if valid else "disabled")) + + radio_var.trace_add("write", update_ui) + entry_var.trace_add("write", update_ui) + update_ui() - var.trace_add("write", validate_encoding) - combobox.bind("", (lambda event: ok_button.invoke()), add=True) - combobox.bind("", (lambda event: cancel_button.invoke()), add=True) - combobox.select_range(0, "end") - combobox.focus() + entry.bind("", (lambda event: ok_button.invoke()), add=True) + entry.bind("", (lambda event: cancel_button.invoke()), add=True) + entry.select_range(0, "end") + entry.focus() dialog.wait_window() return selected_encoding