From bfd42b330f3f313e06d22657253a37d83056323d Mon Sep 17 00:00:00 2001 From: Interitio Date: Tue, 10 Feb 2026 02:30:52 +1000 Subject: [PATCH] feat(gui): Add accelerator keys --- src/beanify/gui/mainwindow.py | 28 ++++++++++++ src/beanify/gui/roweditor.py | 56 ++++++++++++++++++++++++ src/beanify/gui/rowtree.py | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/src/beanify/gui/mainwindow.py b/src/beanify/gui/mainwindow.py index 18a9cf8..9c2a0b2 100644 --- a/src/beanify/gui/mainwindow.py +++ b/src/beanify/gui/mainwindow.py @@ -44,6 +44,7 @@ class MainWindow(ThemedTk): self.load_styles() self.setup_menu() self.setup_window() + self.setup_keys() self.initial_ingest() def load_styles(self): @@ -83,6 +84,33 @@ class MainWindow(ThemedTk): 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("", lambda event: self.rowtree.grab_focus()) + self.bind_all("", lambda event: self.rowtree.focus_prev()) + self.bind_all("", lambda event: self.rowtree.focus_next()) + self.bind_all("", lambda event: self.rowtree.focus_prev_partial()) + self.bind_all( + "", lambda event: self.rowtree.focus_next_partial() + ) + + self.bind_all( + "", lambda event: self.editor.focus_record_frame() + ) + self.bind_all("", lambda event: self.editor.focus_txn_frame()) + self.bind_all("", lambda event: self.editor.toggle_matching()) + self.bind_all( + "", lambda event: self.editor.toggle_extended_matching() + ) + self.bind_all("", lambda event: self.editor.do_save_txn()) + self.bind_all("", lambda event: self.editor.do_apply()) + def setup_window(self): style = ttk.Style(self) # style.configure( diff --git a/src/beanify/gui/roweditor.py b/src/beanify/gui/roweditor.py index edf88a8..df7c397 100644 --- a/src/beanify/gui/roweditor.py +++ b/src/beanify/gui/roweditor.py @@ -32,6 +32,9 @@ class RowEditor(ttk.Frame): self.txn_vars: dict[str, StringVar] = {} self.txn_rows: dict[str, tuple[ttk.Checkbutton, ttk.Label, ttk.Entry]] = {} + self.last_recordfield = None + self.last_txnfield = None + self.layout() @property @@ -105,6 +108,49 @@ class RowEditor(ttk.Frame): # 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]): # Flow and fill recordbox for the given records if records and not self.showing_record: @@ -163,6 +209,7 @@ class RowEditor(ttk.Frame): # Clear the variables and labels self.record_vars.clear() self.record_matchvars.clear() + self.last_recordfield = None for rowlabels in self.record_rows: for label in rowlabels: if label: @@ -214,6 +261,9 @@ class RowEditor(ttk.Frame): textvariable=var, ) entrybox.grid(row=i, column=2, sticky="ew", padx=5) + entrybox.bind( + "", lambda event: self.set_last_recordfield(event.widget) + ) matchbox = ttk.Checkbutton( self.record_frame, @@ -389,6 +439,8 @@ class RowEditor(ttk.Frame): # field -> boolvar txnmatching = {name: BooleanVar() for name in fieldnames} + self.last_txnfield = None + # field -> (match box, key label, value entrybox) txnrows = {} for name, dname in fieldnames.items(): @@ -398,6 +450,10 @@ class RowEditor(ttk.Frame): entrybox = self._make_account_entrybox(name, txnvars[name]) else: entrybox = ttk.Entry(self.txn_frame, textvariable=txnvars[name]) + + entrybox.bind( + "", 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!: Key shortcuts for prev/next record, and hints on the buttons # TODO!: Radio Buttons on the flag diff --git a/src/beanify/gui/rowtree.py b/src/beanify/gui/rowtree.py index 04aa6e8..9ff135c 100644 --- a/src/beanify/gui/rowtree.py +++ b/src/beanify/gui/rowtree.py @@ -201,6 +201,88 @@ class RowTree(ttk.Frame): self.tree.heading(col, text=dname, sort_key=sort_key) 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): self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1)