Source code for trame.layouts.core

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