|
@@ -0,0 +1,219 @@
|
|
|
+#
|
|
|
+# Copyright (c) Daniel Sheffield 2023
|
|
|
+#
|
|
|
+# All rights reserved
|
|
|
+#
|
|
|
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
|
+import itertools
|
|
|
+from decimal import Decimal, InvalidOperation
|
|
|
+from itertools import chain
|
|
|
+from typing import Callable, Union
|
|
|
+from urwid import (
|
|
|
+ connect_signal,
|
|
|
+ AttrMap,
|
|
|
+ Button,
|
|
|
+ Columns,
|
|
|
+ Divider,
|
|
|
+ Edit,
|
|
|
+ Filler,
|
|
|
+ LineBox,
|
|
|
+ Padding,
|
|
|
+ Pile,
|
|
|
+ Text,
|
|
|
+)
|
|
|
+
|
|
|
+from .. import COPYRIGHT
|
|
|
+from ..widgets import (
|
|
|
+ AutoCompleteEdit,
|
|
|
+ AutoCompleteFloatEdit,
|
|
|
+ FocusWidget,
|
|
|
+ AutoCompletePopUp,
|
|
|
+ NoTabCheckBox,
|
|
|
+ FlowBarGraphWithVScale,
|
|
|
+)
|
|
|
+from ..db_utils import QueryManager
|
|
|
+from . import ActivityManager, show_or_exit
|
|
|
+from .Rating import Rating
|
|
|
+
|
|
|
+class RecipeEditor(FocusWidget):
|
|
|
+
|
|
|
+ def keypress(self, size, key):
|
|
|
+ if isinstance(key, tuple):
|
|
|
+ return
|
|
|
+
|
|
|
+ if getattr(self._w.original_widget, 'original_widget', None) is None:
|
|
|
+ return super().keypress(size, key)
|
|
|
+
|
|
|
+ if key == 'tab':
|
|
|
+ self.advance_focus()
|
|
|
+ elif key == 'shift tab':
|
|
|
+ self.advance_focus(reverse=True)
|
|
|
+ else:
|
|
|
+ return super().keypress(size, key)
|
|
|
+
|
|
|
+ def apply_choice(self, name, value):
|
|
|
+ self.apply_changes(name, value)
|
|
|
+ for k,v in self.data.items():
|
|
|
+ if k == name or v:
|
|
|
+ continue
|
|
|
+ options = self.query_manager.unique_suggestions(k, **self.data)
|
|
|
+ if len(options) == 1:
|
|
|
+ self.apply_changes(k, list(options)[0])
|
|
|
+
|
|
|
+ def apply_changes(self, name, value):
|
|
|
+ self.data = {
|
|
|
+ name: value if name != 'organic' else {
|
|
|
+ 'yes': True, 'no': False,
|
|
|
+ True: True, False: False,
|
|
|
+ 'mixed': '',
|
|
|
+ }[value],
|
|
|
+ }
|
|
|
+
|
|
|
+ @property
|
|
|
+ def data(self):
|
|
|
+ ret = dict(itertools.chain(
|
|
|
+ [ (f'product#{idx}', v[0].get_edit_text()) for idx,v in enumerate(self.ingredients) ],
|
|
|
+ [ (f'unit#{idx}', v[1].get_edit_text()) for idx, v in enumerate(self.ingredients) ],
|
|
|
+ [ ('organic', self.organic.state) ]
|
|
|
+ ))
|
|
|
+ return ret
|
|
|
+
|
|
|
+ @data.setter
|
|
|
+ def data(self, _data: dict):
|
|
|
+ for k,v in _data.items():
|
|
|
+ if len(k.split('#')) > 1:
|
|
|
+ name, idx = k.split('#',1)
|
|
|
+ self.ingredients[int(idx)][0 if name == 'product' else 1 ].set_edit_text(v)
|
|
|
+ if k == 'organic':
|
|
|
+ self.organic.set_state(v)
|
|
|
+
|
|
|
+ def clear(self):
|
|
|
+ for ef in self.ingredients:
|
|
|
+ ef[0].set_edit_text('')
|
|
|
+ ef[1].set_edit_text('')
|
|
|
+ self.organic.set_state('mixed')
|
|
|
+ return self.update()
|
|
|
+
|
|
|
+ def update(self):
|
|
|
+ return self
|
|
|
+
|
|
|
+ def __init__(self,
|
|
|
+ activity_manager: ActivityManager,
|
|
|
+ query_manager: QueryManager,
|
|
|
+ autocomplete_cb: Callable[[
|
|
|
+ Union[AutoCompleteEdit, AutoCompleteFloatEdit], str, dict
|
|
|
+ ], None],
|
|
|
+ ):
|
|
|
+ self.buttons = {
|
|
|
+ 'clear': Button(('streak', 'Clear')),
|
|
|
+ 'exit': Button(('streak', 'Exit')),
|
|
|
+ 'add': Button(('streak', 'Add')),
|
|
|
+ }
|
|
|
+ self.ingredients = [
|
|
|
+ (AutoCompleteEdit(('bg', 'product#0')), AutoCompleteEdit(('bg', 'unit#0'))),
|
|
|
+ ]
|
|
|
+ self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
|
|
|
+ self.instructions = Edit(f'')
|
|
|
+
|
|
|
+ bottom_pane = [
|
|
|
+ self.organic,
|
|
|
+ LineBox(self.instructions, title=f'Instructions'),
|
|
|
+ ]
|
|
|
+
|
|
|
+ self.query_manager = query_manager
|
|
|
+ connect_signal(self.organic, 'postchange', lambda _,v: self.update())
|
|
|
+
|
|
|
+ # todo: call this when adding new ingredient
|
|
|
+ for idx, widget in enumerate(self.ingredients):
|
|
|
+ connect_signal(widget[0], 'postchange', lambda _,v: self.update())
|
|
|
+ connect_signal(widget[0], 'apply', lambda w, name: autocomplete_cb(
|
|
|
+ w, name, self.data)
|
|
|
+ )
|
|
|
+ connect_signal(widget[1], 'postchange', lambda _,v: self.update())
|
|
|
+ connect_signal(widget[1], 'apply', lambda w, name: autocomplete_cb(
|
|
|
+ w, name, self.data)
|
|
|
+ )
|
|
|
+
|
|
|
+ connect_signal(self.buttons['clear'], 'click', lambda x: self.clear().update())
|
|
|
+ connect_signal(self.buttons['exit'], 'click', lambda x: show_or_exit('esc'))
|
|
|
+ self.clear()
|
|
|
+
|
|
|
+ header = Text(u'Recipe Editor', 'center')
|
|
|
+ _copyright = Text(COPYRIGHT, 'center')
|
|
|
+
|
|
|
+ banner = Pile([
|
|
|
+ Padding(header, 'center', width=('relative', 100)),
|
|
|
+ Padding(_copyright, 'center', width=('relative', 100)),
|
|
|
+ ])
|
|
|
+ banner = AttrMap(banner, 'banner')
|
|
|
+
|
|
|
+ # todo: call this when adding new ingredient
|
|
|
+ left_pane = [
|
|
|
+ LineBox(AttrMap(
|
|
|
+ AutoCompletePopUp(
|
|
|
+ ingredient[0],
|
|
|
+ self.apply_choice,
|
|
|
+ lambda: activity_manager.show(self.update())
|
|
|
+ ), 'streak'), title=f'Ingredient {idx}', title_align='left'
|
|
|
+ ) for idx, ingredient in enumerate(self.ingredients)
|
|
|
+ ]
|
|
|
+ middle_pane = [
|
|
|
+ LineBox(AttrMap(
|
|
|
+ AutoCompletePopUp(
|
|
|
+ ingredient[1],
|
|
|
+ self.apply_choice,
|
|
|
+ lambda: activity_manager.show(self.update())
|
|
|
+ ), 'streak'), title=f'Unit {idx}', title_align='left'
|
|
|
+ ) for idx, ingredient in enumerate(self.ingredients)
|
|
|
+ ]
|
|
|
+ right_pane = [
|
|
|
+ self.buttons['add'],
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+ components = {
|
|
|
+ 'top_pane': Columns([
|
|
|
+ (9, Pile([
|
|
|
+ Divider(),
|
|
|
+ AttrMap(self.buttons['clear'], 'streak'),
|
|
|
+ Divider(),
|
|
|
+ ])),
|
|
|
+ Divider(),
|
|
|
+ (9, Pile([
|
|
|
+ Divider(),
|
|
|
+ AttrMap(self.buttons['exit'], 'streak'),
|
|
|
+ Divider(),
|
|
|
+ ]))
|
|
|
+ ], dividechars=1),
|
|
|
+ 'bottom_pane': Pile(bottom_pane),
|
|
|
+ 'right_pane': (8, Pile(right_pane)),
|
|
|
+ 'middle_pane': (16, Pile(middle_pane)),
|
|
|
+ 'left_pane': Pile(left_pane),
|
|
|
+ }
|
|
|
+
|
|
|
+ widget = Pile([
|
|
|
+ banner,
|
|
|
+ Divider(),
|
|
|
+ components['top_pane'],
|
|
|
+ Columns((components['left_pane'], components['middle_pane'], (1,Divider()), components['right_pane']),
|
|
|
+ dividechars=0,
|
|
|
+ ),
|
|
|
+ components['bottom_pane'],
|
|
|
+ ])
|
|
|
+ widget = Filler(widget, 'top')
|
|
|
+ widget = AttrMap(widget, 'bg')
|
|
|
+ super().__init__(widget, map(
|
|
|
+ lambda x: next(w for n,w in chain(
|
|
|
+ self.buttons.items(),
|
|
|
+ [
|
|
|
+ ('ingredients', self.ingredients[-1][0]),
|
|
|
+ ('units', self.ingredients[-1][1]),
|
|
|
+ ('organic', self.organic)
|
|
|
+ ],
|
|
|
+ ) if x == n),
|
|
|
+ [
|
|
|
+ 'ingredients', 'units', 'add',
|
|
|
+ 'organic',
|
|
|
+ 'clear', 'exit',
|
|
|
+ ]
|
|
|
+ ))
|