nice_gui.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #!/usr/bin/env python3
  2. import traceback
  3. import justpy as jp
  4. from starlette.applications import Starlette
  5. import uvicorn
  6. import inspect
  7. import time
  8. import asyncio
  9. import os.path
  10. from contextlib import contextmanager
  11. from matplotlib import pyplot as plt
  12. from .utils import handle_exceptions, provide_arguments
  13. if not inspect.stack()[-2].filename.endswith('spawn.py'):
  14. module = os.path.splitext(os.path.basename(inspect.stack()[-1].filename))[0]
  15. uvicorn.run(f'{module}:ui', host='0.0.0.0', port=80, lifespan='on', reload=True)
  16. wp = jp.QuasarPage(delete_flag=False, title='Nice GUI', favicon='favicon.png')
  17. wp.head_html = '<script>confirm = () => true;</script>' # HACK: avoid confirmation dialog for reload
  18. main = jp.Div(a=wp, classes='q-ma-md column items-start', style='row-gap: 1em')
  19. main.add_page(wp)
  20. jp.justpy(lambda: wp, start_server=False)
  21. view_stack = [main]
  22. class Element:
  23. def __init__(self, view: jp.HTMLBaseComponent):
  24. view_stack[-1].add(view)
  25. view.add_page(wp)
  26. self.view = view
  27. @property
  28. def text(self):
  29. return self.view.text
  30. @text.setter
  31. def text(self, text):
  32. self.view.text = text
  33. def set_text(self, text):
  34. self.view.text = text
  35. def __enter__(self):
  36. view_stack.append(self.view)
  37. def __exit__(self, *_):
  38. view_stack.pop()
  39. class Plot(Element):
  40. def __init__(self, view, fig):
  41. super().__init__(view)
  42. self.fig = fig
  43. def __enter__(self):
  44. plt.figure(self.fig)
  45. def __exit__(self, *_):
  46. self.view.set_figure(plt.gcf())
  47. class LinePlot(Plot):
  48. def __init__(self, view, fig, n, limit):
  49. super().__init__(view, fig)
  50. self.x = []
  51. self.Y = [[] for _ in range(n)]
  52. self.lines = [self.fig.gca().plot([], [])[0] for _ in range(n)]
  53. self.slice = slice(0 if limit is None else -limit, None)
  54. def with_legend(self, titles, **kwargs):
  55. self.fig.gca().legend(titles, **kwargs)
  56. self.view.set_figure(self.fig)
  57. return self
  58. def push(self, x, Y):
  59. self.x = [*self.x, *x][self.slice]
  60. for i in range(len(self.lines)):
  61. self.Y[i] = [*self.Y[i], *Y[i]][self.slice]
  62. self.lines[i].set_xdata(self.x)
  63. self.lines[i].set_ydata(self.Y[i])
  64. flat_y = [y_i for y in self.Y for y_i in y]
  65. self.fig.gca().set_xlim(min(self.x), max(self.x))
  66. self.fig.gca().set_ylim(min(flat_y), max(flat_y))
  67. self.view.set_figure(self.fig)
  68. class Ui(Starlette):
  69. def __init__(self):
  70. # NOTE: we enhance our own ui object with all capabilities of jp.app
  71. self.__dict__.update(jp.app.__dict__)
  72. self.tasks = []
  73. @self.on_event('startup')
  74. def startup():
  75. [jp.run_task(t) for t in self.tasks]
  76. def label(self, text='', typography=[]):
  77. if isinstance(typography, str):
  78. typography = [typography]
  79. classes = ' '.join('text-' + t for t in typography)
  80. view = jp.Div(text=text, classes=classes)
  81. return Element(view)
  82. def link(self, text='', href='#', typography=[]):
  83. if isinstance(typography, str):
  84. typography = [typography]
  85. classes = ' '.join('text-' + t for t in typography)
  86. view = jp.A(text=text, href=href, classes=classes)
  87. return Element(view)
  88. def icon(self, name, size='20px', color='dark'):
  89. view = jp.QIcon(name=name, classes=f'q-pt-xs text-{color}', size=size)
  90. return Element(view)
  91. def button(self, text, icon=None, icon_right=None, on_click=None):
  92. view = jp.QBtn(label=text, color='primary')
  93. if icon is not None:
  94. view.icon = icon
  95. if icon_right is not None:
  96. view.icon_right = icon_right
  97. if on_click is not None:
  98. view.on('click', handle_exceptions(provide_arguments(on_click)))
  99. return Element(view)
  100. def checkbox(self, text, on_change=None):
  101. view = jp.QCheckbox(text=text)
  102. if on_change is not None:
  103. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  104. return Element(view)
  105. def switch(self, text, on_change=None):
  106. view = jp.QToggle(text=text)
  107. if on_change is not None:
  108. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  109. return Element(view)
  110. def radio(self, options, value=None, on_change=None):
  111. view = jp.QOptionGroup(value=value, options=[{'label': o, 'value': o} for o in options])
  112. if on_change is not None:
  113. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  114. return Element(view)
  115. def select(self, options, value=None, on_change=None):
  116. view = jp.QSelect(value=value, options=options)
  117. if on_change is not None:
  118. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  119. return Element(view)
  120. def slider(self, min, max, on_change=None):
  121. view = jp.QSlider(min=min, max=max)
  122. if on_change is not None:
  123. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  124. return Element(view)
  125. def input(self, placeholder=None, value=None, type='text', on_change=None):
  126. view = jp.QInput(placeholder=placeholder, type=type)
  127. if value is not None:
  128. view.value = value
  129. if on_change is not None:
  130. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  131. return Element(view)
  132. @contextmanager
  133. def plot(self, close=True):
  134. fig = plt.figure()
  135. view = jp.Matplotlib()
  136. yield Plot(view, fig)
  137. view.set_figure(fig)
  138. if close:
  139. fig.close()
  140. def line_plot(self, n=1, limit=20):
  141. fig = plt.figure()
  142. view = jp.Matplotlib(fig=fig)
  143. return LinePlot(view, fig, n=n, limit=limit)
  144. def row(self):
  145. view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
  146. return Element(view)
  147. def column(self):
  148. view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
  149. return Element(view)
  150. def card(self):
  151. view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
  152. return Element(view)
  153. def timer(self, interval, callback, *, once=False):
  154. parent = view_stack[-1]
  155. async def timeout():
  156. await asyncio.sleep(interval)
  157. handle_exceptions(callback)()
  158. await parent.update()
  159. async def loop():
  160. while True:
  161. try:
  162. start = time.time()
  163. handle_exceptions(callback)()
  164. await parent.update()
  165. dt = time.time() - start
  166. await asyncio.sleep(interval - dt)
  167. except:
  168. traceback.print_exc()
  169. await asyncio.sleep(interval)
  170. self.tasks.append((timeout() if once else loop()))
  171. ui = Ui()