Skip to content

Commit

Permalink
Merge branch 'issue_541_use_event_code_for_shortcuts' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
jzohrab committed Dec 23, 2024
2 parents 5d12302 + c32ed7f commit 51017c1
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 68 deletions.
7 changes: 6 additions & 1 deletion lute/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@

from lute.models.book import Book
from lute.models.language import Language
from lute.settings.current import refresh_global_settings, current_settings
from lute.settings.current import (
refresh_global_settings,
current_settings,
current_hotkeys,
)
from lute.models.repositories import UserSettingRepository
from lute.book.stats import Service as StatsService

Expand Down Expand Up @@ -123,6 +127,7 @@ def inject_menu_bar_vars():
"backup_last_display_date": bs.last_backup_display_date,
"backup_time_since": bs.time_since_last_backup,
"user_settings": json.dumps(current_settings),
"user_hotkeys": json.dumps(current_hotkeys),
}
return ret

Expand Down
42 changes: 21 additions & 21 deletions lute/db/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,27 @@ def add_default_user_settings(session, default_user_backup_path):
# Keyboard shortcuts. These have default values assigned
# as they were the hotkeys defined in the initial Lute
# release.
"hotkey_StartHover": "escape",
"hotkey_PrevWord": "arrowleft",
"hotkey_NextWord": "arrowright",
"hotkey_StatusUp": "arrowup",
"hotkey_StatusDown": "arrowdown",
"hotkey_Bookmark": "b",
"hotkey_CopySentence": "c",
"hotkey_CopyPara": "shift+c",
"hotkey_TranslateSentence": "t",
"hotkey_TranslatePara": "shift+t",
"hotkey_NextTheme": "m",
"hotkey_ToggleHighlight": "h",
"hotkey_ToggleFocus": "f",
"hotkey_Status1": "1",
"hotkey_Status2": "2",
"hotkey_Status3": "3",
"hotkey_Status4": "4",
"hotkey_Status5": "5",
"hotkey_StatusIgnore": "i",
"hotkey_StatusWellKnown": "w",
"hotkey_SaveTerm": "ctrl+enter",
"hotkey_Bookmark": "KeyB",
"hotkey_CopyPara": "shift+KeyC",
"hotkey_CopySentence": "KeyC",
"hotkey_NextTheme": "KeyM",
"hotkey_NextWord": "ArrowRight",
"hotkey_PrevWord": "ArrowLeft",
"hotkey_SaveTerm": "ctrl+Enter",
"hotkey_StartHover": "Escape",
"hotkey_Status1": "Digit1",
"hotkey_Status2": "Digit2",
"hotkey_Status3": "Digit3",
"hotkey_Status4": "Digit4",
"hotkey_Status5": "Digit5",
"hotkey_StatusDown": "ArrowDown",
"hotkey_StatusIgnore": "KeyI",
"hotkey_StatusUp": "ArrowUp",
"hotkey_StatusWellKnown": "KeyW",
"hotkey_ToggleFocus": "KeyF",
"hotkey_ToggleHighlight": "KeyH",
"hotkey_TranslatePara": "shift+KeyT",
"hotkey_TranslateSentence": "KeyT",
# New hotkeys. These must have empty values, because
# users may have already setup their hotkeys, and we can't
# assume that a given key combination is free:
Expand Down
10 changes: 10 additions & 0 deletions lute/settings/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,27 @@
# The current user settings, key/value dict.
current_settings = {}

# Current user hotkey mappings, mapping to mapping_name dict.
current_hotkeys = {}


def refresh_global_settings(session):
"Refresh all settings dictionary."
# Have to reload to not mess up any references
# (e.g. during testing).
current_settings.clear()
current_hotkeys.clear()

settings = session.query(UserSetting).all()
for s in settings:
current_settings[s.key] = s.value

hotkeys = [
s for s in settings if s.key.startswith("hotkey_") and (s.value or "") != ""
]
for h in hotkeys:
current_hotkeys[h.value] = h.key

# Convert some ints into bools.
boolkeys = [
"open_popup_in_new_tab",
Expand Down
59 changes: 54 additions & 5 deletions lute/static/js/lute-hotkey-utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/**
* Get the pressed keys as a string, eg 'meta-c', 'shift-a'.
* Get the pressed keys as a string, eg 'meta-KeyC', 'shift-KeyA'.
*
* Note that there _must_ be a "regular" key pressed as well.
* If only meta/alt/ctl/shift are pressed, returns null.
* If only meta/alt/ctl/shift are pressed, returns something like 'meta-MetaLeft'.
*/
function get_pressed_keys_as_string(event) {
const keys = [];
Expand All @@ -13,13 +12,42 @@ function get_pressed_keys_as_string(event) {
if (event.altKey) keys.push('alt');
if (event.metaKey) keys.push('meta');

let code = event.code ?? event.originalEvent?.code;
// console.log(`event.code = ${code}`);
// console.log('event = ', event)
keys.push(code);
const ret = keys.join('+');
// console.log(`got hotkey = ${ret}`);
return ret;
}


/**
* Function for legacy get_pressed_keys_as_string
*
* Per https://github.com/LuteOrg/lute-v3/issues/541, Lute used to use
* the event.key for keyboard shortcuts, but that caused problems for
* changing keyboard layouts.
*
* This function uses the old way of "event.key" to find the key
* pressed, as a fallback for users who have the old keyboard
* mappings.
*/
function _legacy_pressed_key_string(event) {
const keys = [];

// Check for modifier keys
if (event.ctrlKey) keys.push('ctrl');
if (event.shiftKey) keys.push('shift');
if (event.altKey) keys.push('alt');
if (event.metaKey) keys.push('meta');

// Map special keys to names if needed
const keyMap = {
' ': 'space'
};

if (event.key == null) {
// window.alert("no key for event?");
return null;
}

Expand All @@ -29,6 +57,27 @@ function get_pressed_keys_as_string(event) {

keys.push(actual_key);
const ret = keys.join('+');
// console.log(`got hotkey = ${ret}`);
// console.log(`got legacy hotkey = ${ret}`);
return ret;
}


/**
* get the "hotkey name" (e.g. "hotkey_Status5") for the given event
* from LUTE_USER_HOTKEYS.
*
* First try to get the name using the pressed key string, then use
* the legacy event.
*
* Returns null if no match found.
*/
function get_hotkey_name(event) {
const s = get_pressed_keys_as_string(event);
if (s in LUTE_USER_HOTKEYS)
return LUTE_USER_HOTKEYS[s];
const legacy_s = _legacy_pressed_key_string(event);
if (legacy_s in LUTE_USER_HOTKEYS)
return LUTE_USER_HOTKEYS[legacy_s];
// console.log(`No match for hotkey ${s} or legacy hotkey ${legacy_s}`);
return null;
}
69 changes: 34 additions & 35 deletions lute/static/js/lute.js
Original file line number Diff line number Diff line change
Expand Up @@ -893,54 +893,53 @@ function handle_keydown (e) {
return; // Nothing to do.
}

// User hotkeys are stored in LUTE_USER_SETTINGS
// hash in global space.
const k = LUTE_USER_SETTINGS; // shorthand varname.
const hotkey_name = get_hotkey_name(e);
if (hotkey_name == null)
return;

const next_incr = _lang_is_left_to_right() ? 1 : -1;
const prev_incr = -1 * next_incr;

// Map of shortcuts to lambdas:
let map = {
[k.hotkey_StartHover]: () => start_hover_mode(),
[k.hotkey_PrevWord]: () => _move_cursor('span.word', prev_incr),
[k.hotkey_NextWord]: () => _move_cursor('span.word', next_incr),
[k.hotkey_PrevUnknownWord]: () => _move_cursor('span.word.status0', prev_incr),
[k.hotkey_NextUnknownWord]: () => _move_cursor('span.word.status0', next_incr),
[k.hotkey_PrevSentence]: () => _move_cursor('span.word.sentencestart', prev_incr),
[k.hotkey_NextSentence]: () => _move_cursor('span.word.sentencestart', next_incr),
[k.hotkey_StatusUp]: () => increment_status_for_selected_elements(+1),
[k.hotkey_StatusDown]: () => increment_status_for_selected_elements(-1),
[k.hotkey_Bookmark]: () => handle_bookmark(),
[k.hotkey_CopySentence]: () => handle_copy('sentence-id'),
[k.hotkey_CopyPara]: () => handle_copy('paragraph-id'),
[k.hotkey_CopyPage]: () => handle_copy(null),
[k.hotkey_EditPage]: () => handle_edit_page(),
[k.hotkey_TranslateSentence]: () => handle_translate('sentence-id'),
[k.hotkey_TranslatePara]: () => handle_translate('paragraph-id'),
[k.hotkey_TranslatePage]: () => handle_translate(null),
[k.hotkey_NextTheme]: () => next_theme(),
[k.hotkey_ToggleHighlight]: () => toggle_highlight(),
[k.hotkey_ToggleFocus]: () => toggleFocus(),
[k.hotkey_Status1]: () => update_status_for_marked_elements(1),
[k.hotkey_Status2]: () => update_status_for_marked_elements(2),
[k.hotkey_Status3]: () => update_status_for_marked_elements(3),
[k.hotkey_Status4]: () => update_status_for_marked_elements(4),
[k.hotkey_Status5]: () => update_status_for_marked_elements(5),
[k.hotkey_StatusIgnore]: () => update_status_for_marked_elements(98),
[k.hotkey_StatusWellKnown]: () => update_status_for_marked_elements(99),
[k.hotkey_DeleteTerm]: () => update_status_for_marked_elements(0),
"hotkey_StartHover": () => start_hover_mode(),
"hotkey_PrevWord": () => _move_cursor('span.word', prev_incr),
"hotkey_NextWord": () => _move_cursor('span.word', next_incr),
"hotkey_PrevUnknownWord": () => _move_cursor('span.word.status0', prev_incr),
"hotkey_NextUnknownWord": () => _move_cursor('span.word.status0', next_incr),
"hotkey_PrevSentence": () => _move_cursor('span.word.sentencestart', prev_incr),
"hotkey_NextSentence": () => _move_cursor('span.word.sentencestart', next_incr),
"hotkey_StatusUp": () => increment_status_for_selected_elements(+1),
"hotkey_StatusDown": () => increment_status_for_selected_elements(-1),
"hotkey_Bookmark": () => handle_bookmark(),
"hotkey_CopySentence": () => handle_copy('sentence-id'),
"hotkey_CopyPara": () => handle_copy('paragraph-id'),
"hotkey_CopyPage": () => handle_copy(null),
"hotkey_EditPage": () => handle_edit_page(),
"hotkey_TranslateSentence": () => handle_translate('sentence-id'),
"hotkey_TranslatePara": () => handle_translate('paragraph-id'),
"hotkey_TranslatePage": () => handle_translate(null),
"hotkey_NextTheme": () => next_theme(),
"hotkey_ToggleHighlight": () => toggle_highlight(),
"hotkey_ToggleFocus": () => toggleFocus(),
"hotkey_Status1": () => update_status_for_marked_elements(1),
"hotkey_Status2": () => update_status_for_marked_elements(2),
"hotkey_Status3": () => update_status_for_marked_elements(3),
"hotkey_Status4": () => update_status_for_marked_elements(4),
"hotkey_Status5": () => update_status_for_marked_elements(5),
"hotkey_StatusIgnore": () => update_status_for_marked_elements(98),
"hotkey_StatusWellKnown": () => update_status_for_marked_elements(99),
"hotkey_DeleteTerm": () => update_status_for_marked_elements(0),
}

const ks = get_pressed_keys_as_string(e);
if (ks in map) {
if (hotkey_name in map) {
// Override any existing event - e.g., if "up" arrow is in the map,
// don't scroll screen.
e.preventDefault();
map[ks]();
map[hotkey_name]();
}
else {
// console.log('unhandled key ' + ks);
// console.log(`hotkey "${hotkey_name}" not found in map`);
}
}

Expand Down
1 change: 1 addition & 0 deletions lute/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

<script type="text/javascript">
const LUTE_USER_SETTINGS = {{ user_settings | safe }}
const LUTE_USER_HOTKEYS = {{ user_hotkeys | safe }}
</script>
</head>
<body>
Expand Down
4 changes: 2 additions & 2 deletions lute/templates/read/term_bulk_edit_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lute-hotkey-utils.js') }}" charset="utf-8"></script>
<script type="text/javascript">
const LUTE_USER_SETTINGS = {{ user_settings | safe }}
const LUTE_USER_HOTKEYS = {{ user_hotkeys | safe }}
</script>

<div id="term-form-container">
Expand Down Expand Up @@ -47,8 +48,7 @@

// "Save" shortcut
$(document).keydown(function(event) {
const s = get_pressed_keys_as_string(event);
if (s == LUTE_USER_SETTINGS.hotkey_SaveTerm) {
if (get_hotkey_name(event) == "hotkey_SaveTerm") {
$("#btnsubmit").click();
}
});
Expand Down
3 changes: 3 additions & 0 deletions lute/templates/settings/shortcuts.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@

<script>

// Check any duplicates. All of the keyboard shortcuts are rendered
// into javascript as a dict, shortcut_string => shortcut_name, so
// each non-blank shortcut must be unique.
let _check_dups = function() {
let values = {};

Expand Down
4 changes: 2 additions & 2 deletions lute/templates/term/_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lute-hotkey-utils.js') }}" charset="utf-8"></script>
<script type="text/javascript">
const LUTE_USER_SETTINGS = {{ user_settings | safe }}
const LUTE_USER_HOTKEYS = {{ user_hotkeys | safe }}
</script>


Expand Down Expand Up @@ -317,8 +318,7 @@

// "Save" shortcut
$(document).keydown(function(event) {
const s = get_pressed_keys_as_string(event);
if (s == LUTE_USER_SETTINGS.hotkey_SaveTerm) {
if (get_hotkey_name(event) == "hotkey_SaveTerm") {
$("#btnsubmit").click();
}
});
Expand Down
17 changes: 16 additions & 1 deletion tests/acceptance/lute_test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,24 @@ def fill_reading_bulk_edit_form(self, updates=None):

def press_hotkey(self, hotkey):
"Send a hotkey."
key_to_code_map = {
"1": "Digit1",
"2": "Digit2",
"3": "Digit3",
"4": "Digit4",
"5": "Digit5",
"arrowdown": "ArrowDown",
"arrowup": "ArrowUp",
"h": "KeyH",
"i": "KeyI",
"m": "KeyM",
"w": "KeyW",
}
if hotkey not in key_to_code_map:
raise RuntimeError(f"Missing {hotkey} in acceptance test map")
event_parts = [
"type: 'keydown'",
f"key: '{hotkey.lower()}'",
f"code: '{key_to_code_map[hotkey]}'",
]
if hotkey in ["C", "T"]:
event_parts.append("shiftKey: true")
Expand Down
15 changes: 14 additions & 1 deletion tests/unit/settings/test_current.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
"""

from lute.db import db
from lute.settings.current import refresh_global_settings, current_settings
from lute.settings.current import (
refresh_global_settings,
current_settings,
current_hotkeys,
)


def test_refresh_refreshes_current_settings(app_context):
Expand All @@ -12,3 +16,12 @@ def test_refresh_refreshes_current_settings(app_context):
del current_settings["backup_dir"]
refresh_global_settings(db.session)
assert "backup_dir" in current_settings, "loaded"


def test_hotkey_strings_mapped_to_name(app_context):
"Hotkey key combo to name."
refresh_global_settings(db.session)
hotkey_names = current_hotkeys.values()
assert "hotkey_Status5" in hotkey_names, "this is set by default"
assert current_hotkeys["Digit5"] == "hotkey_Status5", "mapped"
assert "" not in current_hotkeys, "No blank keyboard shortcuts"

0 comments on commit 51017c1

Please sign in to comment.