V5 komplett: Auth, Admin, Federation, Channels, Devices, Cockpit, Profil, Autotext-Fix, Uebersetzer-Fix
Made-with: Cursor
This commit is contained in:
@@ -503,7 +503,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
self._autotext_data.setdefault("eventsSort", "soonest")
|
||||
self._autotext_data.setdefault("eventsMonthsAhead", 13)
|
||||
self._autotext_data.setdefault("selectedLanguage", "system")
|
||||
self._autotext_data.setdefault("user_specialty_default", "dermatology")
|
||||
self._autotext_data.setdefault("user_specialty_default", "")
|
||||
self._autotext_data.setdefault("user_specialties_selected", [])
|
||||
self._autotext_data.setdefault("ui_font_delta", -1)
|
||||
self._autotext_data.setdefault("notizen_open_on_start", True)
|
||||
@@ -2881,61 +2881,104 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
dlg.title("Profil bearbeiten")
|
||||
dlg.configure(bg="#E8F4FA")
|
||||
dlg.resizable(True, True)
|
||||
dlg.geometry("400x560")
|
||||
dlg.minsize(380, 480)
|
||||
dlg.geometry("520x820")
|
||||
dlg.minsize(460, 680)
|
||||
dlg.attributes("-topmost", True)
|
||||
self._register_window(dlg)
|
||||
center_window(dlg, 400, 560)
|
||||
center_window(dlg, 520, 820)
|
||||
|
||||
tk.Label(dlg, text="👤 Profil bearbeiten", font=("Segoe UI", 13, "bold"),
|
||||
tk.Label(dlg, text="\U0001f464 Profil bearbeiten", font=("Segoe UI", 13, "bold"),
|
||||
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
|
||||
|
||||
form = tk.Frame(dlg, bg="#E8F4FA", padx=20, pady=8)
|
||||
scroll_canvas = tk.Canvas(dlg, bg="#E8F4FA", highlightthickness=0)
|
||||
scroll_vsb = ttk.Scrollbar(dlg, orient="vertical", command=scroll_canvas.yview)
|
||||
scroll_canvas.configure(yscrollcommand=scroll_vsb.set)
|
||||
scroll_vsb.pack(side="right", fill="y")
|
||||
scroll_canvas.pack(side="left", fill="both", expand=True)
|
||||
scroll_inner = tk.Frame(scroll_canvas, bg="#E8F4FA")
|
||||
scroll_win = scroll_canvas.create_window((0, 0), window=scroll_inner, anchor="nw")
|
||||
scroll_inner.bind("<Configure>", lambda e: scroll_canvas.configure(scrollregion=scroll_canvas.bbox("all")))
|
||||
scroll_canvas.bind("<Configure>", lambda e: scroll_canvas.itemconfigure(scroll_win, width=e.width))
|
||||
scroll_canvas.bind_all("<MouseWheel>", lambda e: scroll_canvas.yview_scroll(-1 * (e.delta // 120), "units"))
|
||||
dlg.bind("<Destroy>", lambda e: scroll_canvas.unbind_all("<MouseWheel>") if e.widget is dlg else None)
|
||||
|
||||
form = tk.Frame(scroll_inner, bg="#E8F4FA", padx=20, pady=8)
|
||||
form.pack(fill="x")
|
||||
|
||||
tk.Label(form, text="Name / Titel:", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(8, 0))
|
||||
name_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
||||
name_e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
name_e.insert(0, self._user_profile.get("name", ""))
|
||||
_FLD_FONT = ("Segoe UI", 9)
|
||||
_LBL_FONT = ("Segoe UI", 9, "bold")
|
||||
_PH_COLOR = "#aaa"
|
||||
_FG_COLOR = "#1a4d6d"
|
||||
|
||||
tk.Label(form, text="Fachrichtung:", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
||||
spec_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
||||
spec_e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
spec_e.insert(0, self._user_profile.get("specialty", ""))
|
||||
def _make_placeholder_entry(parent, placeholder, initial=""):
|
||||
e = tk.Entry(parent, font=_FLD_FONT, bg="white", fg=_FG_COLOR, relief="flat")
|
||||
e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
|
||||
tk.Label(form, text="Praxis / Klinik:", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
||||
clinic_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
||||
clinic_e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
clinic_e.insert(0, self._user_profile.get("clinic", ""))
|
||||
def _set_ph():
|
||||
if not e.get().strip():
|
||||
e.delete(0, "end")
|
||||
e.insert(0, placeholder)
|
||||
e.configure(fg=_PH_COLOR)
|
||||
|
||||
tk.Label(form, text="Code (ZSR/GLN, optional):", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
||||
code_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
||||
code_e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
code_e.insert(0, self._user_profile.get("code", ""))
|
||||
def _on_focus_in(ev):
|
||||
if e.get() == placeholder and e.cget("fg") == _PH_COLOR:
|
||||
e.delete(0, "end")
|
||||
e.configure(fg=_FG_COLOR)
|
||||
|
||||
tk.Label(form, text="E-Mail (optional):", font=("Segoe UI", 10),
|
||||
bg="#E8F4FA", fg="#888").pack(anchor="w", pady=(4, 0))
|
||||
email_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
||||
email_e.pack(fill="x", ipady=4, pady=(0, 6))
|
||||
email_e.insert(0, self._user_profile.get("email", ""))
|
||||
def _on_focus_out(ev):
|
||||
_set_ph()
|
||||
|
||||
def _get_val():
|
||||
v = e.get().strip()
|
||||
return "" if v == placeholder else v
|
||||
|
||||
e.bind("<FocusIn>", _on_focus_in)
|
||||
e.bind("<FocusOut>", _on_focus_out)
|
||||
e._get_real_value = _get_val
|
||||
|
||||
if initial and initial != placeholder:
|
||||
e.insert(0, initial)
|
||||
e.configure(fg=_FG_COLOR)
|
||||
else:
|
||||
_set_ph()
|
||||
return e
|
||||
|
||||
tk.Label(form, text="Name / Titel:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(8, 0))
|
||||
_name_initial = self._user_profile.get("name", "")
|
||||
if _name_initial == "Benutzer":
|
||||
_name_initial = ""
|
||||
name_e = _make_placeholder_entry(form, "Ihr Name / Titel eingeben...", _name_initial)
|
||||
|
||||
tk.Label(form, text="Fachrichtung:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
spec_e = _make_placeholder_entry(form, "z.B. Dermatologie", self._user_profile.get("specialty", ""))
|
||||
|
||||
tk.Label(form, text="Praxis / Klinik:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
clinic_e = _make_placeholder_entry(form, "z.B. Hautarztpraxis Winterthur", self._user_profile.get("clinic", ""))
|
||||
|
||||
tk.Label(form, text="Code (ZSR/GLN, optional):", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
code_e = _make_placeholder_entry(form, "ZSR / GLN", self._user_profile.get("code", ""))
|
||||
|
||||
tk.Label(form, text="E-Mail:", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
|
||||
email_e = _make_placeholder_entry(form, "praxis@beispiel.ch", self._user_profile.get("email", ""))
|
||||
|
||||
sep = tk.Frame(form, bg="#B9ECFA", height=1)
|
||||
sep.pack(fill="x", pady=(8, 6))
|
||||
|
||||
tk.Label(form, text="🔑 Passwort ändern (optional):", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
|
||||
tk.Label(form, text="\U0001f511 Passwort \u00e4ndern (optional):", font=_LBL_FONT,
|
||||
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(2, 0))
|
||||
tk.Label(form, text="Leer lassen, um das Passwort beizubehalten.",
|
||||
font=("Segoe UI", 8), bg="#E8F4FA", fg="#888").pack(anchor="w")
|
||||
|
||||
pw_old_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
||||
pw_old_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
pw_new_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
||||
pw_new_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
pw_confirm_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
||||
pw_confirm_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
|
||||
relief="flat", bd=0, show="")
|
||||
|
||||
if self._user_profile.get("password_hash"):
|
||||
@@ -2951,7 +2994,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
pw_confirm_e.pack(fill="x", ipady=3, pady=(0, 4))
|
||||
|
||||
def do_save():
|
||||
name = name_e.get().strip()
|
||||
name = name_e._get_real_value() if hasattr(name_e, '_get_real_value') else name_e.get().strip()
|
||||
if not name:
|
||||
messagebox.showwarning("Pflichtfeld", "Name darf nicht leer sein.", parent=dlg)
|
||||
return
|
||||
@@ -2967,18 +3010,19 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
messagebox.showwarning("Zu kurz", "Das neue Passwort muss mindestens 4 Zeichen lang sein.", parent=dlg)
|
||||
return
|
||||
if new_pw != new_pw_confirm:
|
||||
messagebox.showerror("Fehler", "Die neuen Passwörter stimmen nicht überein.", parent=dlg)
|
||||
messagebox.showerror("Fehler", "Die neuen Passw\u00f6rter stimmen nicht \u00fcberein.", parent=dlg)
|
||||
return
|
||||
pw_hash = self._hash_password(new_pw)
|
||||
else:
|
||||
pw_hash = old_hash
|
||||
|
||||
_gv = lambda e: e._get_real_value() if hasattr(e, '_get_real_value') else e.get().strip()
|
||||
updated = {
|
||||
"name": name,
|
||||
"specialty": spec_e.get().strip(),
|
||||
"clinic": clinic_e.get().strip(),
|
||||
"code": code_e.get().strip(),
|
||||
"email": email_e.get().strip(),
|
||||
"specialty": _gv(spec_e),
|
||||
"clinic": _gv(clinic_e),
|
||||
"code": _gv(code_e),
|
||||
"email": _gv(email_e),
|
||||
"password_hash": pw_hash,
|
||||
}
|
||||
for k in ("totp_secret_enc", "totp_active", "backup_codes"):
|
||||
@@ -2989,9 +3033,90 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
self.set_status(f"Profil gespeichert: {name}")
|
||||
dlg.destroy()
|
||||
|
||||
btn_row = tk.Frame(dlg, bg="#E8F4FA")
|
||||
# --- Lizenz / Einladung ---
|
||||
lic_sep = tk.Frame(form, bg="#B9ECFA", height=1)
|
||||
lic_sep.pack(fill="x", pady=(10, 6))
|
||||
tk.Label(form, text="\U0001f4cb Lizenz & Einladung", font=("Segoe UI", 10, "bold"),
|
||||
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 4))
|
||||
|
||||
_lic_key = (self._user_profile.get("license_key") or "").strip()
|
||||
_lic_email = (self._user_profile.get("email") or "").strip()
|
||||
if _lic_key:
|
||||
tk.Label(form, text="Lizenznummer:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3").pack(anchor="w")
|
||||
_lic_lbl = tk.Label(form, text=_lic_key, font=("Consolas", 9),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, cursor="hand2", anchor="w")
|
||||
_lic_lbl.pack(fill="x", pady=(0, 6))
|
||||
_lic_lbl.bind("<Button-1>", lambda e: (
|
||||
dlg.clipboard_clear(), dlg.clipboard_append(_lic_key),
|
||||
self.set_status("Lizenznummer kopiert.")))
|
||||
|
||||
_invite_frame = tk.Frame(form, bg="#E8F4FA")
|
||||
_invite_frame.pack(fill="x", pady=(0, 4))
|
||||
_invite_label = tk.Label(_invite_frame, text="Einladungscode:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3")
|
||||
_invite_label.pack(anchor="w")
|
||||
_invite_val = tk.Label(_invite_frame, text="Wird geladen...", font=("Consolas", 9),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, anchor="w")
|
||||
_invite_val.pack(fill="x")
|
||||
|
||||
_link_frame = tk.Frame(form, bg="#E8F4FA")
|
||||
_link_frame.pack(fill="x", pady=(0, 6))
|
||||
_link_label = tk.Label(_link_frame, text="Einladungslink:", font=("Segoe UI", 9),
|
||||
bg="#E8F4FA", fg="#5B8DB3")
|
||||
_link_label.pack(anchor="w")
|
||||
_link_val = tk.Label(_link_frame, text="", font=("Segoe UI", 8),
|
||||
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
|
||||
padx=6, pady=2, anchor="w", wraplength=440, justify="left")
|
||||
_link_val.pack(fill="x")
|
||||
|
||||
_link_btn_row = tk.Frame(form, bg="#E8F4FA")
|
||||
_link_btn_row.pack(fill="x", pady=(0, 8))
|
||||
|
||||
def _copy_invite_link():
|
||||
text = _link_val.cget("text")
|
||||
if text:
|
||||
dlg.clipboard_clear()
|
||||
dlg.clipboard_append(text)
|
||||
self.set_status("Einladungslink kopiert.")
|
||||
|
||||
tk.Button(_link_btn_row, text="Link kopieren", font=("Segoe UI", 9),
|
||||
bg="#e8f0f8", fg="#2a5a8a", relief="flat", padx=10, pady=3,
|
||||
cursor="hand2", command=_copy_invite_link).pack(side="left", padx=(0, 6))
|
||||
|
||||
def _load_invite_info():
|
||||
try:
|
||||
bu = self.get_backend_url()
|
||||
bt = self.get_backend_token()
|
||||
r = requests.get(f"{bu}/empfang/practice/info",
|
||||
headers={"X-API-Token": bt}, timeout=5)
|
||||
if r.status_code == 200:
|
||||
d = r.json()
|
||||
code = d.get("invite_code", "")
|
||||
pname = d.get("practice_name", "")
|
||||
if code:
|
||||
link = f"https://empfang.aza-medwork.ch/?invite={code}"
|
||||
self.after(0, lambda: _invite_val.configure(text=code))
|
||||
self.after(0, lambda: _link_val.configure(text=link))
|
||||
else:
|
||||
self.after(0, lambda: _invite_val.configure(text=""))
|
||||
self.after(0, lambda: _link_val.configure(text=""))
|
||||
self.after(0, lambda: _invite_frame.pack_forget())
|
||||
self.after(0, lambda: _link_frame.pack_forget())
|
||||
self.after(0, lambda: _link_btn_row.pack_forget())
|
||||
except Exception:
|
||||
self.after(0, lambda: _invite_frame.pack_forget())
|
||||
self.after(0, lambda: _link_frame.pack_forget())
|
||||
self.after(0, lambda: _link_btn_row.pack_forget())
|
||||
|
||||
threading.Thread(target=_load_invite_info, daemon=True).start()
|
||||
|
||||
# --- Buttons ---
|
||||
btn_row = tk.Frame(scroll_inner, bg="#E8F4FA")
|
||||
btn_row.pack(pady=(12, 10))
|
||||
tk.Button(btn_row, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
|
||||
tk.Button(btn_row, text="\U0001f4be Speichern", font=("Segoe UI", 11, "bold"),
|
||||
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
||||
relief="flat", padx=20, pady=6, cursor="hand2",
|
||||
command=do_save).pack(side="left", padx=8)
|
||||
@@ -3001,9 +3126,9 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
command=dlg.destroy).pack(side="left", padx=8)
|
||||
|
||||
if is_2fa_enabled():
|
||||
sep2 = tk.Frame(dlg, bg="#B9ECFA", height=1)
|
||||
sep2 = tk.Frame(scroll_inner, bg="#B9ECFA", height=1)
|
||||
sep2.pack(fill="x", padx=20, pady=(4, 4))
|
||||
tfa_frame = tk.Frame(dlg, bg="#E8F4FA", padx=20)
|
||||
tfa_frame = tk.Frame(scroll_inner, bg="#E8F4FA", padx=20)
|
||||
tfa_frame.pack(fill="x")
|
||||
is_active = self._user_profile.get("totp_active", False)
|
||||
status_text = "2FA aktiv" if is_active else "2FA nicht aktiv"
|
||||
@@ -4001,22 +4126,24 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
||||
dlg.transient(self)
|
||||
dlg.grab_set()
|
||||
dlg.configure(bg="#F2F8FC")
|
||||
dlg.geometry("520x620")
|
||||
dlg.minsize(420, 500)
|
||||
add_resize_grip(dlg, 420, 500)
|
||||
center_window(dlg, 520, 620)
|
||||
body = tk.Frame(dlg, bg="#F2F8FC", padx=12, pady=12)
|
||||
body.pack(fill="both", expand=True)
|
||||
tk.Label(body, text="Primäres Fachgebiet (Erststart)", bg="#F2F8FC", fg="#1a4d6d", font=("Segoe UI", 11, "bold")).pack(anchor="w")
|
||||
primary_var = tk.StringVar(value="dermatology")
|
||||
primary_var = tk.StringVar(value="")
|
||||
label_to_key = {label: key for key, label in catalog}
|
||||
key_to_label = {key: label for key, label in catalog}
|
||||
combo_val = tk.StringVar(value=key_to_label.get("dermatology", "Dermatologie"))
|
||||
combo = ttk.Combobox(body, values=[label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
|
||||
combo_val = tk.StringVar(value="")
|
||||
combo = ttk.Combobox(body, values=[""] + [label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
|
||||
combo.pack(anchor="w", pady=(6, 10))
|
||||
|
||||
tk.Label(body, text="Weitere Fachgebiete (optional)", bg="#F2F8FC", fg="#1a4d6d").pack(anchor="w")
|
||||
vars_map = {}
|
||||
for key, label in catalog:
|
||||
v = tk.BooleanVar(value=(key == "dermatology"))
|
||||
v = tk.BooleanVar(value=False)
|
||||
vars_map[key] = v
|
||||
tk.Checkbutton(body, text=label, variable=v, bg="#F2F8FC", activebackground="#F2F8FC", selectcolor="#E7F4FA").pack(anchor="w")
|
||||
|
||||
@@ -5185,7 +5312,7 @@ WICHTIG unbedingt einhalten:
|
||||
dlg.resizable(True, True)
|
||||
self._register_window(dlg)
|
||||
|
||||
_empfang_font_size = [load_text_font_size("empfang_dlg", 10)]
|
||||
_empfang_font_size = [load_text_font_size("empfang_dlg", 9)]
|
||||
_empfang_text_widgets: list = []
|
||||
|
||||
def _save_prefs():
|
||||
@@ -7386,11 +7513,12 @@ WICHTIG unbedingt einhalten:
|
||||
dlg.title("KI-Einwilligung erforderlich")
|
||||
dlg.transient(self)
|
||||
dlg.grab_set()
|
||||
dlg.geometry("680x520")
|
||||
dlg.minsize(500, 400)
|
||||
dlg.geometry("920x720")
|
||||
dlg.minsize(700, 550)
|
||||
dlg.attributes("-topmost", True)
|
||||
self._register_window(dlg)
|
||||
add_resize_grip(dlg, 500, 400)
|
||||
add_resize_grip(dlg, 700, 550)
|
||||
center_window(dlg, 920, 720)
|
||||
|
||||
frame = ttk.Frame(dlg, padding=12)
|
||||
frame.pack(fill="both", expand=True)
|
||||
@@ -8769,7 +8897,11 @@ WICHTIG:
|
||||
text_widget.bind("<FocusIn>", on_focus_in, add="+")
|
||||
text_widget.bind("<FocusOut>", on_focus_out, add="+")
|
||||
|
||||
_last_expansion = [0.0, ""]
|
||||
|
||||
def on_keyrelease(event):
|
||||
if getattr(self, "_autotext_injecting", [False])[0]:
|
||||
return
|
||||
if not getattr(self, "_autotext_data", {}).get("enabled", True):
|
||||
return
|
||||
entries = (self._autotext_data.get("entries") or {})
|
||||
@@ -8792,7 +8924,12 @@ WICHTIG:
|
||||
word = text_before[word_start:word_end]
|
||||
if not word or word not in entries:
|
||||
return
|
||||
now = time.time()
|
||||
if _last_expansion[1] == word and now - _last_expansion[0] < 1.0:
|
||||
return
|
||||
expansion = entries[word]
|
||||
_last_expansion[0] = now
|
||||
_last_expansion[1] = word
|
||||
start_idx = text_widget.index(f"{insert} - {len(word) + 1} chars")
|
||||
text_widget.delete(start_idx, insert)
|
||||
text_widget.insert(start_idx, expansion + last_char)
|
||||
|
||||
Reference in New Issue
Block a user