diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index 1ab39ded..bc7045d2 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -38,6 +38,7 @@ from tagstudio.core.library.alchemy.enums import SortingModeEnum from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel from tagstudio.qt.flowlayout import FlowLayout from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.mnemonics import assign_mnemonics from tagstudio.qt.pagination import Pagination from tagstudio.qt.platform_strings import trash_term from tagstudio.qt.resource_manager import ResourceManager @@ -52,142 +53,6 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) -def remove_accelerator_marker(label: str) -> str: - """Remove existing accelerator markers (&) from a label.""" - result = "" - skip = False - for i, ch in enumerate(label): - if skip: - skip = False - continue - if ch == "&": - # escaped ampersand "&&" - if i + 1 < len(label) and label[i + 1] == "&": - result += "&" - skip = True - # otherwise skip this '&' - continue - result += ch - return result - - -# Additional weight for first character in string -FIRST_CHARACTER_EXTRA_WEIGHT = 50 -# Additional weight for the beginning of a word -WORD_BEGINNING_EXTRA_WEIGHT = 50 -# Additional weight for a 'wanted' accelerator ie string with '&' -WANTED_ACCEL_EXTRA_WEIGHT = 150 - - -def calculate_weights(text: str): - weights: dict[int, str] = {} - - pos = 0 - start_character = True - wanted_character = False - - while pos < len(text): - c = text[pos] - - # skip non typeable characters - if not c.isalnum() and c != "&": - start_character = True - pos += 1 - continue - - weight = 1 - - # add special weight to first character - if pos == 0: - weight += FIRST_CHARACTER_EXTRA_WEIGHT - elif start_character: # add weight to word beginnings - weight += WORD_BEGINNING_EXTRA_WEIGHT - start_character = False - - # add weight to characters that have an & beforehand - if wanted_character: - weight += WANTED_ACCEL_EXTRA_WEIGHT - wanted_character = False - - # add decreasing weight to left characters - if pos < 50: - weight += 50 - pos - - # try to preserve the wanted accelerators - if c == "&" and (pos != len(text) - 1 and text[pos + 1] != "&" and text[pos + 1].isalnum()): - wanted_character = True - pos += 1 - continue - - while weight in weights: - weight += 1 - - if c != "&": - weights[weight] = c - - pos += 1 - - # update our maximum weight - max_weight = 0 if len(weights) == 0 else max(weights.keys()) - return max_weight, weights - - -def insert_mnemonic(label: str, char: str) -> str: - pos = label.lower().find(char) - if pos >= 0: - return label[:pos] + "&" + label[pos:] - return label - - -def assign_mnemonics(menu: QMenu): - # Collect actions - actions = [a for a in menu.actions() if not a.isSeparator()] - - # Sequence map: mnemonic key -> QAction - sequence_to_action: dict[str, QAction] = {} - - final_text: dict[QAction, str] = {} - - actions.reverse() - - while len(actions) > 0: - action = actions.pop() - label = action.text() - _, weights = calculate_weights(label) - - chosen_char = None - - # Try candidates, starting from highest weight - for weight in sorted(weights.keys(), reverse=True): - c = weights[weight].lower() - other = sequence_to_action.get(c) - - if other is None: - chosen_char = c - sequence_to_action[c] = action - break - else: - # Compare weights with existing action - other_max, _ = calculate_weights(remove_accelerator_marker(other.text())) - if weight > other_max: - # Take over from weaker action - actions.append(other) - sequence_to_action[c] = action - chosen_char = c - - # Apply mnemonic if found - if chosen_char: - plain = remove_accelerator_marker(label) - new_label = insert_mnemonic(plain, chosen_char) - final_text[action] = new_label - else: - # No mnemonic assigned → clean text - final_text[action] = remove_accelerator_marker(label) - - for a, t in final_text.items(): - a.setText(t) - - class MainMenuBar(QMenuBar): file_menu: QMenu open_library_action: QAction diff --git a/src/tagstudio/qt/mnemonics.py b/src/tagstudio/qt/mnemonics.py new file mode 100644 index 00000000..3ef8bbdf --- /dev/null +++ b/src/tagstudio/qt/mnemonics.py @@ -0,0 +1,142 @@ +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMenu + + +def remove_mnemonic_marker(label: str) -> str: + """Remove existing accelerator markers (&) from a label.""" + result = "" + skip = False + for i, ch in enumerate(label): + if skip: + skip = False + continue + if ch == "&": + # escaped ampersand "&&" + if i + 1 < len(label) and label[i + 1] == "&": + result += "&" + skip = True + # otherwise skip this '&' + continue + result += ch + return result + + +# Additional weight for first character in string +FIRST_CHARACTER_EXTRA_WEIGHT = 50 +# Additional weight for the beginning of a word +WORD_BEGINNING_EXTRA_WEIGHT = 50 +# Additional weight for a 'wanted' accelerator ie string with '&' +WANTED_ACCEL_EXTRA_WEIGHT = 150 + + +def calculate_weights(text: str): + weights: dict[int, str] = {} + + pos = 0 + start_character = True + wanted_character = False + + while pos < len(text): + c = text[pos] + + # skip non typeable characters + if not c.isalnum() and c != "&": + start_character = True + pos += 1 + continue + + weight = 1 + + # add special weight to first character + if pos == 0: + weight += FIRST_CHARACTER_EXTRA_WEIGHT + elif start_character: # add weight to word beginnings + weight += WORD_BEGINNING_EXTRA_WEIGHT + start_character = False + + # add weight to characters that have an & beforehand + if wanted_character: + weight += WANTED_ACCEL_EXTRA_WEIGHT + wanted_character = False + + # add decreasing weight to left characters + if pos < 50: + weight += 50 - pos + + # try to preserve the wanted accelerators + if c == "&" and (pos != len(text) - 1 and text[pos + 1] != "&" and text[pos + 1].isalnum()): + wanted_character = True + pos += 1 + continue + + while weight in weights: + weight += 1 + + if c != "&": + weights[weight] = c + + pos += 1 + + # update our maximum weight + max_weight = 0 if len(weights) == 0 else max(weights.keys()) + return max_weight, weights + + +def insert_mnemonic(label: str, char: str) -> str: + pos = label.lower().find(char) + if pos >= 0: + return label[:pos] + "&" + label[pos:] + return label + + +def assign_mnemonics(menu: QMenu): + # Collect actions + actions = [a for a in menu.actions() if not a.isSeparator()] + + # Sequence map: mnemonic key -> QAction + sequence_to_action: dict[str, QAction] = {} + + final_text: dict[QAction, str] = {} + + actions.reverse() + + while len(actions) > 0: + action = actions.pop() + label = action.text() + _, weights = calculate_weights(label) + + chosen_char = None + + # Try candidates, starting from highest weight + for weight in sorted(weights.keys(), reverse=True): + c = weights[weight].lower() + other = sequence_to_action.get(c) + + if other is None: + chosen_char = c + sequence_to_action[c] = action + break + else: + # Compare weights with existing action + other_max, _ = calculate_weights(remove_mnemonic_marker(other.text())) + if weight > other_max: + # Take over from weaker action + actions.append(other) + sequence_to_action[c] = action + chosen_char = c + + # Apply mnemonic if found + if chosen_char: + plain = remove_mnemonic_marker(label) + new_label = insert_mnemonic(plain, chosen_char) + final_text[action] = new_label + else: + # No mnemonic assigned → clean text + final_text[action] = remove_mnemonic_marker(label) + + for a, t in final_text.items(): + a.setText(t) diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index 22bd606e..6c54d85f 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -6,6 +6,8 @@ from typing import Any import structlog import ujson +from tagstudio.qt.mnemonics import remove_mnemonic_marker + logger = structlog.get_logger(__name__) DEFAULT_TRANSLATION = "en" @@ -61,9 +63,7 @@ class Translator: self._strings = self.__get_translation_dict(lang) if system() == "Darwin": for k, v in self._strings.items(): - self._strings[k] = ( - v.replace("&&", "").replace("&", "", 1).replace("", "&&") - ) + self._strings[k] = remove_mnemonic_marker(v) def __format(self, text: str, **kwargs) -> str: try: