import asyncio
import os
from contextlib import contextmanager
from pywebvue.utils import read_file_as_base64_url
from trame.html import AbstractElement, Span, vuetify, Triggers
import pywebvue
import trame as tr
import trame.internal as tri
LOGO_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../html/assets/logo.svg")
)
[docs]class AbstractLayout:
def __init__(self, _root_elem, name, favicon=None, on_ready=None):
self.name = name
self.favicon = None
self.triggers = Triggers("_js_trame_triggers")
if os.path.exists(LOGO_PATH):
self.favicon = read_file_as_base64_url(LOGO_PATH)
self.on_ready = on_ready
self.children = _root_elem.children
self._current_root = _root_elem
# Always add triggers
self.children += [self.triggers]
if favicon:
file_path = os.path.join(tri.base_directory(), favicon)
if os.path.exists(file_path):
self.favicon = file_path
else:
print(f"Invalid path to favicon: {file_path}")
@property
def root(self):
"""
Top level Vue component. Useful for providing / injecting into children components. Setting makes old root child of new root.
"""
return self._current_root
@root.setter
def root(self, new_root):
if new_root and self._current_root != new_root:
new_root.children += [self._current_root]
self._current_root = new_root
@property
def html(self):
"""
Compute corresponding layout String which represent the html part.
"""
return self.root.html
@property
def state(self):
"""
Return App state as a dictionary or extend it when setting.
This is a safe way to build the state incrementaly.
>>> layout.state = { "a": 1, "b": 2 }
>>> print(layout.state)
... {"a": 1, "b": 2}
>>> layout.state = { "c": 3, "d": 4 }
>>> print(layout.state)
... {"a": 1, "b": 2, "c": 3, "d": 4}
"""
return tri.get_app_instance().state
@state.setter
def state(self, value):
_app = tri.get_app_instance()
for (k, v) in value.items():
_app.set(k, v)
[docs] def flush_content(self):
"""Push new content to client"""
_app = tri.get_app_instance()
_app.layout = self.html
def _init_app(self, _app):
_app.name = self.name
_app.layout = self.html
if self.favicon:
_app.favicon = self.favicon
# Evaluate html for added routes
for route in _app.routes:
component = route.get("component", None)
if component is None:
continue
template = component.get("template", None)
if template is None:
continue
if isinstance(template, AbstractElement):
route["component"]["template"] = template.html
[docs] def start(self, port=None, debug=None, **kwargs):
"""
Start the application server.
:param port: Which port to run the server on
:param debug: Whether to enable debugging tools. Defaults to
None, in which case it is set to True if the --dev
flag was passed as a command line argument.
:type debug: bool or None
:param kwargs: arguments to forward to run_server()
Some of the kwargs that may be forwarded to run_server() include:
* exec_mode (str): "main" (default) or "task" for running in an
environment that already has an event loop,
such as a Jupyter notebook.
The kwargs will also be forwarded to print_server_info(), so that
the `server` kwarg may be used to indicate whether a new window
should be opened.
"""
_app = tri.get_app_instance()
self._init_app(_app)
if debug is None:
parser = _app.cli_parser
args, _unknown = parser.parse_known_args()
debug = args.dev
_app._debug = debug
_app.on_ready = tri.print_server_info(self.on_ready, **kwargs)
# Dev validation
tri.validate_key_names()
_app.run_server(port=port, **kwargs)
[docs] def start_thread(
self, port=None, print_server_info=False, on_server_listening=None, **kwargs
):
_app = tri.get_app_instance()
self._init_app(_app)
if print_server_info:
_app.on_ready = tri.print_server_info(
tri.compose_callbacks(self.on_ready, on_server_listening)
)
else:
_app.on_ready = tri.compose_callbacks(self.on_ready, on_server_listening)
# Dev validation
tri.validate_key_names()
server_thread = tri.AppServerThread(_app, port, **kwargs)
server_thread.start()
return server_thread
[docs] def start_desktop_window(self, on_msg=None, **kwargs):
from multiprocessing import Queue
_msg_queue = Queue()
_app = tri.get_app_instance()
self._init_app(_app)
async def process_msg():
keep_processing = True
while keep_processing:
await asyncio.sleep(0.5)
if not _msg_queue.empty():
msg = _msg_queue.get_nowait()
if on_msg:
on_msg(msg)
if msg == "closing":
keep_processing = False
_app.stop_server()
asyncio.get_event_loop().create_task(process_msg())
def start_client(**_):
client_process = tri.ClientWindowProcess(
title=_app.name, port=_app.server_port, msg_queue=_msg_queue, **kwargs
)
client_process.start()
_app.on_ready = tri.compose_callbacks(self.on_ready, start_client)
# Dev validation
tri.validate_key_names()
_app.run_server(port=0)
[docs] def add_route(self, name, path, template):
_app = tri.get_app_instance()
# TODO: Check if route already exists?
_app.routes.append(
{
"name": name,
"path": path,
"component": {
"template": template,
},
}
)
[docs] @contextmanager
def with_route(self, name, path, root):
try:
with root:
yield
finally:
self.add_route(name, path, root)
[docs]class FullScreenPage(AbstractLayout):
"""
A layout that takes the whole screen.
:param name: Text for this page's browser tab (required)
:type name: str
:param favicon: Filename of image for this page's browser tab
:type favicon: str
:param on_ready: Function to run on startup
:type on_ready: function
>>> FullScreenPage("Simple Page").start()
"""
def __init__(self, name, favicon=None, on_ready=None):
super().__init__(vuetify.VApp(id="app"), name, favicon, on_ready)
[docs]class SinglePage(FullScreenPage):
"""
A layout that takes the whole screen, adding a |layout_vuetify_link| for a `toolbar`, a VMain as `content` and a VFooter as a `footer`.
.. |layout_vuetify_link| raw:: html
<a href="https://vuetifyjs.com/api/v-app-bar" target="_blank">vuetify app bar</a>
:param name: Text for this page's browser tab (required)
:type name: str
>>> layout = SinglePage("Page with header / app bar")
The toolbar starts with 2 children, a `logo` and a `title` which are accessible at
the root of the layout object.
>>> layout.toolbar.children += ["More stuff to the toolbar"]
>>> layout.logo.children = [VIcon("mdi-menu")]
>>> layout.title.set_text("My Super App")
Then we have `content` and `footer`. Content is by default empty but the footer
has the default trame information regarding its versions and feature feedback
on when the server is busy with a spining progress.
You can quickly hide the footer by calling the following.
>>> layout.footer.hide()
"""
def __init__(self, name, favicon=None, on_ready=None):
super().__init__(name, favicon, on_ready)
self.toolbar = vuetify.VAppBar(app=True)
if os.path.exists(LOGO_PATH):
self.logo = Span(
f'<img height="32px" width="32px" src="{read_file_as_base64_url(LOGO_PATH)}" />',
classes="mr-2",
style="display: flex; align-content: center;",
)
else:
self.logo = vuetify.VIcon("mdi-menu", classes="mr-4")
args = tri.get_cli_parser().parse_known_args()[0]
dev = args.dev if hasattr(args, "dev") else False
self.title = Span("trame app", classes="title")
self.content = vuetify.VMain()
self.toolbar.children += [self.logo, self.title]
self.footer = vuetify.VFooter(
app=True,
classes="my-0 py-0",
children=[
vuetify.VProgressCircular(
indeterminate=("busy",),
background_opacity=1,
bg_color="#01549b",
color="#04a94d",
size=16,
width=3,
classes="ml-n3 mr-1",
),
f'<a href="https://kitware.github.io/trame/" class="grey--text lighten-1--text text-caption text-decoration-none" target="_blank">Powered by trame {tr.__version__}/{pywebvue.__version__}</a>',
vuetify.VSpacer(),
vuetify.VBtn(
vuetify.VIcon("mdi-autorenew", x_small=True),
v_if=("__dev_reload", dev),
x_small=True,
icon=True,
click="trigger('server_reload')",
classes="mx-2",
),
'<a href="https://www.kitware.com/" class="grey--text lighten-1--text text-caption text-decoration-none" target="_blank">© 2021 Kitware Inc.</a>',
],
)
self.children += [self.toolbar, self.content, self.footer]
[docs]class SinglePageWithDrawer(SinglePage):
"""
A layout that takes the whole screen, adding a |layout_vuetify_link| for a toolbar, a content, a drawer, and a footer.
:param name: Text for this page's browser tab (required)
:type name: str
:param show_drawer: Whether the drawer is open. Default True
:type show_drawer: bool
:param width: How many pixels wide the drawer should be
:type width: Number
:param show_drawer_name: The name referencing the drawer's state. Default "drawerOpen".
:type show_drawer_name: str
>>> SinglePageWithDrawer("Page with drawer").start()
"""
def __init__(
self,
name,
favicon=None,
on_ready=None,
show_drawer=True,
width=200,
show_drawer_name="drawerOpen",
):
super().__init__(name, favicon, on_ready)
self.drawer = vuetify.VNavigationDrawer(
app=True,
clipped=True,
stateless=True,
v_model=(show_drawer_name, show_drawer),
width=width,
)
self.toolbar.clipped_left = True
self.children += [self.drawer]
self.logo.click = f"{show_drawer_name} = !{show_drawer_name}"
[docs]def update_layout(layout):
"""
Flush layout to the client
:param layout: UI content for your application
:type layout: str | trame.layouts.*
>>> layout.title.set_text("Workload finished!")
>>> update_layout(layout)
"""
_app = tri.get_app_instance()
_app.layout = layout if isinstance(layout, str) else layout.html