feat(gui): Add accelerator keys

This commit is contained in:
2026-02-10 02:30:52 +10:00
parent f0d6a76b77
commit bfd42b330f
3 changed files with 166 additions and 0 deletions

View File

@@ -44,6 +44,7 @@ class MainWindow(ThemedTk):
self.load_styles() self.load_styles()
self.setup_menu() self.setup_menu()
self.setup_window() self.setup_window()
self.setup_keys()
self.initial_ingest() self.initial_ingest()
def load_styles(self): def load_styles(self):
@@ -83,6 +84,33 @@ class MainWindow(ThemedTk):
self.menubar.add_cascade(menu=menu_edit, label="Edit") self.menubar.add_cascade(menu=menu_edit, label="Edit")
def setup_keys(self):
# Alt+1, 2, 3 for selecting different regions
# Alt+Enter to create or save
# Alt+R to toggle rule creation mode
# Alt+Shift+R to toggle extended rule creation
# Alt+S to save transaction
# Alt+up/down for next entry in that direction
# Alt+shift+up/down for next incomplete entry in that direction
self.bind_all("<Alt-KeyPress-1>", lambda event: self.rowtree.grab_focus())
self.bind_all("<Alt-Up>", lambda event: self.rowtree.focus_prev())
self.bind_all("<Alt-Down>", lambda event: self.rowtree.focus_next())
self.bind_all("<Alt-Shift-Up>", lambda event: self.rowtree.focus_prev_partial())
self.bind_all(
"<Alt-Shift-Down>", lambda event: self.rowtree.focus_next_partial()
)
self.bind_all(
"<Alt-KeyPress-2>", lambda event: self.editor.focus_record_frame()
)
self.bind_all("<Alt-KeyPress-3>", lambda event: self.editor.focus_txn_frame())
self.bind_all("<Control-r>", lambda event: self.editor.toggle_matching())
self.bind_all(
"<Control-Shift-R>", lambda event: self.editor.toggle_extended_matching()
)
self.bind_all("<Alt-s>", lambda event: self.editor.do_save_txn())
self.bind_all("<Alt-Return>", lambda event: self.editor.do_apply())
def setup_window(self): def setup_window(self):
style = ttk.Style(self) style = ttk.Style(self)
# style.configure( # style.configure(

View File

@@ -32,6 +32,9 @@ class RowEditor(ttk.Frame):
self.txn_vars: dict[str, StringVar] = {} self.txn_vars: dict[str, StringVar] = {}
self.txn_rows: dict[str, tuple[ttk.Checkbutton, ttk.Label, ttk.Entry]] = {} self.txn_rows: dict[str, tuple[ttk.Checkbutton, ttk.Label, ttk.Entry]] = {}
self.last_recordfield = None
self.last_txnfield = None
self.layout() self.layout()
@property @property
@@ -105,6 +108,49 @@ class RowEditor(ttk.Frame):
# Buttons below # Buttons below
def focus_record_frame(self):
if self.showing_record and self.match_var.get():
to_select = self.last_recordfield
if to_select is None:
to_select = next(
(entry for _, _, _, entry in self.record_rows if entry), None
)
if to_select is not None:
to_select.focus_set()
def focus_txn_frame(self):
if self.showing_txn:
to_select = self.last_txnfield
if to_select is None:
to_select = next(
(entry for _, _, entry in self.txn_rows.values() if entry)
)
to_select.focus_set()
def toggle_matching(self):
self.match_var.set(not self.match_var.get())
self._display_matchboxes()
def toggle_extended_matching(self):
if not self.match_var.get() or not self.extended_var.get():
self.match_var.set(True)
self.extended_var.set(True)
else:
self.extended_var.set(False)
self._display_matchboxes()
def do_apply(self):
if self.match_var.get():
self.do_create_rule()
elif self.showing_txn:
self.do_save_txn()
def set_last_recordfield(self, widget):
self.last_recordfield = widget
def set_last_txnfield(self, widget):
self.last_txnfield = widget
def _display_records(self, records: list[Record]): def _display_records(self, records: list[Record]):
# Flow and fill recordbox for the given records # Flow and fill recordbox for the given records
if records and not self.showing_record: if records and not self.showing_record:
@@ -163,6 +209,7 @@ class RowEditor(ttk.Frame):
# Clear the variables and labels # Clear the variables and labels
self.record_vars.clear() self.record_vars.clear()
self.record_matchvars.clear() self.record_matchvars.clear()
self.last_recordfield = None
for rowlabels in self.record_rows: for rowlabels in self.record_rows:
for label in rowlabels: for label in rowlabels:
if label: if label:
@@ -214,6 +261,9 @@ class RowEditor(ttk.Frame):
textvariable=var, textvariable=var,
) )
entrybox.grid(row=i, column=2, sticky="ew", padx=5) entrybox.grid(row=i, column=2, sticky="ew", padx=5)
entrybox.bind(
"<FocusIn>", lambda event: self.set_last_recordfield(event.widget)
)
matchbox = ttk.Checkbutton( matchbox = ttk.Checkbutton(
self.record_frame, self.record_frame,
@@ -389,6 +439,8 @@ class RowEditor(ttk.Frame):
# field -> boolvar # field -> boolvar
txnmatching = {name: BooleanVar() for name in fieldnames} txnmatching = {name: BooleanVar() for name in fieldnames}
self.last_txnfield = None
# field -> (match box, key label, value entrybox) # field -> (match box, key label, value entrybox)
txnrows = {} txnrows = {}
for name, dname in fieldnames.items(): for name, dname in fieldnames.items():
@@ -398,6 +450,10 @@ class RowEditor(ttk.Frame):
entrybox = self._make_account_entrybox(name, txnvars[name]) entrybox = self._make_account_entrybox(name, txnvars[name])
else: else:
entrybox = ttk.Entry(self.txn_frame, textvariable=txnvars[name]) entrybox = ttk.Entry(self.txn_frame, textvariable=txnvars[name])
entrybox.bind(
"<FocusIn>", lambda event: self.set_last_txnfield(event.widget)
)
# TODO!: If we have a list of accounts, we could use a ComboBox here instead? For autocompletion. # TODO!: If we have a list of accounts, we could use a ComboBox here instead? For autocompletion.
# TODO!: Key shortcuts for prev/next record, and hints on the buttons # TODO!: Key shortcuts for prev/next record, and hints on the buttons
# TODO!: Radio Buttons on the flag # TODO!: Radio Buttons on the flag

View File

@@ -201,6 +201,88 @@ class RowTree(ttk.Frame):
self.tree.heading(col, text=dname, sort_key=sort_key) self.tree.heading(col, text=dname, sort_key=sort_key)
self.tree.set_columns(initially_enabled) self.tree.set_columns(initially_enabled)
def grab_focus(self):
selected = self.tree.selection()
if selected:
to_focus = selected[0]
elif children := self.tree.get_children():
to_focus = children[0]
else:
# Nothing to focus on
return
logger.debug(f"RowTree focusing on {to_focus}")
self.tree.focus(to_focus)
self.tree.focus_set()
self.tree.selection_set(to_focus)
def focus_next(self):
selected = self.tree.selection()
if selected:
current = selected[0]
elif children := self.tree.get_children():
current = children[0]
else:
# Nothing to focus on
return
nextitem = self.tree.next(current)
if not nextitem:
nextitem = self.tree.get_children()[0]
self.tree.focus(nextitem)
self.tree.selection_set(nextitem)
def focus_next_partial(self):
selected = self.tree.selection()
if selected:
current = selected[0]
elif children := self.tree.get_children():
current = children[0]
else:
# Nothing to focus on
return
nextitem = self.tree.next(current)
if not nextitem:
nextitem = self.tree.get_children()[0]
while nextitem != current and not self.items[nextitem][1].partial:
nextitem = self.tree.next(nextitem)
if not nextitem:
nextitem = self.tree.get_children()[0]
self.tree.focus(nextitem)
self.tree.selection_set(nextitem)
def focus_prev(self):
selected = self.tree.selection()
if selected:
current = selected[0]
elif children := self.tree.get_children():
current = children[0]
else:
# Nothing to focus on
return
previtem = self.tree.prev(current)
if not previtem:
previtem = self.tree.get_children()[-1]
self.tree.focus(previtem)
self.tree.selection_set(previtem)
def focus_prev_partial(self):
selected = self.tree.selection()
if selected:
current = selected[0]
elif children := self.tree.get_children():
current = children[0]
else:
# Nothing to focus on
return
previtem = self.tree.prev(current)
if not previtem:
previtem = self.tree.get_children()[-1]
while previtem != current and not self.items[previtem][1].partial:
previtem = self.tree.prev(previtem)
if not previtem:
previtem = self.tree.get_children()[-1]
self.tree.focus(previtem)
self.tree.selection_set(previtem)
def layout(self): def layout(self):
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)