Show accelerators/shortcuts in menus

This commit is contained in:
Isaiah Odhner 2023-04-18 20:43:51 -04:00
parent 365eb6e4c7
commit 4cbafea3cb
5 changed files with 135 additions and 58 deletions

View File

@ -19,16 +19,20 @@
"fudgedness",
"Haha",
"hkey",
"humbnail",
"𝗟𝙇",
"llpaper",
"modd",
"Odhner",
"Playscii",
"pycache",
"pypixelart",
"Shft",
"stransi",
"STRINGTABLE",
"ufeff",
"undos",
"ustom",
"vkey"
]
}

View File

@ -1,4 +1,6 @@
from typing import Optional
import json
import re
translations = {}
@ -28,14 +30,77 @@ try:
except FileNotFoundError:
pass
def get(base_language_str: str) -> str:
def get(base_language_str: str, *interpolations: str) -> str:
"""Get a localized string."""
if base_language_str in translations:
return translations[base_language_str]
else:
def find_localization(base_language_str: str):
amp_index = index_of_hotkey(base_language_str)
if amp_index > -1:
without_hotkey = remove_hotkey(base_language_str)
if without_hotkey in translations:
hotkey_def = base_language_str[amp_index:amp_index + 2]
if translations[without_hotkey].upper().find(hotkey_def.upper()) > -1:
return translations[without_hotkey]
else:
if has_hotkey(translations[without_hotkey]):
# window.console && console.warn(`Localization has differing accelerator (hotkey) hint: '${translations[without_hotkey]}' vs '${base_language_str}'`);
# @TODO: detect differing accelerator more generally
return f"{remove_hotkey(translations[without_hotkey])} ({hotkey_def})"
return f"{translations[without_hotkey]} ({hotkey_def})"
if base_language_str in translations:
return translations[base_language_str]
# special case for menu items, where we need to split the string into two parts to translate them separately
# (maybe only because of how I preprocessed the localization files)
parts = base_language_str.split("\t")
if len(parts) == 2:
parts[0] = find_localization(parts[0])
return "\t".join(parts)
# special handling for ellipsis
if base_language_str[-3:] == "...":
return find_localization(base_language_str[:-3]) + "..."
if base_language_str not in untranslated:
untranslated.add(base_language_str)
# append to untranslated strings file
with open("localization/untranslated.txt", "a") as f:
f.write(base_language_str + "\n")
return base_language_str
def interpolate(text: str, interpolations: list):
for i in range(len(interpolations)):
text = text.replace(f"%{i + 1}", interpolations[i])
return text
return interpolate(find_localization(base_language_str), interpolations)
# & defines accelerators (hotkeys) in menus and buttons and things, which get underlined in the UI.
# & can be escaped by doubling it, e.g. "&Taskbar && Start Menu"
def index_of_hotkey(text: str) -> int:
# Returns the index of the ampersand that defines a hotkey, or -1 if not present.
# The space here handles beginning-of-string matching and counteracts the offset for the [^&] so it acts like a negative lookbehind
m = re.search(r"[^&]&[^&\s]", f" {text}")
return m.start() if m else -1
def has_hotkey(text: str) -> bool:
return index_of_hotkey(text) != -1
def remove_hotkey(text: str) -> str:
text = re.sub(r"\s?\(&.\)", "", text)
text = re.sub(r"([^&]|^)&([^&\s])", r"\1\2", text)
return text
def markup_hotkey(text: str) -> str:
"""Returns Rich API-compatible markup underlining the hotkey if present."""
index = index_of_hotkey(text)
if index == -1:
return text
else:
return text[:index] + f"[u]{text[index + 1]}[/u]" + text[index + 2:]
def get_hotkey(text: str) -> Optional[str]:
"""Returns the hotkey if present."""
index = index_of_hotkey(text)
if index == -1:
return None
else:
return text[index + 1]

View File

@ -23,21 +23,14 @@ def index_of_hotkey(text):
def has_hotkey(text):
return index_of_hotkey(text) != -1
def remove_hotkey(text):
text = re.sub(r"\s?\(&.\)", "", text)
text = re.sub(r"([^&]|^)&([^&\s])", r"\1\2", text)
return text
def remove_ellipsis(string):
return string.replace("...", "")
def only_unique(value, index, self):
return self.index(value) == index
def get_strings(lang):
rc_files = glob.glob(f"{os.path.dirname(__file__)}/{lang}/**/*.rc", recursive=True)
for rc_file in rc_files:

View File

@ -9,6 +9,7 @@ from textual.geometry import Offset, Region, Size
from textual.reactive import var, reactive
from textual.widget import Widget
from textual.widgets import Button, Static
from localization.i18n import markup_hotkey
def to_snake_case(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
@ -81,6 +82,20 @@ class Menu(Container):
self.parent_menu_item = parent_menu_item
self.add_class("menu_popup")
self.styles.offset = (parent_menu_item.region.x, parent_menu_item.region.y + parent_menu_item.region.height)
# Find the widest label
max_width = 0
for item in self.items:
if isinstance(item, MenuItem):
if len(item.label.plain) > max_width:
max_width = len(item.label.plain)
# Split on tab character and align the shortcuts
for item in self.items:
if isinstance(item, MenuItem):
markup_parts = item.label.markup.split("\t")
plain_parts = item.label.plain.split("\t")
if len(markup_parts) > 1:
item.label = markup_parts[0] + " " * (max_width - len(plain_parts[0])) + markup_parts[1]
def close(self):
for item in self.items:
@ -103,7 +118,7 @@ class MenuItem(Button):
def __init__(self, name: str, action = None, id: str = None, submenu = None, grayed = False, **kwargs) -> None:
"""Initialize a menu item."""
super().__init__(name, **kwargs)
super().__init__(markup_hotkey(name), **kwargs)
self.add_class("menu_item")
self.disabled = grayed
self.action = action

View File

@ -1162,69 +1162,69 @@ class PaintApp(App):
yield Header()
with Container(id="paint"):
yield MenuBar([
MenuItem(_("File"), submenu=Menu([
MenuItem(_("New"), self.action_new, 57600),
MenuItem(_("Open"), self.action_open, 57601),
MenuItem(_("Save"), self.action_save, 57603),
MenuItem(_("Save As"), self.action_save_as, 57604),
MenuItem(_("&File"), submenu=Menu([
MenuItem(_("&New\tCtrl+N"), self.action_new, 57600),
MenuItem(_("&Open...\tCtrl+O"), self.action_open, 57601),
MenuItem(_("&Save\tCtrl+S"), self.action_save, 57603),
MenuItem(_("Save &As..."), self.action_save_as, 57604),
Separator(),
MenuItem(_("Print Preview"), self.action_print_preview, 57609),
MenuItem(_("Page Setup..."), self.action_page_setup, 57605),
MenuItem(_("Print..."), self.action_print, 57607),
MenuItem(_("Print Pre&view"), self.action_print_preview, 57609),
MenuItem(_("Page Se&tup..."), self.action_page_setup, 57605),
MenuItem(_("&Print...\tCtrl+P"), self.action_print, 57607),
Separator(),
MenuItem(_("Send..."), self.action_send, 37662),
MenuItem(_("S&end..."), self.action_send, 37662),
Separator(),
MenuItem(_("Set As Wallpaper (Tiled)"), self.action_set_as_wallpaper_tiled, 57677),
MenuItem(_("Set As Wallpaper (Centered)"), self.action_set_as_wallpaper_centered, 57675),
MenuItem(_("Set As &Wallpaper (Tiled)"), self.action_set_as_wallpaper_tiled, 57677),
MenuItem(_("Set As Wa&llpaper (Centered)"), self.action_set_as_wallpaper_centered, 57675),
Separator(),
MenuItem(_("Recent File"), self.action_recent_file, 57616, grayed=True),
Separator(),
MenuItem(_("Exit"), self.action_exit, 57665),
MenuItem(_("E&xit\tAlt+F4"), self.action_exit, 57665),
])),
MenuItem(_("Edit"), submenu=Menu([
MenuItem(_("Undo"), self.action_undo, 57643),
MenuItem(_("Repeat"), self.action_redo, 57644),
MenuItem(_("&Edit"), submenu=Menu([
MenuItem(_("&Undo\tCtrl+Z"), self.action_undo, 57643),
MenuItem(_("&Repeat\tF4"), self.action_redo, 57644),
Separator(),
MenuItem(_("Cut"), self.action_cut, 57635),
MenuItem(_("Copy"), self.action_copy, 57634),
MenuItem(_("Paste"), self.action_paste, 57637),
MenuItem(_("Clear Selection"), self.action_clear_selection, 57632),
MenuItem(_("Select All"), self.action_select_all, 57642),
MenuItem(_("Cu&t\tCtrl+X"), self.action_cut, 57635),
MenuItem(_("&Copy\tCtrl+C"), self.action_copy, 57634),
MenuItem(_("&Paste\tCtrl+V"), self.action_paste, 57637),
MenuItem(_("C&lear Selection\tDel"), self.action_clear_selection, 57632),
MenuItem(_("Select &All\tCtrl+A"), self.action_select_all, 57642),
Separator(),
MenuItem(_("Copy To..."), self.action_copy_to, 37663),
MenuItem(_("Paste From..."), self.action_paste_from, 37664),
MenuItem(_("C&opy To..."), self.action_copy_to, 37663),
MenuItem(_("Paste &From..."), self.action_paste_from, 37664),
])),
MenuItem(_("View"), submenu=Menu([
MenuItem(_("Tool Box"), self.action_toggle_tools_box, 59415),
MenuItem(_("Color Box"), self.action_toggle_colors_box, 59416),
MenuItem(_("Status Bar"), self.action_toggle_status_bar, 59393),
MenuItem(_("Text Toolbar"), self.action_text_toolbar, 37678),
MenuItem(_("&View"), submenu=Menu([
MenuItem(_("&Tool Box\tCtrl+T"), self.action_toggle_tools_box, 59415),
MenuItem(_("&Color Box\tCtrl+L"), self.action_toggle_colors_box, 59416),
MenuItem(_("&Status Bar"), self.action_toggle_status_bar, 59393),
MenuItem(_("T&ext Toolbar"), self.action_text_toolbar, 37678),
Separator(),
MenuItem(_("Zoom"), submenu=Menu([
MenuItem(_("Normal Size"), self.action_normal_size, 37670),
MenuItem(_("Large Size"), self.action_large_size, 37671),
MenuItem(_("Custom..."), self.action_custom_zoom, 37672),
MenuItem(_("&Zoom"), submenu=Menu([
MenuItem(_("&Normal Size\tCtrl+PgUp"), self.action_normal_size, 37670),
MenuItem(_("&Large Size\tCtrl+PgDn"), self.action_large_size, 37671),
MenuItem(_("C&ustom..."), self.action_custom_zoom, 37672),
Separator(),
MenuItem(_("Show Grid"), self.action_show_grid, 37677),
MenuItem(_("Show Thumbnail"), self.action_show_thumbnail, 37676),
MenuItem(_("Show &Grid\tCtrl+G"), self.action_show_grid, 37677),
MenuItem(_("Show T&humbnail"), self.action_show_thumbnail, 37676),
])),
MenuItem(_("View Bitmap"), self.action_view_bitmap, 37673),
MenuItem(_("&View Bitmap\tCtrl+F"), self.action_view_bitmap, 37673),
])),
MenuItem(_("Image"), submenu=Menu([
MenuItem(_("Flip/Rotate..."), self.action_flip_rotate, 37680),
MenuItem(_("Stretch/Skew..."), self.action_stretch_skew, 37681),
MenuItem(_("Invert Colors"), self.action_invert_colors, 37682),
MenuItem(_("Attributes..."), self.action_attributes, 37683),
MenuItem(_("Clear Image"), self.action_clear_image, 37684),
MenuItem(_("Draw Opaque"), self.action_draw_opaque, 6868),
MenuItem(_("&Image"), submenu=Menu([
MenuItem(_("&Flip/Rotate...\tCtrl+R"), self.action_flip_rotate, 37680),
MenuItem(_("&Stretch/Skew...\tCtrl+W"), self.action_stretch_skew, 37681),
MenuItem(_("&Invert Colors\tCtrl+I"), self.action_invert_colors, 37682),
MenuItem(_("&Attributes...\tCtrl+E"), self.action_attributes, 37683),
MenuItem(_("&Clear Image\tCtrl+Shft+N"), self.action_clear_image, 37684),
MenuItem(_("&Draw Opaque"), self.action_draw_opaque, 6868),
])),
MenuItem(_("Colors"), submenu=Menu([
MenuItem(_("Edit Colors..."), self.action_edit_colors, 6869),
MenuItem(_("&Colors"), submenu=Menu([
MenuItem(_("&Edit Colors..."), self.action_edit_colors, 6869),
])),
MenuItem(_("Help"), submenu=Menu([
MenuItem(_("Help Topics"), self.action_help_topics, 57670),
MenuItem(_("&Help"), submenu=Menu([
MenuItem(_("&Help Topics"), self.action_help_topics, 57670),
Separator(),
MenuItem(_("About Paint"), self.action_about_paint, 57664),
MenuItem(_("&About Paint"), self.action_about_paint, 57664),
])),
])
yield Container(