From 1f059f990715b3023142a3bbf2a9f5ae72d8b98c Mon Sep 17 00:00:00 2001 From: Sebastien Jourdain Date: Fri, 13 Sep 2024 14:34:06 -0600 Subject: [PATCH] fix(ui): use class components --- src/nrtk_explorer/app/core.py | 11 +- src/nrtk_explorer/app/ui/__init__.py | 8 +- src/nrtk_explorer/app/ui/collapsible_card.py | 54 ++- src/nrtk_explorer/app/ui/layout.py | 333 +++++++++---------- 4 files changed, 191 insertions(+), 215 deletions(-) diff --git a/src/nrtk_explorer/app/core.py b/src/nrtk_explorer/app/core.py index a7cb4920..76dda03d 100644 --- a/src/nrtk_explorer/app/core.py +++ b/src/nrtk_explorer/app/core.py @@ -24,8 +24,6 @@ html.Template.slot_names.add("before") html.Template.slot_names.add("after") -HORIZONTAL_SPLIT_DEFAULT_VALUE = 17 -VERTICAL_SPLIT_DEFAULT_VALUE = 40 DIR_NAME = os.path.dirname(nrtk_explorer.test_data.__file__) DEFAULT_DATASETS = [ @@ -55,10 +53,6 @@ def __init__(self, server=None): self.ctrl.get_image_fpath = lambda i: get_image_fpath(i, self.state.current_dataset) - self.state.horizontal_split = HORIZONTAL_SPLIT_DEFAULT_VALUE - self.state.vertical_split = VERTICAL_SPLIT_DEFAULT_VALUE - self.state.client_only("horizontal_split", "vertical_split") - self._transforms_app = TransformsApp(server=self.server.create_child_server()) self._embeddings_app = EmbeddingsApp( @@ -76,9 +70,6 @@ def __init__(self, server=None): self._transforms_app.set_on_hover(self._embeddings_app.on_image_hovered) self._filtering_app.set_on_apply_filter(self.on_filter_apply) - # Set state variable - self.state.trame__title = "nrtk_explorer" - # Bind instance methods to controller self.ctrl.on_server_reload = self._build_ui self.ctrl.add("on_server_ready")(self.on_server_ready) @@ -160,7 +151,7 @@ def _build_ui(self): ui.reload(ui) extra_args["reload"] = self._build_ui - self.ui = ui.build_layout( + self.ui = ui.NrtkExplorerLayout( server=self.server, dataset_paths=self.input_paths, embeddings_app=self._embeddings_app, diff --git a/src/nrtk_explorer/app/ui/__init__.py b/src/nrtk_explorer/app/ui/__init__.py index 3a2ba4e6..7aa1a7c6 100644 --- a/src/nrtk_explorer/app/ui/__init__.py +++ b/src/nrtk_explorer/app/ui/__init__.py @@ -1,6 +1,6 @@ -from .layout import build_layout +from .layout import NrtkExplorerLayout from .image_list import ImageList -from .collapsible_card import card +from .collapsible_card import CollapsibleCard def reload(m=None): @@ -14,7 +14,7 @@ def reload(m=None): __all__ = [ - "build_layout", + "NrtkExplorerLayout", "ImageList", - "card", + "CollapsibleCard", ] diff --git a/src/nrtk_explorer/app/ui/collapsible_card.py b/src/nrtk_explorer/app/ui/collapsible_card.py index f429fa4c..6a2bc53f 100644 --- a/src/nrtk_explorer/app/ui/collapsible_card.py +++ b/src/nrtk_explorer/app/ui/collapsible_card.py @@ -1,37 +1,31 @@ from trame.widgets import quasar from trame.widgets import html -from trame.app import get_server -state = get_server().state -card_count = 0 +class CollapsibleCard(quasar.QCard): + id_count = 0 -def get_card_open_key(): - global card_count - card_count += 1 - key = f"is_card_open_{card_count}" - state[key] = True - state.client_only(key) - return key + def __init__(self, name=None, collapsed=False, **kwargs): + super().__init__(**kwargs) + if name is None: + CollapsibleCard.id_count += 1 + name = f"is_card_open_{CollapsibleCard.id_count}" + self.state.client_only(name) # keep it local if not provided -def card(open_key=None): - key = open_key if open_key is not None else get_card_open_key() - with quasar.QCard(): - with quasar.QCardSection(): - with html.Div(classes="row items-center no-wrap"): - title_slot = html.Div(classes="col") - with html.Div(classes="col-auto"): - quasar.QBtn( - round=True, - flat=True, - dense=True, - click=f"{key} = !{key}", - icon=(f"{key} ? 'keyboard_arrow_up' : 'keyboard_arrow_down'",), - ) - with quasar.QSlideTransition(): - with html.Div(v_show=f"{key}"): - content_slot = quasar.QCardSection() - actions_slot = quasar.QCardActions(align="right") - - return title_slot, content_slot, actions_slot + with self: + with quasar.QCardSection(): + with html.Div(classes="row items-center no-wrap"): + self.slot_title = html.Div(classes="col") + with html.Div(classes="col-auto"): + quasar.QBtn( + round=True, + flat=True, + dense=True, + click=f"{name} = !{name}", + icon=(f"{name} ? 'keyboard_arrow_up' : 'keyboard_arrow_down'",), + ) + with quasar.QSlideTransition(): + with html.Div(v_show=(name, not collapsed)): + self.slot_content = quasar.QCardSection() + self.slot_actions = quasar.QCardActions(align="right") diff --git a/src/nrtk_explorer/app/ui/layout.py b/src/nrtk_explorer/app/ui/layout.py index c4f32589..4bfe74ee 100644 --- a/src/nrtk_explorer/app/ui/layout.py +++ b/src/nrtk_explorer/app/ui/layout.py @@ -4,182 +4,173 @@ from trame.widgets import html from nrtk_explorer.app import ui - -def toolbar(reload=None): - with quasar.QHeader(): - with quasar.QToolbar(classes="shadow-4"): - quasar.QToolbarTitle("NRTK_EXPLORER") - if reload: - quasar.QBtn( - "Reload", - click=(reload,), - flat=True, - ) +HORIZONTAL_SPLIT_DEFAULT_VALUE = 17 +VERTICAL_SPLIT_DEFAULT_VALUE = 40 def parse_dataset_dirs(datasets): return [{"label": Path(ds).name, "value": ds} for ds in datasets] -def parameters(dataset_paths=[], embeddings_app=None, filtering_app=None, transforms_app=None): - with html.Div(classes="q-pa-md q-gutter-md"): - ( - dataset_title_slot, - dataset_content_slot, - _, - ) = ui.card() - - with dataset_title_slot: - html.Span("Dataset", classes="text-h6") - - with dataset_content_slot: - quasar.QSelect( - label="Dataset", - v_model=("current_dataset",), - options=(parse_dataset_dirs(dataset_paths),), - filled=True, - emit_value=True, - map_options=True, - dense=True, - ) - quasar.QSlider( - v_model=("num_images", 15), - min=(0,), - max=("num_images_max", 25), - disable=("num_images_disabled", True), - step=(1,), - ) - html.P( - "{{num_images}}/{{num_images_max}} images", - classes="text-caption text-center", - ) - - quasar.QToggle( - v_model=("random_sampling", False), - dense=False, - label="Random sampling", - ) - - ( - embeddings_title_slot, - embeddings_content_slot, - embeddings_actions_slot, - ) = ui.card() - - with embeddings_title_slot: - html.Span("Embeddings", classes="text-h6") - - with embeddings_content_slot: - embeddings_app.settings_widget() - - with embeddings_actions_slot: - embeddings_app.compute_ui() - - (annotations_title_slot, annotations_content_slot, _) = ui.card() - - with annotations_title_slot: - quasar.QToggle(v_model=("annotations_enabled_switch", False)) - html.Span("Annotations", classes="text-h6") - - with annotations_content_slot: - quasar.QSelect( - label="Object detection Model", - v_model=("object_detection_model", "facebook/detr-resnet-50"), - options=( - [ - { - "label": "facebook/detr-resnet-50", - "value": "facebook/detr-resnet-50", - }, - ], - ), - filled=True, - emit_value=True, - map_options=True, - ) - - ( - transforms_title_slot, - transforms_content_slot, - transforms_actions_slot, - ) = ui.card() - - with transforms_title_slot: - quasar.QToggle(v_model=("transform_enabled_switch", False)) - html.Span("Transform", classes="text-h6") - - with transforms_content_slot: - transforms_app.settings_widget() - - with transforms_actions_slot: - transforms_app.apply_ui() - - filter_title_slot, filter_content_slot, filter_actions_slot = ui.card() - - with filter_title_slot: - html.Span("Category Filter", classes="text-h6") - - with filter_content_slot: - filtering_app.filter_operator_ui() - filtering_app.filter_options_ui() - - with filter_actions_slot: - filtering_app.filter_apply_ui() - - -def dataset_view( - embeddings_app=None, - transforms_app=None, -): - with quasar.QSplitter( - v_model=("vertical_split",), - limits=("[0,100]",), - horizontal=True, - classes="inherit-height zero-height", +class NrtkDrawer(html.Div): + def __init__( + self, dataset_paths=[], embeddings_app=None, filtering_app=None, transforms_app=None ): - with html.Template(v_slot_before=True): - embeddings_app.visualization_widget() - - with html.Template(v_slot_after=True): - transforms_app.dataset_widget() - - -def explorer( - dataset_paths=[], - embeddings_app=None, - filtering_app=None, - transforms_app=None, -): - with quasar.QSplitter( - model_value=("horizontal_split",), - classes="inherit-height", - before_class="inherit-height zero-height scroll", - after_class="inherit-height zero-height", + super().__init__(classes="q-pa-md q-gutter-md") + + with self: + # DataSet card + with ui.CollapsibleCard() as card: + with card.slot_title: + html.Span("Dataset", classes="text-h6") + with card.slot_content: + quasar.QSelect( + label="Dataset", + v_model=("current_dataset",), + options=(parse_dataset_dirs(dataset_paths),), + filled=True, + emit_value=True, + map_options=True, + dense=True, + ) + quasar.QSlider( + v_model=("num_images", 15), + min=(0,), + max=("num_images_max", 25), + disable=("num_images_disabled", True), + step=(1,), + ) + html.P( + "{{num_images}}/{{num_images_max}} images", + classes="text-caption text-center", + ) + quasar.QToggle( + v_model=("random_sampling", False), + dense=False, + label="Random sampling", + ) + + # Embeddings + with ui.CollapsibleCard() as card: + with card.slot_title: + html.Span("Embeddings", classes="text-h6") + with card.slot_content: + embeddings_app.settings_widget() + with card.slot_actions: + embeddings_app.compute_ui() + + # Annotations + with ui.CollapsibleCard() as card: + with card.slot_title: + quasar.QToggle(v_model=("annotations_enabled_switch", False)) + html.Span("Annotations", classes="text-h6") + with card.slot_content: + quasar.QSelect( + label="Object detection Model", + v_model=("object_detection_model", "facebook/detr-resnet-50"), + options=( + [ + { + "label": "facebook/detr-resnet-50", + "value": "facebook/detr-resnet-50", + }, + ], + ), + filled=True, + emit_value=True, + map_options=True, + ) + + # Transforms + with ui.CollapsibleCard() as card: + with card.slot_title: + quasar.QToggle(v_model=("transform_enabled_switch", False)) + html.Span("Transform", classes="text-h6") + with card.slot_content: + transforms_app.settings_widget() + with card.slot_actions: + transforms_app.apply_ui() + + # Filters + with ui.CollapsibleCard() as card: + with card.slot_title: + html.Span("Category Filter", classes="text-h6") + with card.slot_content: + filtering_app.filter_operator_ui() + filtering_app.filter_options_ui() + with card.slot_actions: + filtering_app.filter_apply_ui() + + +class Spliter(quasar.QSplitter): + def __init__(self, **kwargs): + super().__init__(**kwargs) + with self: + self.slot_before = html.Template(raw_attrs=["v-slot:before"]) + self.slot_after = html.Template(raw_attrs=["v-slot:after"]) + + +class NrtkToolbar(quasar.QHeader): + def __init__(self, reload=None): + super().__init__() + with self: + with quasar.QToolbar(classes="shadow-4"): + quasar.QToolbarTitle("NRTK Explorer") + if reload: + quasar.QBtn( + "Reload", + click=(reload,), + flat=True, + ) + quasar.QSpinnerBox( + v_show="trame__busy", + size="2rem", + ) + + +class NrtkExplorerLayout(QLayout): + def __init__( + self, + server, + reload=None, + dataset_paths=None, + embeddings_app=None, + filtering_app=None, + transforms_app=None, + **kwargs, ): - with html.Template(v_slot_before=True): - parameters( - dataset_paths=dataset_paths, - embeddings_app=embeddings_app, - filtering_app=filtering_app, - transforms_app=transforms_app, - ) - - with html.Template(v_slot_after=True): - dataset_view(embeddings_app=embeddings_app, transforms_app=transforms_app) - - -def build_layout( - server=None, - reload=None, - **kwargs, -): - with QLayout( - server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2" - ) as layout: - toolbar(reload=reload) - - with quasar.QPageContainer(): - with quasar.QPage(): - explorer(**kwargs) - - return layout + super().__init__(server, view="lhh LpR lff", classes="shadow-2 rounded-borders bg-grey-2") + + # Make local variables on state + self.state.client_only("horizontal_split", "vertical_split") + self.state.trame__title = "NRTK Explorer" + + with self: + NrtkToolbar(reload=reload) + with quasar.QPageContainer(): + with quasar.QPage(): + with Spliter( + model_value=("horizontal_split", HORIZONTAL_SPLIT_DEFAULT_VALUE), + classes="inherit-height", + before_class="inherit-height zero-height scroll", + after_class="inherit-height zero-height", + ) as split_drawer_main: + with split_drawer_main.slot_before: + NrtkDrawer( + dataset_paths=dataset_paths, + embeddings_app=embeddings_app, + filtering_app=filtering_app, + transforms_app=transforms_app, + ) + with split_drawer_main.slot_after: + with Spliter( + v_model=("vertical_split", VERTICAL_SPLIT_DEFAULT_VALUE), + limits=("[0,100]",), + horizontal=True, + classes="inherit-height zero-height", + ) as split_scatter_table: + with split_scatter_table.slot_before: + embeddings_app.visualization_widget() + + with split_scatter_table.slot_after: + transforms_app.dataset_widget()