feat(gui): Add accelerator keys
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user