v1.8.23: fix Ctrl+V/C/X/A/Z for non-Latin keyboard layouts, add Entry undo/redo
- Global keycode-based handler for Ctrl shortcuts (works with Russian, Chinese, any layout) - Tkinter maps <<Paste>> to <Control-v> by keysym which fails on non-Latin layouts - New handler intercepts <Control-Key> at 'all' level, checks keycodes and generates correct virtual events - Added Undo/Redo support for Entry widgets (tk.Entry has no built-in undo) - Tab switch focus management: terminal releases focus when switching away Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
123
gui/app.py
123
gui/app.py
@@ -2,6 +2,7 @@
|
||||
Main application window — sidebar + tabview layout.
|
||||
"""
|
||||
|
||||
import tkinter
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
|
||||
@@ -46,6 +47,14 @@ class App(ctk.CTk):
|
||||
self.checker.start()
|
||||
self.checker.check_all_now()
|
||||
|
||||
# Fix Ctrl+V/C/A/X for non-Latin keyboard layouts (e.g. Russian)
|
||||
# Tkinter maps <<Paste>> to <Control-v> by keysym, which fails when
|
||||
# the layout produces non-Latin characters. This fix uses keycodes instead.
|
||||
self._setup_keyboard_layout_fix()
|
||||
|
||||
# Add Undo/Redo support for Entry widgets (tk.Entry has no built-in undo)
|
||||
self._setup_entry_undo()
|
||||
|
||||
# Cleanup on close
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
@@ -85,7 +94,7 @@ class App(ctk.CTk):
|
||||
self.about_btn.pack(side="right", padx=(5, 5))
|
||||
|
||||
# Tabview
|
||||
self.tabview = ctk.CTkTabview(main)
|
||||
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Tab names stored for language updates
|
||||
@@ -204,7 +213,7 @@ class App(ctk.CTk):
|
||||
self.tabview.destroy()
|
||||
|
||||
# Create new tabview with translated names
|
||||
self.tabview = ctk.CTkTabview(main)
|
||||
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
for key in self._tab_keys:
|
||||
@@ -255,6 +264,116 @@ class App(ctk.CTk):
|
||||
# Update sidebar
|
||||
self.sidebar.update_language()
|
||||
|
||||
def _setup_entry_undo(self):
|
||||
"""Add Undo/Redo support for tk.Entry widgets (they have no built-in undo).
|
||||
|
||||
Tracks text changes per-widget and restores on <<Undo>>/<<Redo>>.
|
||||
"""
|
||||
undo_stacks: dict[str, list[str]] = {}
|
||||
redo_stacks: dict[str, list[str]] = {}
|
||||
|
||||
def _save_state(event):
|
||||
w = event.widget
|
||||
wid = str(w)
|
||||
try:
|
||||
current = w.get()
|
||||
except Exception:
|
||||
return
|
||||
stack = undo_stacks.setdefault(wid, [])
|
||||
if not stack or stack[-1] != current:
|
||||
stack.append(current)
|
||||
if len(stack) > 100:
|
||||
stack.pop(0)
|
||||
# Clear redo on new input
|
||||
redo_stacks.pop(wid, None)
|
||||
|
||||
def _on_undo(event):
|
||||
w = event.widget
|
||||
wid = str(w)
|
||||
try:
|
||||
current = w.get()
|
||||
except Exception:
|
||||
return "break"
|
||||
stack = undo_stacks.get(wid, [])
|
||||
# Pop current state
|
||||
while stack and stack[-1] == current:
|
||||
stack.pop()
|
||||
if stack:
|
||||
prev = stack[-1]
|
||||
redo_stacks.setdefault(wid, []).append(current)
|
||||
w.delete(0, "end")
|
||||
w.insert(0, prev)
|
||||
return "break"
|
||||
|
||||
def _on_redo(event):
|
||||
w = event.widget
|
||||
wid = str(w)
|
||||
try:
|
||||
current = w.get()
|
||||
except Exception:
|
||||
return "break"
|
||||
stack = redo_stacks.get(wid, [])
|
||||
if stack:
|
||||
next_val = stack.pop()
|
||||
undo_stacks.setdefault(wid, []).append(current)
|
||||
w.delete(0, "end")
|
||||
w.insert(0, next_val)
|
||||
return "break"
|
||||
|
||||
# Add class-level bindings for Entry
|
||||
self.tk.eval('''
|
||||
bind Entry <<Undo>> {}
|
||||
bind Entry <<Redo>> {}
|
||||
''')
|
||||
tkinter.Misc.bind_class(self, "Entry", "<Key>", _save_state, "+")
|
||||
tkinter.Misc.bind_class(self, "Entry", "<<Undo>>", _on_undo)
|
||||
tkinter.Misc.bind_class(self, "Entry", "<<Redo>>", _on_redo)
|
||||
|
||||
def _setup_keyboard_layout_fix(self):
|
||||
"""Fix Ctrl+V/C/X/A/Z for non-Latin keyboard layouts (Russian, Chinese, etc.).
|
||||
|
||||
Tkinter binds <<Paste>> to <Control-v> by keysym. When the keyboard
|
||||
layout is Russian, pressing Ctrl+V produces <Control-м> which doesn't
|
||||
match. This fix intercepts <Control-Key> at the 'all' level by keycode
|
||||
(layout-independent) and generates the correct virtual events.
|
||||
"""
|
||||
# Keycode → virtual event mapping (physical keys, layout-independent)
|
||||
keycode_to_event = {
|
||||
86: "<<Paste>>", # V
|
||||
67: "<<Copy>>", # C
|
||||
88: "<<Cut>>", # X
|
||||
65: "<<SelectAll>>", # A
|
||||
90: "<<Undo>>", # Z
|
||||
89: "<<Redo>>", # Y
|
||||
}
|
||||
|
||||
def _on_ctrl_key_global(event):
|
||||
# Only act when Ctrl is held (not Ctrl+Shift, etc.)
|
||||
if not (event.state & 0x4):
|
||||
return
|
||||
# Skip if keysym is already Latin (standard handling works)
|
||||
if event.keysym in ('v', 'V', 'c', 'C', 'x', 'X', 'a', 'A',
|
||||
'z', 'Z', 'y', 'Y'):
|
||||
return
|
||||
virtual = keycode_to_event.get(event.keycode)
|
||||
if virtual:
|
||||
event.widget.event_generate(virtual)
|
||||
return "break"
|
||||
|
||||
# Bypass CTk's bind_all restriction by calling tkinter.Misc directly
|
||||
tkinter.Misc.bind_all(self, "<Control-Key>", _on_ctrl_key_global, "+")
|
||||
|
||||
def _on_tab_changed(self):
|
||||
"""Handle tab switch — manage terminal focus."""
|
||||
try:
|
||||
current = self.tabview.get()
|
||||
if current == t("terminal"):
|
||||
self.terminal_tab._terminal.focus_terminal()
|
||||
else:
|
||||
self.focus_set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_close(self):
|
||||
# Disconnect all sessions before closing
|
||||
self.session_pool.disconnect_all()
|
||||
|
||||
Reference in New Issue
Block a user