diff --git a/README.md b/README.md index 1c1a871..e0272dd 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,16 @@ This repository contains examples of how to build application functionalities in 2. [Create a JSON editor for a Pydantic model](examples/pydantic_monaco) 3. [Conditionally rendering elements](examples/conditional_rendering) 4. [Conditionally disabling elements](examples/conditional_disabling) -5. [Changing Pydantic rules based on user input](examples/dynamic_pydantic_rules) -6. [Complex Pydantic rules](examples/complex_pydantic_rules) -7. [Selecting datafiles from the server](examples/data_selector) -8. [Running a Galaxy tool](examples/run_galaxy_tool) -9. [Running a Galaxy workflow](examples/run_galaxy_workflow) -10. [Working with Plotly](examples/plotly) -11. [Working with Matplotlib](examples/matplotlib) -12. [Working with VTK](examples/vtk) -13. [Synchronizing changes between tabs](examples/multitab) +5. [Creating a dialog](examples/dialog) +6. [Changing Pydantic rules based on user input](examples/dynamic_pydantic_rules) +7. [Complex Pydantic rules](examples/complex_pydantic_rules) +8. [Selecting datafiles from the server](examples/data_selector) +9. [Running a Galaxy tool](examples/run_galaxy_tool) +10. [Running a Galaxy workflow](examples/run_galaxy_workflow) +11. [Working with Plotly](examples/plotly) +12. [Working with Matplotlib](examples/matplotlib) +13. [Working with VTK](examples/vtk) +14. [Synchronizing changes between tabs](examples/multitab) We also provide examples that take advantage of ORNL resources: diff --git a/examples/complex_pydantic_rules/view.py b/examples/complex_pydantic_rules/view.py index 06197b5..8c2298a 100644 --- a/examples/complex_pydantic_rules/view.py +++ b/examples/complex_pydantic_rules/view.py @@ -4,7 +4,6 @@ from nova.trame import ThemedApp from nova.trame.view.components import InputField from nova.trame.view.layouts import VBoxLayout -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -29,10 +28,9 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600): - with VBoxLayout(columns=3, gap="0.5em"): - InputField(v_model="data.text", type="textarea") - InputField(v_model="data.tokenized_text", readonly=True, type="textarea") + with VBoxLayout(gap="0.5em", stretch=True): + InputField(v_model="data.text", type="textarea") + InputField(v_model="data.tokenized_text", readonly=True, type="textarea") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/conditional_disabling/view.py b/examples/conditional_disabling/view.py index 8b20f19..a286e2b 100644 --- a/examples/conditional_disabling/view.py +++ b/examples/conditional_disabling/view.py @@ -4,7 +4,6 @@ from nova.trame import ThemedApp from nova.trame.view.components import InputField from nova.trame.view.layouts import GridLayout, HBoxLayout -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -29,18 +28,17 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600): - with GridLayout(columns=2, classes="mb-2", gap="0.5em"): - InputField(v_model="data.email_address") - InputField(v_model="data.full_name") - - with HBoxLayout(gap="0.5em", valign="center"): - InputField(v_model="data.disable_phone_field", type="checkbox") - - # Now, we can use disable_phone_field to conditionally disable the phone number field. - # Note that because we need to bind to a parameter that doesn't start with v_ (indicating a - # v-directive in Vue.js), we need to use the Trame tuple syntax to bind to disabled. - InputField(v_model="data.phone_number", disabled=("data.disable_phone_field",)) + with GridLayout(columns=2, classes="mb-2", gap="0.5em"): + InputField(v_model="data.email_address") + InputField(v_model="data.full_name") + + with HBoxLayout(gap="0.5em", valign="center"): + InputField(v_model="data.disable_phone_field", type="checkbox") + + # Now, we can use disable_phone_field to conditionally disable the phone number field. + # Note that because we need to bind to a parameter that doesn't start with v_ (indicating a + # v-directive in Vue.js), we need to use the Trame tuple syntax to bind to disabled. + InputField(v_model="data.phone_number", disabled=("data.disable_phone_field",)) def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/conditional_rendering/view.py b/examples/conditional_rendering/view.py index 116d886..f8776ec 100644 --- a/examples/conditional_rendering/view.py +++ b/examples/conditional_rendering/view.py @@ -30,26 +30,25 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600): - with GridLayout(columns=2, classes="mb-2", gap="0.5em"): - InputField(v_model="data.email_address") - InputField(v_model="data.full_name") - - with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"): - InputField(v_model="data.show_phone_field", type="checkbox") - - # Now, we can use show_phone_field to conditionally render the phone number field. - InputField(v_if="data.show_phone_field", v_model="data.phone_number") - # Following a v_if, we can use v_else_if and v_else. - html.Span("{{ data.full_name }}'s phone number is hidden.", v_else_if="data.full_name") - html.Span("Phone number is hidden.", v_else=True) - - with VBoxLayout(gap="0.5em"): - # We can also use v_show to conditionally render a field. Note that when using v_show, you won't - # be able to use v_else_if or v_else afterwards. There is a deeper discussion of the differences - # between v_if and v_show in the Vue.js documentation: https://vuejs.org/guide/essentials/conditional#v-if-vs-v-show. - vuetify.VBtn("Toggle Comments Field", click=self.view_model.toggle_comments) - InputField(v_show="data.show_comments_field", v_model="data.comments", type="textarea") + with GridLayout(columns=2, classes="mb-2", gap="0.5em"): + InputField(v_model="data.email_address") + InputField(v_model="data.full_name") + + with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"): + InputField(v_model="data.show_phone_field", type="checkbox") + + # Now, we can use show_phone_field to conditionally render the phone number field. + InputField(v_if="data.show_phone_field", v_model="data.phone_number") + # Following a v_if, we can use v_else_if and v_else. + html.Span("{{ data.full_name }}'s phone number is hidden.", v_else_if="data.full_name") + html.Span("Phone number is hidden.", v_else=True) + + with VBoxLayout(gap="0.5em", stretch=True): + # We can also use v_show to conditionally render a field. Note that when using v_show, you won't + # be able to use v_else_if or v_else afterwards. There is a deeper discussion of the differences + # between v_if and v_show in the Vue.js documentation: https://vuejs.org/guide/essentials/conditional#v-if-vs-v-show. + vuetify.VBtn("Toggle Comments Field", click=self.view_model.toggle_comments) + InputField(v_show="data.show_comments_field", v_model="data.comments", type="textarea") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/data_selector/view.py b/examples/data_selector/view.py index be46325..43ef3f5 100644 --- a/examples/data_selector/view.py +++ b/examples/data_selector/view.py @@ -7,7 +7,6 @@ from nova.trame.view.components import DataSelector from nova.trame.view.layouts import VBoxLayout from trame.widgets import html -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -34,17 +33,16 @@ def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): - with VBoxLayout(height=400): - # Please note that this is a dangerous operation. You should ensure that you restrict this - # component to only expose files that are strictly necessary to making your application - # functional. - DataSelector( - v_model="data.selected_files", - directory=os.environ.get("HOME", "/"), - classes="mb-1", - ) - html.Span("You have selected {{ data.selected_files.length }} files.") + with VBoxLayout(stretch=True): + # Please note that this is a dangerous operation. You should ensure that you restrict this + # component to only expose files that are strictly necessary to making your application + # functional. + DataSelector( + v_model="data.selected_files", + directory=os.environ.get("HOME", "/"), + classes="mb-1", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/dialog/main.py b/examples/dialog/main.py new file mode 100644 index 0000000..430ac45 --- /dev/null +++ b/examples/dialog/main.py @@ -0,0 +1,12 @@ +"""Runs the dialog example.""" + +from examples.dialog.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/dialog/view.py b/examples/dialog/view.py new file mode 100644 index 0000000..ba3a406 --- /dev/null +++ b/examples/dialog/view.py @@ -0,0 +1,47 @@ +"""View for dialog example.""" + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import client +from trame.widgets import vuetify3 as vuetify + +from .view_model import ViewModel + + +class App(ThemedApp): + """View for dialog example.""" + + def __init__(self) -> None: + super().__init__() + + self.create_vm() + # If you forget to call connect, then the application will crash when you attempt to update the view. + self.view_model.view_state_bind.connect("view_state") + + self.create_ui() + + def create_ui(self) -> None: + with super().create_ui() as layout: + with layout.content: + with VBoxLayout(halign="center", valign="center", stretch=True): + vuetify.VBtn("Open the Dialog", click=self.view_model.open_dialog) + + # An important note about working with Trame is that it doesn't automatically listen to changes in + # Pydantic fields. While our InputField component handles this automatically, when you are using other + # components to modify fields then you are responsible for ensuring that the field changes are synced to + # the server properly. Fortunately, Trame provides a mechanism for managing this easily via the + # DeepReactive component. + with client.DeepReactive("view_state"): + # VDialog will automatically set view_state.dialog_open to False when the user clicks outside of the + # dialog. Try moving the VDialog outside of the DeepReactive context manager and see how it changes + # the behavior when attempting to close the dialog. + with vuetify.VDialog(v_model="view_state.dialog_open", width=400): + with vuetify.VCard(classes="pa-4"): + vuetify.VCardTitle("Dialog") + vuetify.VCardSubtitle("Click anywhere outside of the dialog to dismiss.") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + self.view_model = ViewModel(binding) diff --git a/examples/dialog/view_model.py b/examples/dialog/view_model.py new file mode 100644 index 0000000..bf9cd86 --- /dev/null +++ b/examples/dialog/view_model.py @@ -0,0 +1,23 @@ +"""View model implementation for dialog example.""" + +from nova.mvvm.interface import BindingInterface +from pydantic import BaseModel, Field + + +class ViewState(BaseModel): + """Pydantic model for holding view state.""" + + dialog_open: bool = Field(default=False) + + +class ViewModel: + """View model implementation for dialog example.""" + + def __init__(self, binding: BindingInterface) -> None: + self.view_state = ViewState() + + self.view_state_bind = binding.new_bind(self.view_state) + + def open_dialog(self) -> None: + self.view_state.dialog_open = True + self.view_state_bind.update_in_view(self.view_state) diff --git a/examples/dynamic_pydantic_rules/view.py b/examples/dynamic_pydantic_rules/view.py index 97b16b6..6b20a6e 100644 --- a/examples/dynamic_pydantic_rules/view.py +++ b/examples/dynamic_pydantic_rules/view.py @@ -4,7 +4,6 @@ from nova.trame import ThemedApp from nova.trame.view.components import InputField from nova.trame.view.layouts import GridLayout -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -29,11 +28,10 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600): - with GridLayout(columns=3, gap="0.5em"): - InputField(v_model="data.min") - InputField(v_model="data.max") - InputField(v_model="data.value") + with GridLayout(columns=3, gap="0.5em"): + InputField(v_model="data.min") + InputField(v_model="data.max") + InputField(v_model="data.value") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/matplotlib/view.py b/examples/matplotlib/view.py index f5a61fd..f9fb29a 100644 --- a/examples/matplotlib/view.py +++ b/examples/matplotlib/view.py @@ -7,7 +7,6 @@ from nova.trame.view.components import InputField from nova.trame.view.components.visualization import MatplotlibFigure from nova.trame.view.layouts import GridLayout, HBoxLayout -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -33,20 +32,15 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard( - classes="d-flex flex-column mx-auto my-4", max_width=1200, style="height: calc(100vh - 120px);" - ): - with GridLayout(classes="mb-4", columns=2, gap="1em"): - InputField("plot_data.data_points") - InputField("plot_data.function", type="select") - - with HBoxLayout(classes="flex-1-0 overflow-hidden"): - # Try increasing the number of data points to something very large and observe the drop in - # performance. Then, set webagg to True and try again and see the difference. webagg is a - # server-side rendering mode for Matplotlib. - self.figure_view = MatplotlibFigure( - classes="h-100 w-100", figure=self.view_model.get_updated_figure(), webagg=False - ) + with GridLayout(classes="mb-4", columns=2, gap="1em"): + InputField("plot_data.data_points") + InputField("plot_data.function", type="select") + + with HBoxLayout(classes="overflow-hidden", stretch=True): + # Try increasing the number of data points to something very large and observe the drop in + # performance. Then, set webagg to True and try again and see the difference. webagg is a + # server-side rendering mode for Matplotlib. + self.figure_view = MatplotlibFigure(figure=self.view_model.get_updated_figure(), webagg=False) def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/multitab/view/input.py b/examples/multitab/view/input.py index 469f8b6..88bb914 100644 --- a/examples/multitab/view/input.py +++ b/examples/multitab/view/input.py @@ -1,5 +1,7 @@ """View for the input tab.""" +from typing import Any + from nova.trame.view.components import InputField from nova.trame.view.layouts import GridLayout @@ -7,11 +9,11 @@ class InputTab: """View for the input tab.""" - def __init__(self) -> None: - self.create_ui() + def __init__(self, **kwargs: Any) -> None: + self.create_ui(**kwargs) - def create_ui(self) -> None: - with GridLayout(columns=3, classes="mb-4", gap="0.5em"): + def create_ui(self, **kwargs: Any) -> None: + with GridLayout(columns=3, classes="mb-4", gap="0.5em", **kwargs): InputField("inputs.count") InputField("inputs.min") InputField("inputs.max") diff --git a/examples/multitab/view/main_view.py b/examples/multitab/view/main_view.py index 0c2e503..e403c1c 100644 --- a/examples/multitab/view/main_view.py +++ b/examples/multitab/view/main_view.py @@ -2,6 +2,7 @@ from nova.mvvm.trame_binding import TrameBinding from nova.trame import ThemedApp +from trame.widgets import client from trame.widgets import vuetify3 as vuetify from ..model import InputModel, StatsModel @@ -28,19 +29,14 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.pre_content: - with vuetify.VTabs( - v_model="view_state.active_tab", classes="pl-8", update_modelValue="flushState('view_state')" - ): - vuetify.VTab("Input", value=0) - vuetify.VTab("Statistics", value=1) + with client.DeepReactive("view_state"): + with vuetify.VTabs(v_model="view_state.active_tab", classes="pl-8"): + vuetify.VTab("Input", value=0) + vuetify.VTab("Statistics", value=1) with layout.content: - with vuetify.VCard(classes="d-flex flex-column", style="height: calc(100vh - 150px);"): - with vuetify.VWindow(v_model="view_state.active_tab"): - with vuetify.VWindowItem(value=0): - InputTab() - with vuetify.VWindowItem(value=1): - StatsTab() + InputTab(v_if="view_state.active_tab == 0") + StatsTab(v_if="view_state.active_tab == 1") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/multitab/view/stats.py b/examples/multitab/view/stats.py index 4a41e4b..61cd9d0 100644 --- a/examples/multitab/view/stats.py +++ b/examples/multitab/view/stats.py @@ -1,23 +1,26 @@ """View for the stats tab.""" +from typing import Any + from nova.trame.view.components import InputField -from nova.trame.view.layouts import GridLayout +from nova.trame.view.layouts import GridLayout, VBoxLayout from trame.widgets import html class StatsTab: """View for the stats tab.""" - def __init__(self) -> None: - self.create_ui() + def __init__(self, **kwargs: Any) -> None: + self.create_ui(**kwargs) - def create_ui(self) -> None: - html.P("Generated List: {{ stats.values }}", classes="mb-4") + def create_ui(self, **kwargs: Any) -> None: + with VBoxLayout(**kwargs): + html.P("Generated List: {{ stats.values }}", classes="mb-4") - with GridLayout(columns=3, gap="0.5em"): - InputField("stats.average", readonly=True) - InputField("stats.mean", readonly=True) - InputField("stats.median", readonly=True) - InputField("stats.quantile", readonly=True) - InputField("stats.std", readonly=True) - InputField("stats.variance", readonly=True) + with GridLayout(columns=3, gap="0.5em"): + InputField("stats.average", readonly=True) + InputField("stats.mean", readonly=True) + InputField("stats.median", readonly=True) + InputField("stats.quantile", readonly=True) + InputField("stats.std", readonly=True) + InputField("stats.variance", readonly=True) diff --git a/examples/ornl/neutron_data_selector/view.py b/examples/ornl/neutron_data_selector/view.py index 7080fcb..4d2f931 100644 --- a/examples/ornl/neutron_data_selector/view.py +++ b/examples/ornl/neutron_data_selector/view.py @@ -5,7 +5,6 @@ from nova.trame.view.components.ornl import NeutronDataSelector from nova.trame.view.layouts import VBoxLayout from trame.widgets import html -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -32,20 +31,19 @@ def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): - with VBoxLayout(height=400): - # Please note that this example will not work locally if /HFIR and /SNS don't exist on your - # development machine. You can either simulate or try to mount them depending on your - # circumstances. - NeutronDataSelector( - v_model="data.selected_files", - base_paths=["/HFIR", "/SNS"], - classes="mb-1", - # You can uncomment the below lines to restrict data selection to a specific instrument. - # facility="SNS", - # instrument="TOPAZ", - ) - html.Span("You have selected {{ data.selected_files.length }} files.") + with VBoxLayout(stretch=True): + # Please note that this example will not work locally if /HFIR and /SNS don't exist on your + # development machine. You can either simulate or try to mount them depending on your + # circumstances. + NeutronDataSelector( + v_model="data.selected_files", + base_paths=["/HFIR", "/SNS"], + classes="mb-1", + # You can uncomment the below lines to restrict data selection to a specific instrument. + # facility="SNS", + # instrument="TOPAZ", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/ornl/oncat/view.py b/examples/ornl/oncat/view.py index f3efaba..0ea1049 100644 --- a/examples/ornl/oncat/view.py +++ b/examples/ornl/oncat/view.py @@ -5,7 +5,6 @@ from nova.trame.view.components.ornl import NeutronDataSelector from nova.trame.view.layouts import VBoxLayout from trame.widgets import html -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -32,28 +31,27 @@ def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): - with VBoxLayout(height=400): - # You must set the ONCAT_CLIENT_ID and ONCAT_CLIENT_SECRET environment variables for this to - # work locally. If you don't know how to set them, then please visit https://oncat.ornl.gov/ and - # review the documentation or reach out to ONCat support for assistance. When this runs in - # production, we will set the environment for you automatically. - NeutronDataSelector( - v_model="data.selected_files", - base_paths=["/HFIR", "/SNS"], - classes="mb-1", - # This triggers the NeutronDataSelector to pull data from ONCat. - data_source="oncat", - # ONCat by default will return a large amount of metadata for each file. This can be - # extremely slow, so we strongly recommend setting the projection attribute to restrict the - # metadata to only what you need. This parameter matches the ONCat projection parameter - # exactly. - projection=["indexed.run_number"], - # You can uncomment the below lines to restrict data selection to a specific instrument. - # facility="SNS", - # instrument="TOPAZ", - ) - html.Span("You have selected {{ data.selected_files.length }} files.") + with VBoxLayout(stretch=True): + # You must set the ONCAT_CLIENT_ID and ONCAT_CLIENT_SECRET environment variables for this to + # work locally. If you don't know how to set them, then please visit https://oncat.ornl.gov/ and + # review the documentation or reach out to ONCat support for assistance. When this runs in + # production, we will set the environment for you automatically. + NeutronDataSelector( + v_model="data.selected_files", + base_paths=["/HFIR", "/SNS"], + classes="mb-1", + # This triggers the NeutronDataSelector to pull data from ONCat. + data_source="oncat", + # ONCat by default will return a large amount of metadata for each file. This can be + # extremely slow, so we strongly recommend setting the projection attribute to restrict the + # metadata to only what you need. This parameter matches the ONCat projection parameter + # exactly. + projection=["indexed.run_number"], + # You can uncomment the below lines to restrict data selection to a specific instrument. + # facility="SNS", + # instrument="TOPAZ", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/plotly/view.py b/examples/plotly/view.py index 3ca02b9..81b8351 100644 --- a/examples/plotly/view.py +++ b/examples/plotly/view.py @@ -5,9 +5,8 @@ from nova.mvvm.trame_binding import TrameBinding from nova.trame import ThemedApp from nova.trame.view.components import InputField -from nova.trame.view.layouts import GridLayout, HBoxLayout +from nova.trame.view.layouts import GridLayout from trame.widgets import plotly -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -33,17 +32,11 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard( - classes="d-flex flex-column mx-auto my-4", max_width=1200, style="height: calc(100vh - 120px);" - ): - with GridLayout(classes="mb-4", columns=2, gap="1em"): - InputField("plot_data.data_points") - InputField("plot_data.function", type="select") - - with HBoxLayout(classes="flex-1-0 overflow-hidden"): - self.figure_view = plotly.Figure( - classes="h-100 w-100", figure=self.view_model.get_updated_figure() - ) + with GridLayout(classes="mb-4", columns=2, gap="1em"): + InputField("plot_data.data_points") + InputField("plot_data.function", type="select") + + self.figure_view = plotly.Figure(figure=self.view_model.get_updated_figure()) def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/pydantic_form/view.py b/examples/pydantic_form/view.py index 05f6125..7a40a71 100644 --- a/examples/pydantic_form/view.py +++ b/examples/pydantic_form/view.py @@ -32,34 +32,33 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600): - with GridLayout(columns=2, gap="0.5em"): - # The GUI must bind the Pydantic model fields to elements. - # Most bindings use the tuple syntax: (parameter_name, default_value). - # If you don't have a default value, then you can use: (parameter_name,). - InputField(v_model=("data.user_name",)) + with GridLayout(columns=2, gap="0.5em"): + # The GUI must bind the Pydantic model fields to elements. + # Most bindings use the tuple syntax: (parameter_name, default_value). + # If you don't have a default value, then you can use: (parameter_name,). + InputField(v_model=("data.user_name",)) - # v_model allows you to just pass the parameter_name as a string. - # This typically does not work when binding other parameters. - InputField(v_model="data.domain_name") + # v_model allows you to just pass the parameter_name as a string. + # This typically does not work when binding other parameters. + InputField(v_model="data.domain_name") - # Try just passing a string to model_value, and see what happens here! - InputField( - model_value=("data.email",), column_span=2, label="Computed Email Address", readonly=True - ) + # Try just passing a string to model_value, and see what happens here! + InputField( + model_value=("data.email",), column_span=2, label="Computed Email Address", readonly=True + ) - # If you need to trigger some Python code on an event, then you can call view model functions - # directly. - vuetify.VBtn( - "Submit", - column_span=2, - disabled=("state.submit_disabled",), - click=self.view_model.submit, - ) + # If you need to trigger some Python code on an event, then you can call view model functions + # directly. + vuetify.VBtn( + "Submit", + column_span=2, + disabled=("state.submit_disabled",), + click=self.view_model.submit, + ) - # This is less common, but you can use handlebars expressions to display the form data inside of - # a string. - html.Span("You've submitted the following email address: {{ data.email }}", v_if="state.submitted") + # This is less common, but you can use handlebars expressions to display the form data inside of + # a string. + html.Span("You've submitted the following email address: {{ data.email }}", v_if="state.submitted") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/pydantic_monaco/view.py b/examples/pydantic_monaco/view.py index 2ed90cd..59547b3 100644 --- a/examples/pydantic_monaco/view.py +++ b/examples/pydantic_monaco/view.py @@ -3,8 +3,7 @@ from nova.mvvm.trame_binding import TrameBinding from nova.trame import ThemedApp from nova.trame.view.layouts import VBoxLayout -from trame.widgets import code, html -from trame.widgets import vuetify3 as vuetify +from trame.widgets import client, code, html from .model import Model from .view_model import ViewModel @@ -26,24 +25,38 @@ def __init__(self) -> None: self.view_model.update_form_data() self.view_model.update_form_state() + # The Monaco editor in Trame will not automatically scale to the size of its container. Since we typically want + # behavior, you can add it with the following JavaScript. + # This performs the scaling. + self.scale_editor = "window.trame.refs.monaco_container.editor.layout()" + # This instructs the browser to perform the scaling once immediately and again every time the browser is + # resized. The setTimeout call delays the first scaling to give the editor time to initialize. + self.start_scaling = ( + "window.setTimeout(() => {" + f" {self.scale_editor};" + f" window.addEventListener('resize', () => {{ {self.scale_editor} }});" + "}, 100);" + ) + self.create_ui() def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4", max_width=600, style="height: calc(100vh - 120px);"): - with VBoxLayout(halign="center", height="100%"): - # If a component doesn't support the v_model parameter, as with the Monaco editor, then we can - # set the initial value and listen to the events manually. - code.Editor( - model_value=("data",), - classes="h-75", - language="json", - theme="vs-dark", - input=(self.view_model.on_input, "[$event]"), - ) - - html.P("{{ error }}", v_for="error in state.errors") + with VBoxLayout(stretch=True): + # If a component doesn't support the v_model parameter, as with the Monaco editor, then we can + # set the initial value and listen to the events manually. + code.Editor( + ref="monaco_container", + model_value=("data",), + language="json", + theme="vs-dark", + input=(self.view_model.on_input, "[$event]"), + ) + # This injects our JavaScript snippet when the browser loads this content. + client.ClientTriggers(mounted=self.start_scaling) + + html.P("{{ error }}", v_for="error in state.errors") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/run_galaxy_tool/view.py b/examples/run_galaxy_tool/view.py index 49bc7e4..c07d424 100644 --- a/examples/run_galaxy_tool/view.py +++ b/examples/run_galaxy_tool/view.py @@ -5,7 +5,6 @@ from nova.trame.view.components import ExecutionButtons, InputField, ProgressBar, ToolOutputWindows from nova.trame.view.layouts import VBoxLayout from trame.widgets import html -from trame.widgets import vuetify3 as vuetify from .model import Model from .view_model import ViewModel @@ -31,30 +30,29 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): - with VBoxLayout(gap="0.5em"): - InputField(v_model="data.fractal_type", type="select") + with VBoxLayout(gap="0.5em"): + InputField(v_model="data.fractal_type", type="select") - # These components add UI components for monitoring the job automatically. - # The "fractals" string needs to be consistent with the ToolRunner in the view model. - ProgressBar("fractals") - ToolOutputWindows("fractals") + # These components add UI components for monitoring the job automatically. + # The "fractals" string needs to be consistent with the ToolRunner in the view model. + ProgressBar("fractals") + ToolOutputWindows("fractals") - with html.Div(v_if="stats.count.length > 0"): - html.P( - "PIL computed {{ stats.count.length }} bands for the generated fractal image.", - classes="mb-2", - ) - html.P( - ( - "Band {{ index + 1 }} Stats: " - "Count={{ stats.count[index] }}, " - "Extrema={{ stats.extrema[index] }}, " - "Mean={{ stats.mean[index] }}, " - "Median={{ stats.median[index] }}" - ), - v_for="(_, index) in stats.count.length", - ) + with html.Div(v_if="stats.count.length > 0"): + html.P( + "PIL computed {{ stats.count.length }} bands for the generated fractal image.", + classes="mb-2", + ) + html.P( + ( + "Band {{ index + 1 }} Stats: " + "Count={{ stats.count[index] }}, " + "Extrema={{ stats.extrema[index] }}, " + "Mean={{ stats.mean[index] }}, " + "Median={{ stats.median[index] }}" + ), + v_for="(_, index) in stats.count.length", + ) with layout.post_content: # This adds UI components for running and stopping the tool. diff --git a/examples/run_galaxy_workflow/view.py b/examples/run_galaxy_workflow/view.py index cf2d8d3..a245d33 100644 --- a/examples/run_galaxy_workflow/view.py +++ b/examples/run_galaxy_workflow/view.py @@ -31,16 +31,13 @@ def __init__(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): - with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"): - InputField(v_model="data.input_path") - with vuetify.VBtn( - disabled=("state.running || state.errors",), click=self.view_model.run_workflow - ): - vuetify.VProgressCircular(v_if="state.running", indeterminate=True, size=16) - html.Span("Run Workflow", v_else=True) - - html.Span("Workflow Completed!", v_if="data.complete") + with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"): + InputField(v_model="data.input_path") + with vuetify.VBtn(disabled=("state.running || state.errors",), click=self.view_model.run_workflow): + vuetify.VProgressCircular(v_if="state.running", indeterminate=True, size=16) + html.Span("Run Workflow", v_else=True) + + html.Span("Workflow Completed!", v_if="data.complete") def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/examples/vtk/view.py b/examples/vtk/view.py index fec329d..abafd92 100644 --- a/examples/vtk/view.py +++ b/examples/vtk/view.py @@ -5,7 +5,6 @@ from nova.trame import ThemedApp from nova.trame.view.layouts import VBoxLayout from trame.widgets import vtk -from trame.widgets import vuetify3 as vuetify # VTK factory initialization from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa @@ -43,13 +42,10 @@ def create_renderer(self) -> None: def create_ui(self) -> None: with super().create_ui() as layout: with layout.content: - with vuetify.VCard( - classes="d-flex flex-column mx-auto my-4", max_width=1200, style="height: calc(100vh - 120px);" - ): - with VBoxLayout(height="100%", gap="0.5em"): - # Choosing vtkRemoteView will enforce server-side rendering. We strongly recommend this for - # anything non-trivial in size. - vtk.VtkRemoteView(self.render_window) + with VBoxLayout(gap="0.5em", stretch=True): + # Choosing vtkRemoteView will enforce server-side rendering. We strongly recommend this for + # anything non-trivial in size. + vtk.VtkRemoteView(self.render_window) def create_vm(self) -> None: binding = TrameBinding(self.state) diff --git a/poetry.lock b/poetry.lock index 73f79c4..1024cae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1594,13 +1594,13 @@ pyqt6 = ["pyqt6"] [[package]] name = "nova-trame" -version = "0.27.0" +version = "1.0.1" description = "A Python Package for injecting curated themes and custom components into Trame applications" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "nova_trame-0.27.0-py3-none-any.whl", hash = "sha256:b8a3884141029504a81f5d6b3b2d5cd5c30ff1169bf166334c5dec7e96c8dd8b"}, + {file = "nova_trame-1.0.1-py3-none-any.whl", hash = "sha256:3ba9a8d0ba93ee2e2e0e21d0460733ac9316dec73d389e74b9de80c5ebb1c231"}, ] [package.dependencies]