瀏覽代碼

Separate Suggestions overlay and main Transaction editor into separate widgets

Daniel Sheffield 3 年之前
父節點
當前提交
3922589f7c
共有 2 個文件被更改,包括 181 次插入137 次删除
  1. 52 9
      grocery_transactions.py
  2. 129 128
      widgets.py

+ 52 - 9
grocery_transactions.py

@@ -15,7 +15,10 @@ from widgets import (
     NoTabCheckBox,
     AutoCompleteEdit,
     AutoCompleteFloatEdit,
-    GroceryTransactionEditor, 
+    TransactionEditor,
+    SuggestionPopup,
+    GroceryTransactionEditor,
+    ActivityManager,
     _set_focus_path,
 )
 from decimal import Decimal
@@ -157,6 +160,46 @@ def interleave(_list, div):
         yield element
         yield div
 
+def _apply_choice_callback(activity_manager, name, widget, value):
+    txn = activity_manager.get('transaction')
+    txn.apply_choice(name)(widget, value)
+    activity_manager.show(txn.update())
+
+def _show_suggestions_callback(activity_manager, name, options):
+    txn = activity_manager.get('transaction')
+    activity_manager.show(
+        activity_manager.create(SuggestionPopup, None,
+            txn.original_widget, name, options,
+            lambda w,x: _apply_choice_callback(activity_manager, name, w, x),
+            lambda: activity_manager.show(txn.update())
+        ))
+
+def _save_and_clear_cb(activity_manager):
+    txn = activity_manager.get('transaction')
+    activity_manager.app.save(txn.data)
+    txn.clear()
+    activity_manager.show(txn.update())
+    
+
+class QueryManager(object):
+    
+    def __init__(self, activity_manager):
+        self.activity_manager = activity_manager
+    
+    def get_session_transactions(self, date, store):
+        return get_session_transactions(
+            cur, get_session_transactions_statement(
+                parse_time(date), store, full_name=True, exact_time=True
+            ), date, store)
+    
+    def autocomplete(self, name, data):
+        options = self.unique_suggestions(name, **data)
+        if len(options) > 0:
+            _show_suggestions_callback(self.activity_manager, name, options)
+
+    def unique_suggestions(self, name, **kwargs):
+        return unique_suggestions(cur, get_transactions_statement(), name, **kwargs)
+
 #screen = urwid.raw_display.Screen()
 #screen.set_terminal_properties(colors=256, has_underline=True)
 #screen.register_palette(palette)
@@ -166,14 +209,14 @@ log = args[1]
 
 cur.execute("BEGIN")
 
-app = GroceryTransactionEditor(cur,
-    lambda name, **kwargs: unique_suggestions(cur, get_transactions_statement(), name, **kwargs),
-    lambda date, store, *x, **y: get_session_transactions(
-        cur, get_session_transactions_statement(
-            parse_time(date), store, full_name=True, exact_time=True
-        ), date, store),
-    log, cols, grid_layout,
-    side_pane, bottom_pane)
+activity_manager = ActivityManager()
+query_manager = QueryManager(activity_manager)
+
+activity_manager.create(TransactionEditor, 'transaction',
+    query_manager, cols, grid_layout, side_pane, bottom_pane,
+    lambda: _save_and_clear_cb(activity_manager))
+
+app = GroceryTransactionEditor(activity_manager, cur, log)
 
 loop = urwid.MainLoop(app, palette, unhandled_input=show_or_exit)
 loop.run()

+ 129 - 128
widgets.py

@@ -24,7 +24,7 @@ class AutoCompleteEdit(urwid.Edit):
             title = name.title()
             passthrough = u'  ' if name.lower() == 'unit' else u''
             
-        super(AutoCompleteEdit, self).__init__(passthrough, *args, **kwargs)
+        super().__init__(passthrough, *args, **kwargs)
         self.apply = apply_change_func
 
     def keypress(self, size, key):
@@ -35,7 +35,7 @@ class AutoCompleteEdit(urwid.Edit):
             self.set_edit_text('')
             return
         
-        return super(AutoCompleteEdit, self).keypress(size, key)
+        return super().keypress(size, key)
 
 
 class AutoCompleteFloatEdit(numedit.FloatEdit):
@@ -114,14 +114,14 @@ class AutoCompleteFloatEdit(numedit.FloatEdit):
             self.update_caption()
             return
 
-        return super(AutoCompleteFloatEdit, self).keypress(size, key)
+        return super().keypress(size, key)
 
 class NoTabCheckBox(urwid.CheckBox):
     def keypress(self, size, key):
         if not isinstance(key, tuple) and key == 'tab':
             return 
         else:
-            return super(NoTabCheckBox, self).keypress(size, key)
+            return super().keypress(size, key)
 
 def _set_focus_path(container, path):
     try:
@@ -137,23 +137,12 @@ def _set_focus_path(container, path):
     raise IndexError
 
 class GroceryTransactionEditor(urwid.WidgetPlaceholder):
-    def __init__(self, cur, unique_suggestions, get_session_transactions,
-        log, fields, layout, side_pane, bottom_pane):
-        super(GroceryTransactionEditor, self).__init__(urwid.SolidFill(u'/'))
-        self.organic_checkbox = NoTabCheckBox(
-            u"Organic",
-            on_state_change=self.apply_organic_state
-        )
-        self._init_data(fields)
-        self.widgets = dict()
-        self.layout = layout
-        self.side_pane = side_pane
-        self.bottom_pane = bottom_pane
-        self.show('transaction')
+    def __init__(self, activity_manager, cur, log):
+        super().__init__(urwid.SolidFill(u'/'))
+        self.activity_manager = activity_manager
         self.cur = cur
-        self.unique_suggestions = unique_suggestions
-        self.get_session_transactions = get_session_transactions
-        
+        txn = self.activity_manager.get('transaction')
+
         with open(log, 'r') as f:
             date = None
             store = None
@@ -174,11 +163,46 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
                 self.cur.execute(line)
         
             if None not in (date, store):
-                self._apply_choice('ts', date)
-                self._apply_choice('store', store)
+                txn._apply_choice('ts', date)
+                txn._apply_choice('store', store)
+
+        self.activity_manager.show(self)
+        self.activity_manager.show(txn.update())
 
         self.log = self.open(log)
         
+    
+    def _open(self, log):
+        with open(log, 'a') as f:
+            yield f
+    
+    def open(self, log):
+        self._to_close = self._open(log)
+        return next(self._to_close)
+    
+    def close(self):
+        if self._to_close is not None:
+            self._to_close.close()
+            self._to_close = None
+        
+    def save(self, data):
+        ts = data['ts']
+        store = data['store']
+        description = data['description']
+        quantity = data['quantity']
+        unit = data['unit']
+        price = data['price']
+        product = data['product']
+        organic = data['organic'] if data['organic'] else 'false'
+        statement = \
+            f"CALL insert_transaction('{ts}', $store${store}$store$, " \
+            f"$descr${description}$descr$, {quantity}, $unit${unit}$unit$, " \
+            f"{price}, $produ${product}$produ$, {organic});\n"
+        self.log.write(statement)
+        self.cur.execute(statement)
+
+class TransactionEditor(urwid.WidgetPlaceholder):
+
     def iter_focus_paths(self):
         initial = [2,0,0,0]
         container = self.original_widget.original_widget.original_widget
@@ -227,21 +251,19 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         
         container.set_focus_path([2,0,0,0])
 
-        
     def keypress(self, size, key):
         if isinstance(key, tuple):
             return
         
         if getattr(self.original_widget.original_widget, 'original_widget', None) is None:
-            return super(GroceryTransactionEditor, self).keypress(size, key)
+            return super().keypress(size, key)
         
         if key == 'tab':
             self.advance_focus()
         elif key == 'shift tab':
             self.advance_focus(reverse=True)
         else:
-            return super(GroceryTransactionEditor, self).keypress(size, key)
-        
+            return super().keypress(size, key)
 
     def _init_data(self, fields):
         self.data = OrderedDict([
@@ -249,35 +271,14 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         ])
         self.clear()
 
-    def get_activity(self, name):
-        return getattr(self, name)
-
-    def show(self, name, *args, **kwargs):
-        if name in self.widgets:
-            self.original_widget = self.widgets[name]
-            self.update(name)
-            return
-        
-        a = self.get_activity(name)
-        widget = a(*args, **kwargs)
-        if name != 'suggestions':
-            self.widgets[name] = widget
-        
-        self.original_widget = widget
-
     def _apply_choice(self, name, value):
-        self.data.update({
-            name: value,
-        })
+        self._apply_changes(name, value)
         for k,v in self.data.items():
             if k == name or v:
                 continue
-            options = self.unique_suggestions(k, **self.data)
+            options = self.query_manager.unique_suggestions(k, **self.data)
             if len(options) == 1 and k != 'ts':
-                self.data.update({
-                    k: list(options)[0],
-                })
-        self.show('transaction')
+                self._apply_changes(k, list(options)[0])
     
     def apply_choice(self, name):
         return  lambda w,x: self._apply_choice(name, x)
@@ -286,73 +287,12 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         self.data.update({
             name: value,
         })
-
-    def _autocomplete(self, name):
-        options = self.unique_suggestions(name, **self.data)
-        if 0 < len(options):
-            self.show('suggestions', name, options=options)
     
-    def apply_organic_state(self, w, state):
-        self.data['organic'] = repr(state).lower()
-
     def apply_changes(self, name):
-        apply = lambda w,x: self._apply_changes(name, x)
-        return apply
-
-    def suggestions(self, name, options=None):
-        body = [urwid.Text(name.title()), urwid.Divider()]
-        for c in options:
-            button = urwid.Button(c)
-            urwid.connect_signal(button, 'click', self.apply_choice(name), c)
-            body.append(urwid.AttrMap(button, None, focus_map='reversed'))
-        walker = urwid.SimpleFocusListWalker(body, wrap_around=False)
-        listbox = urwid.ListBox(walker)
-        pad = urwid.Padding(listbox, left=2, right=2)
-        top = urwid.Overlay(pad, self.original_widget,
-            align='center', width=('relative', 60),
-            valign='middle', height=('relative', 60),
-            min_width=20, min_height=9)
-        def keypress(size, key, original=top.keypress):
-            if key == 'esc':
-                self.show('transaction')
-                return
-
-            if key == 'tab':
-                return
+        return lambda w,x: self._apply_changes(name, x)
 
-            return original(size, key)
-
-        top.keypress = keypress
-        return urwid.AttrMap(top, 'banner')
-    
-    def _open(self, log):
-        with open(log, 'a') as f:
-            yield f
-    
-    def open(self, log):
-        self._to_close = self._open(log)
-        return next(self._to_close)
-    
-    def close(self):
-        if self._to_close is not None:
-            self._to_close.close()
-            self._to_close = None
-        
-    def save(self):
-        ts = self.data['ts']
-        store = self.data['store']
-        description = self.data['description']
-        quantity = self.data['quantity']
-        unit = self.data['unit']
-        price = self.data['price']
-        product = self.data['product']
-        organic = self.data['organic'] if self.data['organic'] else 'false'
-        statement = \
-            f"CALL insert_transaction('{ts}', $store${store}$store$, " \
-            f"$descr${description}$descr$, {quantity}, $unit${unit}$unit$, " \
-            f"{price}, $produ${product}$produ$, {organic});\n"
-        self.log.write(statement)
-        self.cur.execute(statement)
+    def apply_organic_state(self, w, state):
+        self.data['organic'] = repr(state).lower()
 
     def clear(self):
         for k in self.data:
@@ -360,34 +300,39 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
                 continue
             self.data[k] = ''
         self.organic_checkbox.set_state(False)
-
-    def save_and_clear(self):
-        self.save()
-        self.clear()
-        self.show('transaction')
-
-    def update(self, name):
-        getattr(self, f"update_{name}")()
     
-    def update_transaction(self):
+    def update(self):
         for k in self.edit_fields:
             self.edit_fields[k].set_edit_text(self.data[k])
         date, store = self.data['ts'], self.data['store']
         self.text_fields['dbview'].set_text(
-            self.get_session_transactions(date, store) if None not in (
+            self.query_manager.get_session_transactions(date, store) if None not in (
                 date or None, store or None
             ) else ''
         )
         self.organic_checkbox.set_state(True if self.data['organic'] == 'true' else False)
+        return self
 
-    def transaction(self):
+    def __init__(self, query_manager, fields,
+        layout, side_pane, bottom_pane,
+        save_and_clear_cb):
+        super().__init__(urwid.SolidFill(u'/'))
+        self.organic_checkbox = NoTabCheckBox(
+            u"Organic",
+            on_state_change=self.apply_organic_state
+        )
+        self.query_manager = query_manager
+        self._init_data(fields)
+        self.layout = layout
+        self.side_pane = side_pane
+        self.bottom_pane = bottom_pane
         self.edit_fields = OrderedDict()
         self.text_fields = OrderedDict()
         for k in self.data:
             if k in self.side_pane and k != 'unit':
-                ef = AutoCompleteFloatEdit(('bg', k), apply_change_func=self._autocomplete)
+                ef = AutoCompleteFloatEdit(('bg', k), apply_change_func=lambda name: query_manager.autocomplete(name, self.data))
             elif k != 'organic':
-                ef = AutoCompleteEdit(('bg', k), apply_change_func=self._autocomplete)
+                ef = AutoCompleteEdit(('bg', k), apply_change_func=lambda name: query_manager.autocomplete(name, self.data))
             else:
                 continue
             ef.set_edit_text(self.data[k])
@@ -397,7 +342,7 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         header = urwid.Text(u'Fill Transaction', 'center')
         _copyright = urwid.Text(COPYRIGHT, 'center')
         done_button = urwid.Button(('streak', u'Done'))
-        urwid.connect_signal(done_button, 'click', lambda w: self.save_and_clear())
+        urwid.connect_signal(done_button, 'click', lambda w: save_and_clear_cb())
         banner = urwid.Pile([
             urwid.Padding(header, 'center', width=('relative', 100)),
             urwid.Padding(_copyright, 'center', width=('relative', 100)),
@@ -447,4 +392,60 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         ])
         widget = urwid.Filler(widget, 'top') 
         widget = urwid.AttrMap(widget, 'bg')
+        self.original_widget = widget
+
+class SuggestionPopup(urwid.Overlay):
+    
+    def __init__(self, under, name, options, apply_cb, esc_cb):
+        self.esc_cb = esc_cb
+        self.under = under
+        body = [urwid.Text(name.title()), urwid.Divider()]
+        for c in options:
+            button = urwid.Button(c)
+            urwid.connect_signal(button, 'click', apply_cb, c)
+            body.append(urwid.AttrMap(button, None, focus_map='reversed'))
+        walker = urwid.SimpleFocusListWalker(body, wrap_around=False)
+        listbox = urwid.ListBox(walker)
+        pad = urwid.Padding(listbox, left=2, right=2)
+        pad = urwid.AttrMap(pad, 'banner')
+        super().__init__(pad, under,
+            align='center', width=('relative', 60),
+            valign='middle', height=('relative', 60),
+            min_width=20, min_height=9)
+
+    def keypress(self, size, key):
+        if key == 'esc':
+            self.esc_cb()
+            return
+
+        if key == 'tab':
+            return
+
+        return super().keypress(size, key)
+
+class ActivityManager(object):
+
+    def __init__(self):
+        self.widgets = dict()
+        self.app = None
+    
+    def add(self, widget, name):
+        self.widgets[name] = widget
+        return widget
+    
+    def get(self, name):
+        if name in self.widgets:
+            return self.widgets[name]
+        raise Exception("Widget {name} not found")
+    
+    def create(self, cls, name, *args, **kwargs):
+        widget = cls(*args, **kwargs)
+        if name is not None:
+            self.add(widget, name)
         return widget
+    
+    def show(self, widget):
+        if self.app is not None:
+            self.app.original_widget = widget
+            return
+        self.app = widget