Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 1 addition & 136 deletions src/tagstudio/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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
Expand All @@ -52,142 +53,6 @@
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
Expand Down
142 changes: 142 additions & 0 deletions src/tagstudio/qt/mnemonics.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions src/tagstudio/qt/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import structlog
import ujson

from tagstudio.qt.mnemonics import remove_mnemonic_marker

logger = structlog.get_logger(__name__)

DEFAULT_TRANSLATION = "en"
Expand Down Expand Up @@ -61,9 +63,7 @@ def change_language(self, lang: str):
self._strings = self.__get_translation_dict(lang)
if system() == "Darwin":
for k, v in self._strings.items():
self._strings[k] = (
v.replace("&&", "<ESC_AMP>").replace("&", "", 1).replace("<ESC_AMP>", "&&")
)
self._strings[k] = remove_mnemonic_marker(v)

def __format(self, text: str, **kwargs) -> str:
try:
Expand Down