-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththeme.py
More file actions
486 lines (428 loc) · 26.6 KB
/
Copy paththeme.py
File metadata and controls
486 lines (428 loc) · 26.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
"""
theme.py — визуальная тема "а-ля Mission Planner": тёмно-синяя шапка и
статус-бар, синие акцентные кнопки, вкладки с подсветкой активной.
Ничего не знает про конкретные виджеты приложения — просто настраивает
ttk.Style и возвращает палитру цветов для точечного использования в app.py.
Підтримує ДВІ палітри (світла/темна, apply_theme(root, dark=...)) --
шапка/статус-бар/акценти лишаються тими самими в обох (вони й так темні
за задумом, "а-ля Mission Planner"), змінюється лише робоча область:
фон сторінок, поля вводу/списки, колір тексту.
"""
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
import i18n
# Палитра в духе Mission Planner: тёмно-синяя шапка/тулбар, синий акцент
# на активных элементах. Те частини, що спільні для обох тем (шапка,
# статус-бар, зелений акцент) -- НЕ дублюються нижче в PALETTE_DARK,
# лишаються тими самими.
NAVY_DARK = "#0F2438" # тёмно-синий — навигационная панель, статус-бар, активные вкладки
NAVY = "#173553" # чуть светлее — неактивные тёмные элементы
HEADER_BG = "#000000" # чёрный — фон шапки (лого/заголовок/переключатель языка)
# зелёный акцент для кнопок -- взят из логотипа (маршрут/точки/"ANALYZER")
GREEN_ACCENT = "#5A961E"
GREEN_ACCENT_HOVER = "#6FB026"
GREEN_DARK = "#355E12"
TEXT_LIGHT = "#FFFFFF"
TEXT_MUTED = "#9FB6CC" # приглушённый текст на тёмном фоне (статус-бар)
# --- світла тема (за замовчуванням раніше) ---
LIGHT_BG = "#F1F3F5" # фон рабочей области
LIGHT_PANEL = "#FFFFFF" # фон полей ввода/списков
LIGHT_TEXT = "#1B1B1B"
LIGHT_BORDER = "#C9CFD6"
# --- темна тема ---
DARK_BG = "#151515"
DARK_PANEL = "#262626"
DARK_TEXT = "#E8E8E8"
DARK_BORDER = "#444444"
PALETTE_LIGHT = {
"navy_dark": NAVY_DARK,
"navy": NAVY,
"header_bg": HEADER_BG,
"blue": GREEN_ACCENT, # оставлено под старым ключом ради обратной совместимости в app.py
"blue_hover": GREEN_ACCENT_HOVER,
"green": GREEN_ACCENT,
"green_hover": GREEN_ACCENT_HOVER,
"green_dark": GREEN_DARK,
"bg": LIGHT_BG,
"panel": LIGHT_PANEL,
"text": LIGHT_TEXT,
"text_light": TEXT_LIGHT,
"text_muted": TEXT_MUTED,
"border": LIGHT_BORDER,
"dark": False,
}
PALETTE_DARK = {
"navy_dark": NAVY_DARK,
"navy": NAVY,
"header_bg": HEADER_BG,
"blue": GREEN_ACCENT,
"blue_hover": GREEN_ACCENT_HOVER,
"green": GREEN_ACCENT,
"green_hover": GREEN_ACCENT_HOVER,
"green_dark": GREEN_DARK,
"bg": DARK_BG,
"panel": DARK_PANEL,
"text": DARK_TEXT,
"text_light": TEXT_LIGHT,
"text_muted": TEXT_MUTED,
"border": DARK_BORDER,
"dark": True,
}
# лишено заради зворотної сумісності з будь-яким кодом, що досі імпортує
# theme.PALETTE напряму (тепер -- це просто світла тема за замовчуванням)
PALETTE = PALETTE_LIGHT
# ============================================================
# Єдині кольори для графіків висоти/кута/глісади -- ОДНЕ джерело
# правди для "Місія" (mission_page.py/mission_editor.py: профіль
# висот) і "Аналіз" (elevation_view.py/angle_view.py/landing_view.py:
# профіль зльоту/маршруту/кута/глісади). Раніше кожен файл тримав
# власну копію цих самих кольорів -- і вони випадково розійшлись між
# "Місія" й "Аналіз" (лінія рельєфу/польоту відрізнялась кольором),
# довелось звіряти вручну й синхронізувати. Тепер кожен файл читає
# звідси (chart_colors(dark)), а не тримає свій хардкод.
CHART_COLORS_DARK = {
"line_primary": "#ff2222", # висота польоту/профіль маршруту/точка посадки -- усе, що стосується самого польоту
"line_terrain": "#3b82f6", # рельєф (лінія, без заливки)
"grid": "#3a3a3a",
"grid_minor": "#2a2a2a",
"text": "#e0e0e0",
"muted": "#888888",
"accent_green": "#5ecb5e", # інформаційні мітки (напр. DO_CHANGE_SPEED)
"marker_outline": "#dddddd",
}
CHART_COLORS_LIGHT = {
"line_primary": "#d62728",
"line_terrain": "#1f77b4",
"grid": "#e0e0e0",
"grid_minor": "#f0f0f0",
"text": "#000000",
"muted": "#666666",
"accent_green": "#2ca02c",
"marker_outline": "white",
}
def chart_colors(dark: bool) -> dict:
"""Повертає CHART_COLORS_DARK/LIGHT -- єдине джерело кольорів для
ВСІХ графіків профілю висоти/кута/глісади в програмі."""
return CHART_COLORS_DARK if dark else CHART_COLORS_LIGHT
# ============================================================
# Єдині кольори для ВСІХ повзунків/смуг прокрутки в програмі --
# tk.Scale (зум карти на "Місія") і tk.Scrollbar (зовнішня прокрутка
# сторінки "Місія", прокрутка таблиці місії, прокрутка вкладок
# "Аналіз") усі мають виглядати ОДНАКОВО. Свідомо ЗАВЖДИ tk.Scale/
# tk.Scrollbar, ніколи ttk.Scale/ttk.Scrollbar -- на Windows нативна
# тема ttk часто ігнорує кольори, задані через ttk.Style/опції, для
# цих двох класів віджетів (та сама причина, що й для інших "чорних"
# контейнерів на "Місія" раніше в цьому проєкті).
SLIDER_COLORS_DARK = {
"bg": "#2b2b2b", "trough": "#d9d9d9", "active": "#444444",
}
SLIDER_COLORS_LIGHT = {
"bg": "#888888", "trough": "#e6e6e6", "active": "#666666",
}
def slider_colors(dark: bool) -> dict:
"""Повертає SLIDER_COLORS_DARK/LIGHT -- єдине джерело кольорів для
ВСІХ tk.Scale/tk.Scrollbar у програмі."""
return SLIDER_COLORS_DARK if dark else SLIDER_COLORS_LIGHT
# ============================================================
# Єдині кольори/шрифт/форма стрілки для ВСІХ оверлеїв НА КАРТІ
# (компас+стрілка вітру й азимуту польоту -- Зліт/Посадка/Маршрут,
# позначки обходу НП -- Обліт НП/Оптимізація тощо). НЕ залежить від
# dark/dark -- це кольори для контрасту проти супутникового знімка під
# ними, а не проти фону самого застосунку (той не змінюється залежно
# від теми програми, тому й кольори лишаються сталими в обох темах).
#
# Виявлено на практиці: до появи цього єдиного джерела стрілка вітру
# на "Маршруті" й на "Зльоті/Посадці" мали РІЗНУ довжину й розмір
# шрифту, хоча малювали один і той самий фізичний показник -- просто
# кожне місце рахувало власні числа окремо. Будь-яка НОВА позначка на
# карті має брати кольори/шрифт/форму СТРІЛКИ звідси, а не вигадувати
# власні.
MAP_OVERLAY_COLORS = {
"wind": "#00BFFF",
"flight_az": "#39FF14",
"danger": "#FF4444",
"safe": "#44FF88",
}
MAP_OVERLAY_LABEL_FONT_SIZE = 11 # підпис біля стрілки (швидкість/напрямок/час)
MAP_OVERLAY_SMALL_FONT_SIZE = 8 # дрібніші підписи (N/E/S/W компаса тощо)
MAP_ARROW_WIDTH = 3
MAP_ARROW_SHAPE = (12, 14, 5) # tk create_line(arrowshape=...)
def map_overlay_font(size: int = MAP_OVERLAY_LABEL_FONT_SIZE) -> tuple:
"""Шрифт для підписів на карті -- та сама родина (_FONT_FAMILY), що
й у решті інтерфейсу, просто з явним розміром під конкретний підпис."""
return (_FONT_FAMILY, size, "bold")
MAP_LABEL_BACKDROP_ALPHA = 140 # 0-255, наскільки темна підкладка під текстом
def draw_label_backdrop(canvas, bbox: tuple, image_refs: list, tag: str = "overlay_layer") -> None:
"""Напівпрозора чорна підкладка під підписом на карті (bbox --
canvas.bbox(text_id), (x0,y0,x1,y1)) -- ЄДИНА реалізація для ВСІХ
підписів на будь-якій карті (Зліт/Посадка/Маршрут/Обліт НП/
Оптимізація), щоб текст не губився на строкатому супутниковому
знімку під ним.
Справжня альфа-прозорість через PIL (create_image), НЕ
canvas.create_rectangle(..., stipple="gray50") -- останній дає
грубий пунктирний патерн (не плавну прозорість), і коли поруч
опиняється кілька таких підкладок (напр. кілька позначок вітру на
"Маршруті") -- вони візуально зливаються в суцільну темну пляму,
а не виглядають як кілька акуратних напівпрозорих ярликів.
tag -- ВИЯВЛЕНО НА ТЕСТАХ: якщо жорстко прошити "overlay_layer"
всередині цієї функції, підкладка на "Маршруті" (де своя логіка
очищення/перемальовки прив'язана до тегу "route_wind") малюється
коректно, але з ЧУЖИМ тегом -- і код, що шукає елементи за
"route_wind" (напр. для очищення перед перемальовкою), її просто
не бачить і не видаляє при оновленні. Виклик МАЄ передавати СВІЙ
тег, що використовується на цьому конкретному canvas.
image_refs -- список (звичайний Python list), куди ОБОВ'ЯЗКОВО
треба покласти повернутий PhotoImage -- Tkinter не тримає власного
посилання на зображення canvas, без зовнішнього списку об'єкт
негайно збирається сміттярем і підкладка зникає з екрана."""
x0, y0, x1, y1 = bbox
x0, y0, x1, y1 = x0 - 3, y0 - 2, x1 + 3, y1 + 2
w, h = int(x1 - x0), int(y1 - y0)
if w <= 0 or h <= 0:
return
try:
from PIL import Image, ImageTk
img = Image.new("RGBA", (w, h), (0, 0, 0, MAP_LABEL_BACKDROP_ALPHA))
photo = ImageTk.PhotoImage(img)
image_refs.append(photo) # інакше збереться сміттярем одразу
canvas.create_image(x0, y0, image=photo, anchor="nw", tags=tag)
except ImportError:
# PIL відсутній (опційна залежність, див. README) -- запасний
# варіант через stipple, гірше виглядає, але не падає зовсім
canvas.create_rectangle(
x0, y0, x1, y1, fill="#000000", outline="", stipple="gray50",
tags=tag,
)
_FONT_FAMILY = "Segoe UI" # на Windows есть всегда; на других ОС ttk сам подберёт похожий
def apply_theme(root: tk.Tk, dark: bool = False) -> dict:
"""
Настраивает ttk.Style под тему Mission Planner. Повертає палітру
кольорів. Можна викликати ПОВТОРНО (при перемиканні світла/темна в
Конфігурації) -- ttk.Style().configure() одразу перефарбовує УСІ вже
створені ttk-віджети зі стандартними іменами стилів (TFrame/TLabel/
TButton тощо), без потреби перебудовувати їх заново. Віджети з
прямим bg= (не через ttk.Style) не підхоплюють це автоматично -- їх
перефарбовує сам виклик (app.py: apply_app_theme()).
"""
colors = PALETTE_DARK if dark else PALETTE_LIGHT
style = ttk.Style(root)
# 'clam' — единственная встроенная тема, которая реально позволяет
# перекрашивать фон/акценты кросс-платформенно (native-темы Windows
# игнорируют часть настроек цвета)
try:
style.theme_use("clam")
except tk.TclError:
pass
root.configure(bg=colors["bg"])
base_font = (_FONT_FAMILY, 9)
bold_font = (_FONT_FAMILY, 9, "bold")
style.configure(".", background=colors["bg"], foreground=colors["text"], font=base_font)
style.configure("TFrame", background=colors["bg"])
style.configure("TLabel", background=colors["bg"], foreground=colors["text"])
style.configure("TCheckbutton", background=colors["bg"], foreground=colors["text"])
style.map("TCheckbutton", background=[("active", colors["bg"])])
style.configure("TLabelframe", background=colors["bg"], bordercolor=colors["border"])
style.configure(
"TLabelframe.Label", background=colors["bg"],
foreground=NAVY if not dark else "#7fa8d9", font=bold_font,
)
# Treeview -- БЕЗЫМЕННИЙ (дефолтний) стиль, для будь-якого дерева/
# таблиці в проєкті, що НЕ використовує власний іменований стиль
# (напр. mission_table навмисне має власний "MissionBlack.Treeview"
# з додатковими кольорами виділення -- цей блок його не чіпає).
# ВАЖЛИВО: у темі "clam" ttk.Treeview має ОКРЕМИЙ від загального
# "background" параметр -- "fieldbackground" (колір області з самими
# рядками) -- він НЕ успадковується від style.configure(".", ...)
# вище автоматично. Без явного налаштування САМЕ "fieldbackground"
# тут -- дерево лишається з дефолтним (зазвичай білим) кольором
# реалізації теми "clam", тоді як текст (foreground) успадковується
# коректно -- звідси "світлий текст на білому тлі" в темній темі
# (реальний баг, знайдений і підтверджений на живому застосунку:
# діалог "Файли SD" -- sd_file_manager.py, там Treeview створюється
# без style= взагалі).
style.configure(
"Treeview", background=colors["bg"], fieldbackground=colors["bg"], foreground=colors["text"],
)
style.map(
"Treeview",
background=[("selected", colors["blue"])],
foreground=[("selected", "white")],
)
style.configure(
"Treeview.Heading", background=colors["panel"], foreground=colors["text"], relief="flat",
)
style.map("Treeview.Heading", background=[("active", colors["border"])])
style.configure("TEntry", fieldbackground=colors["panel"], foreground=colors["text"], bordercolor=colors["border"])
style.configure(
"TSpinbox", fieldbackground=colors["panel"], foreground=colors["text"],
bordercolor=colors["border"], arrowsize=12,
)
style.configure(
"TCombobox", fieldbackground=colors["panel"], foreground=colors["text"],
bordercolor=colors["border"], background=colors["panel"], arrowcolor=colors["text"],
)
style.map(
"TCombobox",
fieldbackground=[("readonly", colors["panel"])],
# без цього кнопка-стрілка (окремий елемент стилю, НЕ те саме,
# що fieldbackground -- той чіпає лише текстове поле) лишається
# на стандартному СВІТЛОМУ кольорі теми ttk за замовчуванням і
# особливо помітно "спалахує" світлим при наведенні миші поверх
# темної теми навколо -- саме той ефект, що впадав у вічі.
background=[("readonly", colors["panel"]), ("active", colors["border"]), ("!active", colors["panel"])],
arrowcolor=[("disabled", colors["text_muted"])],
)
# список випадаючого combobox -- окремий шар (Tk listbox всередині
# popdown-вікна), style.map тут не діє, задається напряму опцією
root.option_add("*TCombobox*Listbox.background", colors["panel"])
root.option_add("*TCombobox*Listbox.foreground", colors["text"])
# обычные кнопки — зелёный акцент (як в лого), як основні дії в MP
style.configure(
"TButton", background=GREEN_ACCENT, foreground=TEXT_LIGHT,
borderwidth=0, focusthickness=0, padding=(12, 6), font=bold_font,
)
style.map(
"TButton",
background=[("disabled", "#B7C4D1"), ("pressed", GREEN_DARK), ("active", GREEN_ACCENT_HOVER)],
foreground=[("disabled", "#E8ECEF")],
)
# нейтральные кнопки — тот же спокойный вид, что у вкладок на страницах
# "Аналіз"/"Довідка" (без синей заливки), для действий, которые не
# нужно акцентировать как основные (напр. "Завантажити"/"Зберегти").
# При нажатии — та же логика, что у остальных элементов навигации:
# тёмный/чёрный фон + "утопленный" вид кнопки (чуть меньше на вид).
secondary_bg = "#DEE3E8" if not dark else "#3a3a3a"
secondary_fg = colors["text"]
style.configure(
"Secondary.TButton", background=secondary_bg, foreground=secondary_fg,
borderwidth=0, focusthickness=0, padding=(12, 6), font=bold_font,
)
style.map(
"Secondary.TButton",
background=[
("disabled", "#EDEFF1" if not dark else "#2a2a2a"),
("pressed", HEADER_BG),
("active", "#C9CFD6" if not dark else "#4a4a4a"),
],
foreground=[("disabled", "#9AA5AE"), ("pressed", TEXT_LIGHT)],
padding=[("pressed", (10, 5))],
)
# вкладки блокнота — светло-серые/тёмно-серые неактивные, тёмно-синие активные
tab_idle_bg = "#DEE3E8" if not dark else "#2b2b2b"
style.configure("TNotebook", background=colors["bg"], borderwidth=0, tabmargins=(2, 4, 2, 0))
style.configure(
"TNotebook.Tab", background=tab_idle_bg, foreground=colors["text"],
padding=(14, 7), font=bold_font, borderwidth=0,
)
style.map(
"TNotebook.Tab",
background=[("selected", NAVY_DARK)],
foreground=[("selected", TEXT_LIGHT)],
expand=[("selected", (1, 1, 1, 0))],
)
# полосы прокрутки — нейтральные, но не выбивающиеся из темы
style.configure(
"TScrollbar", background=tab_idle_bg, troughcolor=colors["bg"],
bordercolor=colors["border"], arrowsize=12,
)
# шапка окна -- та сама в обох темах (вона й так темна за задумом)
style.configure("Header.TFrame", background=HEADER_BG)
style.configure("Header.TLabel", background=HEADER_BG, foreground=TEXT_LIGHT, font=(_FONT_FAMILY, 13, "bold"))
style.configure("HeaderSub.TLabel", background=HEADER_BG, foreground=TEXT_MUTED, font=(_FONT_FAMILY, 9))
# статус-бар -- та сама в обох темах
style.configure("Status.TFrame", background=NAVY_DARK)
style.configure("Status.TLabel", background=NAVY_DARK, foreground=TEXT_MUTED, font=base_font)
# переключатель языка — маленькие кнопки-тумблеры на тёмном фоне шапки
style.configure(
"LangToggle.TButton", background=GREEN_DARK, foreground=TEXT_MUTED,
borderwidth=0, padding=(8, 4), font=bold_font,
)
style.map(
"LangToggle.TButton",
background=[("pressed", GREEN_ACCENT), ("active", HEADER_BG)],
foreground=[("pressed", TEXT_LIGHT)],
)
style.configure(
"LangToggleActive.TButton", background=GREEN_ACCENT, foreground=TEXT_LIGHT,
borderwidth=0, padding=(8, 4), font=bold_font,
)
style.map("LangToggleActive.TButton", background=[("active", GREEN_ACCENT_HOVER)])
return colors
def set_window_icon(root: tk.Tk, path: str) -> bool:
"""
Пробует установить иконку окна из файла (.png/.gif через PhotoImage,
.ico через iconbitmap на Windows). Возвращает True при успехе, иначе
молча отступает — отсутствие иконки не должно ронять программу.
"""
try:
if path.lower().endswith(".ico"):
root.iconbitmap(path)
else:
img = tk.PhotoImage(file=path)
root.iconphoto(True, img)
root._icon_ref = img # держим ссылку, иначе GC съест картинку
return True
except (tk.TclError, FileNotFoundError):
return False
def make_text_readonly(widget: tk.Text) -> None:
"""Text-віджет доступний для читання Й КОПІЮВАННЯ (виділення мишею,
Ctrl+C, Ctrl+A, копіювання через контекстне меню правої кнопки),
але недоступний для редагування.
СПІЛЬНА функція для ВСІХ read-only текстових полів у проєкті
(раніше -- окремі копії того самого коду в help_page.py,
analysis_page.py, ardupilot_link.py, sd_file_manager.py -- баг
"Ctrl+C не працює" виявлявся й виправлявся по одному разу на
кожен файл окремо, поки хтось знову на нього не натикався в
ЩЕ ОДНОМУ місці; тепер один спільний фікс покриває всі й будь-які
майбутні місця одразу).
Copy РЕАЛІЗОВАНО ЯВНО (widget.clipboard_clear()+append()), і
визначається за event.KEYCODE (апаратний код фізичної клавіші), А
НЕ event.keysym (символ, який ця клавіша ДРУКУЄ У ПОТОЧНІЙ
РОЗКЛАДЦІ). Причина: на кириличній розкладці клавіатури фізична
клавіша "C" дає зовсім ІНШИЙ keysym (щось на кшталт "Cyrillic_es",
не латинське "c") -- прив'язка САМЕ на рядок "<Control-c>" тому
просто НЕ спрацьовувала на такій розкладці (підтверджено
користувачем: на англійській розкладці Ctrl+C працював, на
кириличній -- ні). keycode -- фізичний скан-код клавіші, однаковий
незалежно від розкладки (67 -- "C", 65 -- "A", стандартні Windows
Virtual-Key Codes).
На відміну від звичайного widget.config(state="disabled") -- той у
tkinter блокує НЕ ЛИШЕ сам ввід тексту, а й виділення мишею та
стандартні комбінації типу Ctrl+C, оскільки disabled Text взагалі
не приймає фокус для таких операцій. Тримаємо state="normal", і
замість цього перехоплюємо натискання клавіш -- Control-комбінації
і навігаційні клавіші (стрілки, Home/End, PageUp/Down, Tab)
пропускаємо як є, решту (звичайний друкований ввід, Delete,
BackSpace тощо) блокуємо, повертаючи "break"."""
def _copy_selection():
try:
selected = widget.get("sel.first", "sel.last")
except tk.TclError:
return
widget.clipboard_clear()
widget.clipboard_append(selected)
def _select_all():
widget.tag_add("sel", "1.0", "end")
def _on_key(event):
if event.state & 0x4:
if event.keycode == 67:
_copy_selection()
return "break"
if event.keycode == 65:
_select_all()
return "break"
return None
if event.keysym in (
"Left", "Right", "Up", "Down", "Home", "End",
"Prior", "Next", "Tab", "Shift_L", "Shift_R",
):
return None
return "break"
def _on_right_click(event):
menu = tk.Menu(widget, tearoff=0)
menu.add_command(label=i18n.t("ctx_copy"), command=_copy_selection)
menu.tk_popup(event.x_root, event.y_root)
widget.config(state="normal")
widget.bind("<Key>", _on_key)
widget.bind("<Button-3>", _on_right_click)