feat: abandon UserControl as parent class for custom controls

since that is now deprecated; see also

- https://flet.dev/blog/flet-fastapi-and-async-api-improvements#custom-controls-api-normalized
- https://flet.dev/docs/getting-started/custom-controls/

also, the docs now have a good "counter" example which inspired a
change to the timestamp control, hopefully this means smarter thread
management and no more event loop errors..?
This commit is contained in:
Lance Edgar 2024-07-05 19:52:22 -05:00
parent 2a835d9bcb
commit 1df3327d9b
9 changed files with 345 additions and 357 deletions

View file

@ -1,58 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
#
# This file is part of WuttaPOS.
#
# WuttaPOS is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
WuttaPOS - custom controls (base class)
"""
import flet as ft
from wuttapos.util import make_button, show_snackbar
class WuttaControl(ft.UserControl):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
super().__init__(*args, **kwargs)
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
# TODO: why must we save this aside from self.page ?
# but sometimes self.page gets set to None, so we must..
self.mypage = page
def informed_refresh(self, **kwargs):
pass
def make_button(self, *args, **kwargs):
return make_button(*args, **kwargs)
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def show_snackbar(self, text, bgcolor='yellow'):
show_snackbar(self.mypage, text, bgcolor=bgcolor)

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2023-2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -28,34 +28,46 @@ import time
import flet as ft
from .base import WuttaControl
from .keyboard import WuttaKeyboard
from wuttapos.util import show_snackbar
class WuttaFeedback(WuttaControl):
class WuttaFeedback(ft.Container):
default_font_size = 20
default_button_height = 60
def __init__(self, config, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
self.on_send = kwargs.pop('on_send', None)
self.on_cancel = kwargs.pop('on_cancel', None)
super().__init__(config, *args, **kwargs)
super().__init__(*args, **kwargs)
def build(self):
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
self.button = ft.Container(content=ft.Text("Feedback", size=self.default_font_size,
weight=ft.FontWeight.BOLD),
height=self.default_button_height,
width=self.default_button_height * 3,
on_click=self.initial_click,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor='blue')
# TODO: why must we save this aside from self.page ?
# but sometimes self.page gets set to None, so we must..
self.mypage = page
return self.button
self.content = ft.Text("Feedback", size=self.default_font_size,
weight=ft.FontWeight.BOLD)
self.height = self.default_button_height
self.width = self.default_button_height * 3
self.on_click = self.initial_click
self.alignment = ft.alignment.center
self.border = ft.border.all(1, 'black')
self.border_radius = ft.border_radius.all(5)
self.bgcolor = 'blue'
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def initial_click(self, e):
@ -155,7 +167,7 @@ class WuttaFeedback(WuttaControl):
})
self.dlg.open = False
self.show_snackbar("MESSAGE WAS SENT", bgcolor='green')
show_snackbar(self.mypage, "MESSAGE WAS SENT", bgcolor='green')
self.mypage.update()
if self.on_send:

View file

@ -29,18 +29,23 @@ import rattail
import flet as ft
import wuttapos
from .base import WuttaControl
from .timestamp import WuttaTimestamp
from .feedback import WuttaFeedback
from wuttapos.util import make_button
class WuttaHeader(WuttaControl):
class WuttaHeader(ft.Stack):
def __init__(self, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.terminal_id = kwargs.pop('terminal_id', None)
self.on_reset = kwargs.pop('on_reset', None)
super().__init__(*args, **kwargs)
def build(self):
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
self.txn_display = ft.Text("Txn: N", weight=ft.FontWeight.BOLD, size=20)
self.cust_display = ft.Text("Cust: N", weight=ft.FontWeight.BOLD, size=20)
@ -56,8 +61,7 @@ class WuttaHeader(WuttaControl):
terminal_style.bgcolor = 'red'
terminal_style.color = 'white'
return ft.Stack(
controls=[
self.controls = [
ft.Container(
content=ft.Row(
[
@ -98,9 +102,11 @@ class WuttaHeader(WuttaControl):
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
),
]
],
)
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def did_mount(self):
self.informed_refresh()
@ -188,7 +194,7 @@ WuttaPOS. If not, see <http://www.gnu.org/licenses/>.
has_perm = auth.has_permission(session, user, 'pos.test_error')
session.close()
if has_perm:
test_error = self.make_button("TEST ERROR", font_size=24,
test_error = make_button("TEST ERROR", font_size=24,
height=60,
width=60 * 3,
bgcolor='red',

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2023-2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -26,20 +26,23 @@ WuttaPOS - keyboard control
import flet as ft
from .base import WuttaControl
class WuttaKeyboard(WuttaControl):
class WuttaKeyboard(ft.Container):
default_font_size = 20
default_button_size = 80
def __init__(self, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
self.on_keypress = kwargs.pop('on_keypress', None)
self.on_long_backspace = kwargs.pop('on_long_backspace', None)
super().__init__(*args, **kwargs)
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
self.caps_lock = False
self.caps_map = dict([(k, k.upper())
for k in 'abcdefghijklmnopqrstuvwxyz'])
@ -69,6 +72,63 @@ class WuttaKeyboard(WuttaControl):
'/': '?',
}
self.keys = {}
def make_key(key, data=None,
on_click=self.simple_keypress,
on_long_press=None,
width=self.default_button_size,
bgcolor=None):
button = ft.Container(content=ft.Text(key, size=self.default_font_size,
weight=ft.FontWeight.BOLD),
data=data or key,
height=self.default_button_size,
width=width,
on_click=on_click,
on_long_press=on_long_press,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor=bgcolor,
)
self.keys[key] = button
return button
def caps_click(e):
self.update_caps_lock(not self.caps_lock)
self.caps_key = make_key('CAPS', on_click=caps_click)
if self.caps_lock:
self.caps_key.bgcolor = 'blue'
def shift_click(e):
self.update_shift(not self.shift)
self.shift_key = make_key('SHIFT', on_click=shift_click)
if self.shift:
self.shift_key.bgcolor = 'blue'
rows = [
[make_key(k) for k in "`1234567890-="] + [make_key('', bgcolor='yellow',
on_long_press=self.long_backspace)],
[make_key(k) for k in "qwertyuiop[]\\"],
[self.caps_key] + [make_key(k) for k in "asdfghjkl;'"] + [make_key('', bgcolor='blue')],
[self.shift_key] + [make_key(k) for k in "zxcvbnm,./"],
[make_key('SPACE', width=self.default_button_size * 5)],
]
rows = [ft.Row(controls, alignment=ft.MainAxisAlignment.CENTER)
for controls in rows]
self.content = ft.Column(rows)
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def update_caps_lock(self, caps_lock):
self.caps_lock = caps_lock
@ -131,58 +191,3 @@ class WuttaKeyboard(WuttaControl):
def long_backspace(self, e):
if self.on_long_backspace:
self.on_long_backspace()
def build(self):
self.keys = {}
def make_key(key, data=None,
on_click=self.simple_keypress,
on_long_press=None,
width=self.default_button_size,
bgcolor=None):
button = ft.Container(content=ft.Text(key, size=self.default_font_size,
weight=ft.FontWeight.BOLD),
data=data or key,
height=self.default_button_size,
width=width,
on_click=on_click,
on_long_press=on_long_press,
alignment=ft.alignment.center,
border=ft.border.all(1, 'black'),
border_radius=ft.border_radius.all(5),
bgcolor=bgcolor,
)
self.keys[key] = button
return button
def caps_click(e):
self.update_caps_lock(not self.caps_lock)
self.caps_key = make_key('CAPS', on_click=caps_click)
if self.caps_lock:
self.caps_key.bgcolor = 'blue'
def shift_click(e):
self.update_shift(not self.shift)
self.shift_key = make_key('SHIFT', on_click=shift_click)
if self.shift:
self.shift_key.bgcolor = 'blue'
rows = [
[make_key(k) for k in "`1234567890-="] + [make_key('', bgcolor='yellow',
on_long_press=self.long_backspace)],
[make_key(k) for k in "qwertyuiop[]\\"],
[self.caps_key] + [make_key(k) for k in "asdfghjkl;'"] + [make_key('', bgcolor='blue')],
[self.shift_key] + [make_key(k) for k in "zxcvbnm,./"],
[make_key('SPACE', width=self.default_button_size * 5)],
]
rows = [ft.Row(controls, alignment=ft.MainAxisAlignment.CENTER)
for controls in rows]
return ft.Container(
content=ft.Column(
rows,
),
)

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2023-2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -26,14 +26,15 @@ WuttaPOS - login form control
import flet as ft
from .base import WuttaControl
from .keyboard import WuttaKeyboard
from .tenkey import WuttaTenkeyMenu
from wuttapos.util import make_button
class WuttaLoginForm(WuttaControl):
class WuttaLoginForm(ft.Column):
def __init__(self, config, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
# permission to be checked for login to succeed
self.perm_required = kwargs.pop('perm_required', 'pos.ring_sales')
@ -57,17 +58,21 @@ class WuttaLoginForm(WuttaControl):
self.on_authz_failure = kwargs.pop('on_authz_failure', None)
self.on_login_success = kwargs.pop('on_login_success', None)
super().__init__(config, *args, **kwargs)
super().__init__(*args, **kwargs)
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
# track which login input has focus
self.focused = None
def build(self):
login_form = self.build_login_form()
self.expand = True
self.alignment = ft.MainAxisAlignment.CENTER
if self.use_tenkey:
controls = [
self.controls = [
ft.Row(
[
login_form,
@ -81,7 +86,7 @@ class WuttaLoginForm(WuttaControl):
]
else: # full keyboard
controls = [
self.controls = [
login_form,
ft.Row(),
ft.Row(),
@ -90,9 +95,12 @@ class WuttaLoginForm(WuttaControl):
on_long_backspace=self.keyboard_long_backspace),
]
return ft.Column(controls=controls,
expand=True,
alignment=ft.MainAxisAlignment.CENTER)
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def build_login_form(self):
form_fields = []
@ -113,13 +121,13 @@ class WuttaLoginForm(WuttaControl):
form_fields.append(self.password)
login_button = self.make_button("Login",
login_button = make_button("Login",
height=60,
width=60 * 2.5,
bgcolor='blue',
on_click=self.attempt_login)
reset_button = self.make_button("Clear",
reset_button = make_button("Clear",
height=60,
width=60 * 2.5,
on_click=self.clear_login)

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2023-2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -26,11 +26,11 @@ WuttaPOS - base lookup control
import flet as ft
from .base import WuttaControl
from .keyboard import WuttaKeyboard
from wuttapos.util import make_button
class WuttaLookup(WuttaControl):
class WuttaLookup(ft.Container):
default_font_size = 40
font_size = default_font_size * 0.8
@ -39,7 +39,8 @@ class WuttaLookup(WuttaControl):
long_scroll_delta = 500
def __init__(self, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
self.show_search = kwargs.pop('show_search', True)
self.initial_search = kwargs.pop('initial_search', None)
self.allow_empty_query = kwargs.pop('allow_empty_query', False)
@ -48,12 +49,14 @@ class WuttaLookup(WuttaControl):
super().__init__(*args, **kwargs)
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
# track current selection
self.selected_uuid = None
self.selected_datarow = None
def build(self):
self.search_results = ft.DataTable(
columns=[ft.DataColumn(self.make_cell_text(text))
for text in self.get_results_columns()],
@ -64,7 +67,7 @@ class WuttaLookup(WuttaControl):
weight=ft.FontWeight.BOLD,
visible=False)
self.select_button = self.make_button("Select",
self.select_button = make_button("Select",
font_size=self.font_size * 0.8,
height=self.default_button_height_dlg * 0.8,
width=self.default_button_height_dlg * 1.3,
@ -72,14 +75,14 @@ class WuttaLookup(WuttaControl):
disabled=True,
bgcolor=self.disabled_bgcolor)
self.up_button = self.make_button("",
self.up_button = make_button("",
font_size=self.font_size,
height=self.default_button_height_dlg,
width=self.default_button_height_dlg,
on_click=self.up_click,
on_long_press=self.up_longpress)
self.down_button = self.make_button("",
self.down_button = make_button("",
font_size=self.font_size,
height=self.default_button_height_dlg,
width=self.default_button_height_dlg,
@ -110,13 +113,13 @@ class WuttaLookup(WuttaControl):
[
ft.Text("SEARCH FOR:"),
self.searchbox,
self.make_button("Lookup",
make_button("Lookup",
font_size=self.font_size * 0.8,
height=self.default_button_height_dlg * 0.8,
width=self.default_button_height_dlg * 1.3,
on_click=self.lookup,
bgcolor='blue'),
self.make_button("Reset",
make_button("Reset",
font_size=self.font_size * 0.8,
height=self.default_button_height_dlg * 0.8,
width=self.default_button_height_dlg * 1.3,
@ -150,7 +153,7 @@ class WuttaLookup(WuttaControl):
ft.Row(),
ft.Row(),
ft.Row(),
self.make_button("Cancel",
make_button("Cancel",
font_size=self.font_size * 0.8,
height=self.default_button_height_dlg * 0.8,
width=self.default_button_height_dlg * 1.3,
@ -162,10 +165,15 @@ class WuttaLookup(WuttaControl):
),
])
return ft.Container(
content=ft.Column(controls=controls),
height=None if self.show_search else 600,
)
self.content = ft.Column(controls=controls)
self.height = None if self.show_search else 600
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def get_results_columns(self):
raise NotImplementedError

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2023-2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -26,15 +26,16 @@ WuttaPOS - ten-key control
import flet as ft
from .base import WuttaControl
from wuttapos.util import make_button
class WuttaTenkeyMenu(WuttaControl):
class WuttaTenkeyMenu(ft.Container):
default_font_size = 40
default_button_size = 100
def __init__(self, *args, **kwargs):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
self.simple = kwargs.pop('simple', False)
self.on_char = kwargs.pop('on_char', None)
self.on_enter = kwargs.pop('on_enter', None)
@ -42,9 +43,12 @@ class WuttaTenkeyMenu(WuttaControl):
self.on_up_longpress = kwargs.pop('on_up_longpress', None)
self.on_down_click = kwargs.pop('on_down_click', None)
self.on_down_longpress = kwargs.pop('on_down_longpress', None)
super().__init__(*args, **kwargs)
def build(self):
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
row1 = [
self.make_tenkey_button("1"),
@ -90,8 +94,7 @@ class WuttaTenkeyMenu(WuttaControl):
self.make_tenkey_button("ENTER", width=self.default_button_size * 2),
])
return ft.Container(
content=ft.Column(
self.content = ft.Column(
[
ft.Row(
controls=row1,
@ -111,9 +114,15 @@ class WuttaTenkeyMenu(WuttaControl):
),
],
spacing=0,
),
)
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def make_tenkey_button(
self,
text,
@ -132,7 +141,7 @@ class WuttaTenkeyMenu(WuttaControl):
if not on_click:
on_click = self.tenkey_click
return self.make_button(text, font_size=font_size, bgcolor='green',
return make_button(text, font_size=font_size, bgcolor='green',
height=height, width=width,
on_click=on_click,
on_long_press=on_long_press)

View file

@ -24,43 +24,35 @@
WuttaPOS - timestamp control
"""
import threading
import time
import asyncio
import flet as ft
from .base import WuttaControl
class WuttaTimestamp(ft.Text):
class WuttaTimestamp(WuttaControl):
def __init__(self, config, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
def __init__(self, *args, **kwargs):
self.weight = kwargs.pop('weight', None)
self.size = kwargs.pop('size', None)
super().__init__(*args, **kwargs)
def build(self):
text = self.render_time(self.app.localtime())
self.display = ft.Text(text, weight=self.weight, size=self.size)
self.config = config
self.app = self.config.get_app()
# nb. daemonized thread should be stopped when app exits
# cf. https://docs.python.org/3/library/threading.html#thread-objects
thread = threading.Thread(target=self.update_display, daemon=True)
thread.start()
self.value = self.render_time(self.app.localtime())
return self.display
def did_mount(self):
self.running = True
self.page.run_task(self.update_display)
def will_unmount(self):
self.running = False
def render_time(self, value):
return value.strftime('%a %d %b %Y %I:%M:%S %p')
def update_display(self):
while True:
if self.page:
self.display.value = self.render_time(self.app.localtime())
try:
async def update_display(self):
while self.running:
self.value = self.render_time(self.app.localtime())
self.update()
except RuntimeError as error:
if str(error) == 'Event loop is closed':
break
raise
time.sleep(0.5)
await asyncio.sleep(0.5)

View file

@ -2,7 +2,7 @@
################################################################################
#
# WuttaPOS -- Pythonic Point of Sale System
# Copyright © 2023 Lance Edgar
# Copyright © 2024 Lance Edgar
#
# This file is part of WuttaPOS.
#
@ -26,21 +26,24 @@ WuttaPOS - txn item control
import flet as ft
from .base import WuttaControl
class WuttaTxnItem(WuttaControl):
class WuttaTxnItem(ft.Row):
"""
Control for displaying a transaction line item within main POS
items list.
"""
font_size = 24
def __init__(self, config, row, *args, **kwargs):
super().__init__(config, *args, **kwargs)
self.row = row
def __init__(self, config, row, page=None, *args, **kwargs):
self.on_reset = kwargs.pop('on_reset', None)
def build(self):
super().__init__(*args, **kwargs)
self.config = config
self.app = config.get_app()
self.enum = self.app.enum
self.row = row
self.major_style = ft.TextStyle(size=self.font_size,
weight=ft.FontWeight.BOLD)
@ -50,11 +53,11 @@ class WuttaTxnItem(WuttaControl):
if self.row.row_type in (self.enum.POS_ROW_TYPE_SELL,
self.enum.POS_ROW_TYPE_OPEN_RING):
return self.build_item_sell()
self.build_item_sell()
elif self.row.row_type in (self.enum.POS_ROW_TYPE_TENDER,
self.enum.POS_ROW_TYPE_CHANGE_BACK):
return self.build_item_tender()
self.build_item_tender()
def build_item_sell(self):
@ -72,8 +75,7 @@ class WuttaTxnItem(WuttaControl):
# set initial text display values
self.refresh(update=False)
return ft.Row(
[
self.controls = [
ft.Text(
spans=[
ft.TextSpan(f"{self.row.description}",
@ -92,13 +94,11 @@ class WuttaTxnItem(WuttaControl):
],
),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
]
self.alignment = ft.MainAxisAlignment.SPACE_BETWEEN
def build_item_tender(self):
return ft.Row(
[
self.controls = [
ft.Text(
spans=[
ft.TextSpan(f"{self.row.description}",
@ -112,9 +112,15 @@ class WuttaTxnItem(WuttaControl):
],
),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
]
self.alignment = ft.MainAxisAlignment.SPACE_BETWEEN
def informed_refresh(self, **kwargs):
pass
def reset(self, e=None):
if self.on_reset:
self.on_reset(e=e)
def refresh(self, update=True):