|
@@ -5,9 +5,8 @@
|
|
#
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
import itertools
|
|
import itertools
|
|
-from decimal import Decimal, InvalidOperation
|
|
|
|
from itertools import chain
|
|
from itertools import chain
|
|
-from typing import Callable, Union
|
|
|
|
|
|
+from typing import List, Tuple, Union, Iterable, Callable
|
|
from urwid import (
|
|
from urwid import (
|
|
connect_signal,
|
|
connect_signal,
|
|
AttrMap,
|
|
AttrMap,
|
|
@@ -21,6 +20,7 @@ from urwid import (
|
|
Pile,
|
|
Pile,
|
|
Text,
|
|
Text,
|
|
)
|
|
)
|
|
|
|
+from urwid.numedit import FloatEdit
|
|
|
|
|
|
from .. import COPYRIGHT
|
|
from .. import COPYRIGHT
|
|
from ..widgets import (
|
|
from ..widgets import (
|
|
@@ -52,6 +52,21 @@ def in_same_row(name):
|
|
_, row = name.split('#', 1)
|
|
_, row = name.split('#', 1)
|
|
return lambda x: x[0][1] == int(row)
|
|
return lambda x: x[0][1] == int(row)
|
|
|
|
|
|
|
|
+def unzip(iter: List[Tuple[AutoCompleteEdit, FloatEdit, AutoCompleteEdit]]) -> Tuple[
|
|
|
|
+ List[AutoCompleteEdit], List[FloatEdit], List[AutoCompleteEdit]
|
|
|
|
+]:
|
|
|
|
+ return zip(*iter)
|
|
|
|
+
|
|
|
|
+def extract_values(x: Union[List[AutoCompleteFloatEdit], List[FloatEdit]]) -> Iterable[str]:
|
|
|
|
+ if isinstance(x, list) or isinstance(x, tuple):
|
|
|
|
+ if len(x) == 0:
|
|
|
|
+ return []
|
|
|
|
+ return ( v.get_edit_text() for v in x )
|
|
|
|
+ raise Exception(f"Unsupported type: {type(x)}")
|
|
|
|
+
|
|
|
|
+def to_named_value(name: str) -> Callable[[str], Tuple[str,str]]:
|
|
|
|
+ return lambda e: (f'{name}#{e[0]}', e[1])
|
|
|
|
+
|
|
class RecipeEditor(FocusWidget):
|
|
class RecipeEditor(FocusWidget):
|
|
|
|
|
|
def keypress(self, size, key):
|
|
def keypress(self, size, key):
|
|
@@ -65,6 +80,8 @@ class RecipeEditor(FocusWidget):
|
|
self.advance_focus()
|
|
self.advance_focus()
|
|
elif key == 'shift tab':
|
|
elif key == 'shift tab':
|
|
self.advance_focus(reverse=True)
|
|
self.advance_focus(reverse=True)
|
|
|
|
+ elif key == 'ctrl delete':
|
|
|
|
+ self.clear()
|
|
else:
|
|
else:
|
|
return super().keypress(size, key)
|
|
return super().keypress(size, key)
|
|
|
|
|
|
@@ -95,9 +112,12 @@ class RecipeEditor(FocusWidget):
|
|
|
|
|
|
@property
|
|
@property
|
|
def data(self):
|
|
def data(self):
|
|
|
|
+ zipped = zip(
|
|
|
|
+ ['product', 'quantity', 'unit'],
|
|
|
|
+ map(extract_values, unzip(self.ingredients)),
|
|
|
|
+ )
|
|
ret = dict(itertools.chain(
|
|
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) ],
|
|
|
|
|
|
+ *[ map(to_named_value(n), enumerate(l)) for n,l in zipped ],
|
|
[ ('organic', self.organic.state) ]
|
|
[ ('organic', self.organic.state) ]
|
|
))
|
|
))
|
|
return ret
|
|
return ret
|
|
@@ -106,8 +126,12 @@ class RecipeEditor(FocusWidget):
|
|
def data(self, _data: dict):
|
|
def data(self, _data: dict):
|
|
for k,v in _data.items():
|
|
for k,v in _data.items():
|
|
if len(k.split('#')) > 1:
|
|
if len(k.split('#')) > 1:
|
|
- name, idx = k.split('#',1)
|
|
|
|
- self.ingredients[int(idx)][0 if name == 'product' else 1 ].set_edit_text(v)
|
|
|
|
|
|
+ name, idx = k.split('#', 1)
|
|
|
|
+ w = self.ingredients[int(idx)][ next(( pos for pos, n in zip(
|
|
|
|
+ [0, 1, 2],
|
|
|
|
+ ['product', 'quantity', 'unit']
|
|
|
|
+ ) if n == name ))]
|
|
|
|
+ w.set_edit_text(v)
|
|
if k == 'organic':
|
|
if k == 'organic':
|
|
self.organic.set_state(v)
|
|
self.organic.set_state(v)
|
|
|
|
|
|
@@ -115,7 +139,9 @@ class RecipeEditor(FocusWidget):
|
|
for ef in self.ingredients:
|
|
for ef in self.ingredients:
|
|
ef[0].set_edit_text('')
|
|
ef[0].set_edit_text('')
|
|
ef[1].set_edit_text('')
|
|
ef[1].set_edit_text('')
|
|
|
|
+ ef[2].set_edit_text('')
|
|
self.organic.set_state('mixed')
|
|
self.organic.set_state('mixed')
|
|
|
|
+ self.instructions.set_edit_text('')
|
|
return self.update()
|
|
return self.update()
|
|
|
|
|
|
def update(self):
|
|
def update(self):
|
|
@@ -124,18 +150,28 @@ class RecipeEditor(FocusWidget):
|
|
def __init__(self,
|
|
def __init__(self,
|
|
activity_manager: ActivityManager,
|
|
activity_manager: ActivityManager,
|
|
query_manager: QueryManager,
|
|
query_manager: QueryManager,
|
|
|
|
+ recipe,
|
|
):
|
|
):
|
|
self.buttons = {
|
|
self.buttons = {
|
|
'clear': Button(('streak', 'Clear')),
|
|
'clear': Button(('streak', 'Clear')),
|
|
'exit': Button(('streak', 'Exit')),
|
|
'exit': Button(('streak', 'Exit')),
|
|
'add': Button(('streak', 'Add')),
|
|
'add': Button(('streak', 'Add')),
|
|
}
|
|
}
|
|
- self.ingredients = [
|
|
|
|
- (AutoCompleteEdit(('bg', 'product#0')), AutoCompleteEdit(('bg', 'unit#0'))),
|
|
|
|
- (AutoCompleteEdit(('bg', 'product#1')), AutoCompleteEdit(('bg', 'unit#1'))),
|
|
|
|
|
|
+ self.ingredients: List[Tuple[AutoCompleteEdit, FloatEdit, AutoCompleteEdit]] = [
|
|
|
|
+ (
|
|
|
|
+ AutoCompleteEdit(('bg', f'product#{idx}'), edit_text=ingredient[0]),
|
|
|
|
+ FloatEdit(('bg', f''), default=ingredient[1]),
|
|
|
|
+ AutoCompleteEdit(('bg', f'unit#{idx}'), edit_text=ingredient[2]),
|
|
|
|
+ ) for idx, ingredient in enumerate(recipe['ingredients'])
|
|
|
|
+ ] if len(recipe['ingredients']) else [
|
|
|
|
+ (
|
|
|
|
+ AutoCompleteEdit(('bg', 'product#0')),
|
|
|
|
+ FloatEdit(('bg', f'')),
|
|
|
|
+ AutoCompleteEdit(('bg', 'unit#0'))
|
|
|
|
+ ),
|
|
]
|
|
]
|
|
self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
|
|
self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
|
|
- self.instructions = Edit(f'', multiline=True, allow_tab=True)
|
|
|
|
|
|
+ self.instructions = Edit('', edit_text=recipe['instructions'] or u'', multiline=True, allow_tab=True)
|
|
|
|
|
|
bottom_pane = [
|
|
bottom_pane = [
|
|
self.organic,
|
|
self.organic,
|
|
@@ -157,7 +193,8 @@ class RecipeEditor(FocusWidget):
|
|
))))
|
|
))))
|
|
))
|
|
))
|
|
connect_signal(widget[1], 'postchange', lambda _,v: self.update())
|
|
connect_signal(widget[1], 'postchange', lambda _,v: self.update())
|
|
- connect_signal(widget[1], 'apply', lambda w, name: self.autocomplete_callback(
|
|
|
|
|
|
+ connect_signal(widget[2], 'postchange', lambda _,v: self.update())
|
|
|
|
+ connect_signal(widget[2], 'apply', lambda w, name: self.autocomplete_callback(
|
|
w, self.autocomplete_options(name, dict(map(
|
|
w, self.autocomplete_options(name, dict(map(
|
|
to_unnumbered_field,
|
|
to_unnumbered_field,
|
|
filter(in_same_row(name), map(to_numbered_field, self.data.items())
|
|
filter(in_same_row(name), map(to_numbered_field, self.data.items())
|
|
@@ -166,7 +203,7 @@ class RecipeEditor(FocusWidget):
|
|
|
|
|
|
connect_signal(self.buttons['clear'], 'click', lambda x: self.clear().update())
|
|
connect_signal(self.buttons['clear'], 'click', lambda x: self.clear().update())
|
|
connect_signal(self.buttons['exit'], 'click', lambda x: show_or_exit('esc'))
|
|
connect_signal(self.buttons['exit'], 'click', lambda x: show_or_exit('esc'))
|
|
- self.clear()
|
|
|
|
|
|
+ connect_signal(self.instructions, 'postchange', lambda _,v: self.update())
|
|
|
|
|
|
header = Text(u'Recipe Editor', 'center')
|
|
header = Text(u'Recipe Editor', 'center')
|
|
_copyright = Text(COPYRIGHT, 'center')
|
|
_copyright = Text(COPYRIGHT, 'center')
|
|
@@ -188,15 +225,20 @@ class RecipeEditor(FocusWidget):
|
|
) for idx, ingredient in enumerate(self.ingredients)
|
|
) for idx, ingredient in enumerate(self.ingredients)
|
|
]
|
|
]
|
|
middle_pane = [
|
|
middle_pane = [
|
|
|
|
+ LineBox(
|
|
|
|
+ ingredient[1], title=f'Quantity {idx}', title_align='left'
|
|
|
|
+ ) for idx, ingredient in enumerate(self.ingredients)
|
|
|
|
+ ]
|
|
|
|
+ right_pane = [
|
|
LineBox(AttrMap(
|
|
LineBox(AttrMap(
|
|
AutoCompletePopUp(
|
|
AutoCompletePopUp(
|
|
- ingredient[1],
|
|
|
|
|
|
+ ingredient[2],
|
|
self.apply_choice,
|
|
self.apply_choice,
|
|
lambda: activity_manager.show(self.update())
|
|
lambda: activity_manager.show(self.update())
|
|
), 'streak'), title=f'Unit {idx}', title_align='left'
|
|
), 'streak'), title=f'Unit {idx}', title_align='left'
|
|
) for idx, ingredient in enumerate(self.ingredients)
|
|
) for idx, ingredient in enumerate(self.ingredients)
|
|
]
|
|
]
|
|
- right_pane = [
|
|
|
|
|
|
+ gutter = [
|
|
*[ Divider() for _ in itertools.product(
|
|
*[ Divider() for _ in itertools.product(
|
|
range(3), self.ingredients[:-1]
|
|
range(3), self.ingredients[:-1]
|
|
)],
|
|
)],
|
|
@@ -221,16 +263,22 @@ class RecipeEditor(FocusWidget):
|
|
]))
|
|
]))
|
|
], dividechars=1),
|
|
], dividechars=1),
|
|
'bottom_pane': Pile(bottom_pane),
|
|
'bottom_pane': Pile(bottom_pane),
|
|
- 'right_pane': (8, Pile(right_pane)),
|
|
|
|
|
|
+ 'right_pane': (16, Pile(right_pane)),
|
|
'middle_pane': (16, Pile(middle_pane)),
|
|
'middle_pane': (16, Pile(middle_pane)),
|
|
'left_pane': Pile(left_pane),
|
|
'left_pane': Pile(left_pane),
|
|
|
|
+ 'gutter': (8, Pile(gutter))
|
|
}
|
|
}
|
|
|
|
|
|
widget = Pile([
|
|
widget = Pile([
|
|
banner,
|
|
banner,
|
|
Divider(),
|
|
Divider(),
|
|
components['top_pane'],
|
|
components['top_pane'],
|
|
- Columns((components['left_pane'], components['middle_pane'], (1,Divider()), components['right_pane']),
|
|
|
|
|
|
+ Columns((
|
|
|
|
+ components['left_pane'],
|
|
|
|
+ components['middle_pane'],
|
|
|
|
+ components['right_pane'],
|
|
|
|
+ (1,Divider()),
|
|
|
|
+ components['gutter']),
|
|
dividechars=0,
|
|
dividechars=0,
|
|
),
|
|
),
|
|
components['bottom_pane'],
|
|
components['bottom_pane'],
|
|
@@ -242,11 +290,14 @@ class RecipeEditor(FocusWidget):
|
|
self.buttons.items(),
|
|
self.buttons.items(),
|
|
[
|
|
[
|
|
('ingredients', self.ingredients[-1][0]),
|
|
('ingredients', self.ingredients[-1][0]),
|
|
- ('units', self.ingredients[-1][1]),
|
|
|
|
|
|
+ ('instructions', self.instructions),
|
|
|
|
+ ('quantity', self.ingredients[-1][1]),
|
|
|
|
+ ('units', self.ingredients[-1][2]),
|
|
('organic', self.organic)
|
|
('organic', self.organic)
|
|
],
|
|
],
|
|
) if x == n),
|
|
) if x == n),
|
|
[
|
|
[
|
|
|
|
+ 'instructions',
|
|
'ingredients', 'units', 'add',
|
|
'ingredients', 'units', 'add',
|
|
'organic',
|
|
'organic',
|
|
'clear', 'exit',
|
|
'clear', 'exit',
|