Skip to content

Commit b901191

Browse files
committed
Issue 141: make shortcuts customizable.
1 parent 7f14906 commit b901191

File tree

13 files changed

+327
-162
lines changed

13 files changed

+327
-162
lines changed

lute/app_factory.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,6 @@ def show_version():
207207
is_docker=ac.is_docker,
208208
)
209209

210-
@app.route("/hotkeys")
211-
def show_hotkeys():
212-
return render_template("hotkeys.html")
213-
214210
@app.route("/info")
215211
def show_info():
216212
"""

lute/dev_api/routes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ def load_demo_languages():
5959
lute.db.demo.load_demo_languages()
6060
langs = db.session.query(Language).all()
6161
for lang in langs:
62-
lang.dictionaries[0].dicturi = f"/dev_api/dummy_dict/{lang.name}/###"
62+
d = lang.dictionaries[0]
63+
d.dicturi = f"/dev_api/dummy_dict/{lang.name}/###"
64+
d.dicttype = "embeddedhtml" # Ensure not pop-up
6365
db.session.add(lang)
6466
db.session.commit()
6567
return redirect("/", 302)

lute/models/setting.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,27 @@ def load():
163163
"open_popup_in_new_tab": False,
164164
"stop_audio_on_term_form_open": True,
165165
"stats_calc_sample_size": 5,
166+
# Keyboard shortcuts:
167+
"hotkey_StartHover": "escape",
168+
"hotkey_PrevWord": "arrowleft",
169+
"hotkey_NextWord": "arrowright",
170+
"hotkey_StatusUp": "arrowup",
171+
"hotkey_StatusDown": "arrowdown",
172+
"hotkey_Bookmark": "b",
173+
"hotkey_CopySentence": "c",
174+
"hotkey_CopyPara": "shift+c",
175+
"hotkey_TranslateSentence": "t",
176+
"hotkey_TranslatePara": "shift+t",
177+
"hotkey_NextTheme": "m",
178+
"hotkey_ToggleHighlight": "h",
179+
"hotkey_ToggleFocus": "f",
180+
"hotkey_Status1": "1",
181+
"hotkey_Status2": "2",
182+
"hotkey_Status3": "3",
183+
"hotkey_Status4": "4",
184+
"hotkey_Status5": "5",
185+
"hotkey_StatusIgnore": "i",
186+
"hotkey_StatusWellKnown": "w",
166187
}
167188
for k, v in keys_and_defaults.items():
168189
if not UserSetting.key_exists(k):

lute/settings/routes.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,101 @@ def set_key_value(key, value):
163163
result = {"result": "failure", "message": message}
164164
db.session.commit()
165165
return jsonify(result)
166+
167+
168+
class UserShortcutsForm(FlaskForm):
169+
"""
170+
Shortcuts form.
171+
172+
The route manages getting and storing the settings
173+
from the db, as there's a variable number of settings,
174+
and it's easier to just work with the data directly
175+
rather than trying to create a variable number of fields.
176+
177+
I'm only using this form to get the validate_on_submit()!
178+
There's likely a better way to do this.
179+
"""
180+
181+
182+
def _get_categorized_hotkeys():
183+
"""
184+
Return hotkey UserSetting keys and values,
185+
grouped by category.
186+
"""
187+
188+
categorized_settings = {
189+
"Navigation": ["hotkey_StartHover", "hotkey_PrevWord", "hotkey_NextWord"],
190+
"Update status": [
191+
"hotkey_Status1",
192+
"hotkey_Status2",
193+
"hotkey_Status3",
194+
"hotkey_Status4",
195+
"hotkey_Status5",
196+
"hotkey_StatusIgnore",
197+
"hotkey_StatusWellKnown",
198+
"hotkey_StatusUp",
199+
"hotkey_StatusDown",
200+
],
201+
"Misc": [
202+
"hotkey_Bookmark",
203+
"hotkey_CopySentence",
204+
"hotkey_CopyPara",
205+
"hotkey_TranslateSentence",
206+
"hotkey_TranslatePara",
207+
"hotkey_NextTheme",
208+
"hotkey_ToggleHighlight",
209+
"hotkey_ToggleFocus",
210+
],
211+
}
212+
213+
settings = {h.key: h.value for h in db.session.query(UserSetting).all()}
214+
return {
215+
category: {k: settings[k] for k in keylist}
216+
for category, keylist in categorized_settings.items()
217+
}
218+
219+
220+
@bp.route("/shortcuts", methods=["GET", "POST"])
221+
def edit_shortcuts():
222+
"Edit shortcuts."
223+
form = UserShortcutsForm()
224+
if form.validate_on_submit():
225+
# print(request.form, flush=True)
226+
# Update the settings in the database
227+
for k, v in request.form.items():
228+
# print(f"{k} = {v}", flush=True)
229+
UserSetting.set_value(k, v)
230+
db.session.commit()
231+
flash("Shortcuts updated", "success")
232+
return redirect("/")
233+
234+
categorized_settings = _get_categorized_hotkeys()
235+
236+
setting_descs = {
237+
"hotkey_StartHover": "Deselect all words",
238+
"hotkey_PrevWord": "Move to previous word",
239+
"hotkey_NextWord": "Move to next word",
240+
"hotkey_StatusUp": "Bump the status up by 1",
241+
"hotkey_StatusDown": "Bump that status down by 1",
242+
"hotkey_Bookmark": "Bookmark the current page",
243+
"hotkey_CopySentence": "Copy the sentence of the current word",
244+
"hotkey_CopyPara": "Copy the paragraph of the current word",
245+
"hotkey_TranslateSentence": "Translate the sentence of the current word",
246+
"hotkey_TranslatePara": "Translate the paragraph of the current word",
247+
"hotkey_NextTheme": "Change to the next theme",
248+
"hotkey_ToggleHighlight": "Toggle highlights",
249+
"hotkey_ToggleFocus": "Toggle focus mode",
250+
"hotkey_Status1": "Set status to 1",
251+
"hotkey_Status2": "Set status to 2",
252+
"hotkey_Status3": "Set status to 3",
253+
"hotkey_Status4": "Set status to 4",
254+
"hotkey_Status5": "Set status to 5",
255+
"hotkey_StatusIgnore": "Set status to Ignore",
256+
"hotkey_StatusWellKnown": "Set status to Well Known",
257+
}
258+
259+
return render_template(
260+
"settings/shortcuts.html",
261+
setting_descs=setting_descs,
262+
categorized_settings=categorized_settings,
263+
)

lute/static/css/styles.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,23 @@ table input:not([type="checkbox"]) {
12091209
width: 20rem;
12101210
}
12111211

1212+
#shortcutstable th {
1213+
text-align: left;
1214+
}
1215+
1216+
#shortcutstable .settingcategory {
1217+
font-size: 1.2em;
1218+
font-weight: bold;
1219+
}
1220+
1221+
#shortcutstable input[type=text] {
1222+
max-width: 8rem;
1223+
}
1224+
1225+
.dupShortcut {
1226+
background-color: #FFCCCB;
1227+
}
1228+
12121229
#book td:first-child,
12131230
#language td:first-child {
12141231
width: 15rem;

lute/static/js/lute-hotkey-utils.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Get the pressed keys as a string, eg 'meta-c', 'shift-a'.
3+
*
4+
* Note that there _must_ be a "regular" key pressed as well.
5+
* If only meta/alt/ctl/shift are pressed, returns null.
6+
*/
7+
function get_pressed_keys_as_string(event) {
8+
const keys = [];
9+
10+
// Check for modifier keys
11+
if (event.ctrlKey) keys.push('ctrl');
12+
if (event.shiftKey) keys.push('shift');
13+
if (event.altKey) keys.push('alt');
14+
if (event.metaKey) keys.push('meta');
15+
16+
// Map special keys to names if needed
17+
const keyMap = {
18+
' ': 'space'
19+
};
20+
21+
if (event.key == null) {
22+
// window.alert("no key for event?");
23+
return null;
24+
}
25+
26+
const actual_key = keyMap[event.key] || event.key.toLowerCase();
27+
if (['shift', 'ctrl', 'alt', 'meta'].includes(actual_key))
28+
return null
29+
30+
keys.push(actual_key);
31+
const ret = keys.join('+');
32+
// window.alert(`got hotkey = ${ret}`);
33+
return ret;
34+
}

lute/static/js/lute.js

Lines changed: 26 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -775,33 +775,6 @@ function add_page_after() {
775775
}
776776

777777

778-
function getKeyString(event) {
779-
const keys = [];
780-
781-
// Check for modifier keys
782-
if (event.ctrlKey) keys.push('ctrl');
783-
if (event.shiftKey) keys.push('shift');
784-
if (event.altKey) keys.push('alt');
785-
if (event.metaKey) keys.push('meta');
786-
787-
// Map special keys to names if needed
788-
const keyMap = {
789-
' ': 'space'
790-
};
791-
792-
const key = keyMap[event.key] || event.key.toLowerCase();
793-
794-
// If it's a normal key (not a modifier), add it to the keys array
795-
if (!['shift', 'ctrl', 'alt', 'meta'].includes(key)) {
796-
keys.push(key);
797-
}
798-
799-
const ret = keys.join('+');
800-
console.log(`Got keydown = ${ret}`);
801-
return ret;
802-
}
803-
804-
805778
function get_right_increment() {
806779
// read/index.js has some data rendered at the top of the page.
807780
const lang_is_rtl = $('#lang_is_rtl');
@@ -819,55 +792,35 @@ function handle_keydown (e) {
819792
return; // Nothing to do.
820793
}
821794

822-
// User hotkeys, to be read from UserSettings.
823-
const user_keys = {
824-
'HotkeyStartHover': 'escape',
825-
'HotkeyPrevWord': 'arrowleft',
826-
'HotkeyNextWord': 'arrowright',
827-
'HotkeyStatusUp': 'arrowup',
828-
'HotkeyStatusDown': 'arrowdown',
829-
'HotkeyBookmark': 'b',
830-
'HotkeyCopySentence': 'c',
831-
'HotkeyCopyPara': 'shift+c',
832-
'HotkeyTranslateSentence': 't',
833-
'HotkeyTranslatePara': 'shift+t',
834-
'HotkeyNextTheme': 'm',
835-
'HotkeyToggleHighlight': 'h',
836-
'HotkeyToggleFocus': 'f',
837-
'HotkeyStatus1': '1',
838-
'HotkeyStatus2': '2',
839-
'HotkeyStatus3': '3',
840-
'HotkeyStatus4': '4',
841-
'HotkeyStatus5': '5',
842-
'HotkeyStatusIgnore': 'i',
843-
'HotkeyStatusWellKnown': 'w',
844-
};
795+
// User hotkeys are stored in LUTE_USER_SETTINGS
796+
// hash in global space.
797+
const k = LUTE_USER_SETTINGS; // shorthand varname.
845798

846799
// Map of shortcuts to lambdas:
847800
let map = {
848-
[user_keys['HotkeyStartHover']]: () => start_hover_mode(),
849-
[user_keys['HotkeyPrevWord']]: () => move_cursor(-1 * get_right_increment()),
850-
[user_keys['HotkeyNextWord']]: () => move_cursor(get_right_increment()),
851-
[user_keys['HotkeyStatusUp']]: () => increment_status_for_selected_elements(+1),
852-
[user_keys['HotkeyStatusDown']]: () => increment_status_for_selected_elements(-1),
853-
[user_keys['HotkeyBookmark']]: () => handle_bookmark(),
854-
[user_keys['HotkeyCopySentence']]: () => handle_copy('sentence-id'),
855-
[user_keys['HotkeyCopyPara']]: () => handle_copy('paragraph-id'),
856-
[user_keys['HotkeyTranslateSentence']]: () => handle_translate('sentence-id'),
857-
[user_keys['HotkeyTranslatePara']]: () => handle_translate('paragraph-id'),
858-
[user_keys['HotkeyNextTheme']]: () => next_theme(),
859-
[user_keys['HotkeyToggleHighlight']]: () => toggle_highlight(),
860-
[user_keys['HotkeyToggleFocus']]: () => toggleFocus(),
861-
[user_keys['HotkeyStatus1']]: () => update_status_for_marked_elements(1),
862-
[user_keys['HotkeyStatus2']]: () => update_status_for_marked_elements(2),
863-
[user_keys['HotkeyStatus3']]: () => update_status_for_marked_elements(3),
864-
[user_keys['HotkeyStatus4']]: () => update_status_for_marked_elements(4),
865-
[user_keys['HotkeyStatus5']]: () => update_status_for_marked_elements(5),
866-
[user_keys['HotkeyStatusIgnore']]: () => update_status_for_marked_elements(98),
867-
[user_keys['HotkeyStatusWellKnown']]: () => update_status_for_marked_elements(99),
868-
}
869-
870-
const ks = getKeyString(e);
801+
[k.hotkey_StartHover]: () => start_hover_mode(),
802+
[k.hotkey_PrevWord]: () => move_cursor(-1 * get_right_increment()),
803+
[k.hotkey_NextWord]: () => move_cursor(get_right_increment()),
804+
[k.hotkey_StatusUp]: () => increment_status_for_selected_elements(+1),
805+
[k.hotkey_StatusDown]: () => increment_status_for_selected_elements(-1),
806+
[k.hotkey_Bookmark]: () => handle_bookmark(),
807+
[k.hotkey_CopySentence]: () => handle_copy('sentence-id'),
808+
[k.hotkey_CopyPara]: () => handle_copy('paragraph-id'),
809+
[k.hotkey_TranslateSentence]: () => handle_translate('sentence-id'),
810+
[k.hotkey_TranslatePara]: () => handle_translate('paragraph-id'),
811+
[k.hotkey_NextTheme]: () => next_theme(),
812+
[k.hotkey_ToggleHighlight]: () => toggle_highlight(),
813+
[k.hotkey_ToggleFocus]: () => toggleFocus(),
814+
[k.hotkey_Status1]: () => update_status_for_marked_elements(1),
815+
[k.hotkey_Status2]: () => update_status_for_marked_elements(2),
816+
[k.hotkey_Status3]: () => update_status_for_marked_elements(3),
817+
[k.hotkey_Status4]: () => update_status_for_marked_elements(4),
818+
[k.hotkey_Status5]: () => update_status_for_marked_elements(5),
819+
[k.hotkey_StatusIgnore]: () => update_status_for_marked_elements(98),
820+
[k.hotkey_StatusWellKnown]: () => update_status_for_marked_elements(99),
821+
}
822+
823+
const ks = get_pressed_keys_as_string(e);
871824
if (ks in map) {
872825
// Override any existing event - e.g., if "up" arrow is in the map,
873826
// don't scroll screen.

lute/templates/base.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
<script type="text/javascript" src="{{ url_for('static', filename='vendor/datatables/datatables.button.download.js') }}" charset="utf-8"></script>
3939
<script type="text/javascript" src="{{ url_for('static', filename='vendor/jquery/jquery.hoverIntent.js') }}" charset="utf-8"></script>
4040

41-
<!-- this calls a route defined in app_factory to never cache this file -->
41+
<!-- the "never_cache" calls a route defined in app_factory to never cache that particular .js file -->
42+
<script type="text/javascript" src="/static/js/never_cache/lute-hotkey-utils.js" charset="utf-8"></script>
4243
<script type="text/javascript" src="/static/js/never_cache/lute.js" charset="utf-8"></script>
4344

4445
<script type="text/javascript">
@@ -98,6 +99,7 @@ <h1 id="luteTitle" title="Learning Using Texts">
9899
<ul class="sub-menu">
99100
<li><a id="lang_index" href="/language/index">Languages</a></li>
100101
<li><a href="/settings/index">Settings</a></li>
102+
<li><a href="/settings/shortcuts">Keyboard shortcuts</a></li>
101103
</ul>
102104
</div>
103105
{% if backup_enabled and backup_directory != '' %}

lute/templates/hotkeys.html

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)