diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 2380173a..df8c6e52 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -4,8 +4,6 @@ This module contains the base ``ScanWorkflow`` class that all workflows should s as well as specific workflows. """ -from __future__ import annotations - import os from typing import ( Generic, @@ -41,7 +39,12 @@ from openflexure_microscope_server.things.background_detect import ( from openflexure_microscope_server.things.camera import BaseCamera, CaptureParams from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.stage import BaseStage -from openflexure_microscope_server.ui import PropertyControl, property_control_for +from openflexure_microscope_server.ui import ( + UI_ELEMENT_RESPONSE, + Accordion, + UIElementList, + property_control_for, +) SettingModelType = TypeVar("SettingModelType", bound=BaseModel) @@ -160,9 +163,9 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing): return True, focus_height - @lt.property - def settings_ui(self) -> list[PropertyControl]: - """A list of PropertyControl objects to create the settings in the scan tab.""" + @lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE) + def settings_ui(self) -> UIElementList: + """Return the UI for the workflow's settings in the scan tab.""" raise NotImplementedError( "Each scan workflow must implement a settings_ui method." ) @@ -556,35 +559,43 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): return imaged, focus_height - @lt.property - def settings_ui(self) -> list[PropertyControl]: - """A list of PropertyControl objects to create the settings in the scan tab.""" - return [ - property_control_for( - self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05 - ), - property_control_for( - self, "stack_images_to_save", label="Images in Stack to Save" - ), - property_control_for( - self, - "stack_min_images_to_test", - label="Minimum number of images to test for focus", - ), - property_control_for(self, "stack_dz", label="Stack dz (steps)", step=5), - property_control_for( - self, "autofocus_dz", label="Autofocus Range (steps)", step=200 - ), - property_control_for( - self, "max_range", label="Maximum Distance (steps)", step=1000 - ), - property_control_for( - self, "skip_background", label="Detect and Skip Empty Fields" - ), - property_control_for( - self, "equal_distances", label="Set Equal x and y Distances" - ), - ] + @lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE) + def settings_ui(self) -> UIElementList: + """Return the UI for the workflow's settings in the scan tab.""" + return UIElementList( + [ + Accordion( + title="Background Detect", + children=self._background_detector.settings_ui, + ), + property_control_for( + self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05 + ), + property_control_for( + self, "stack_images_to_save", label="Images in Stack to Save" + ), + property_control_for( + self, + "stack_min_images_to_test", + label="Minimum number of images to test for focus", + ), + property_control_for( + self, "stack_dz", label="Stack dz (steps)", step=5 + ).model_dump(), + property_control_for( + self, "autofocus_dz", label="Autofocus Range (steps)", step=200 + ), + property_control_for( + self, "max_range", label="Maximum Distance (steps)", step=1000 + ), + property_control_for( + self, "skip_background", label="Detect and Skip Empty Fields" + ), + property_control_for( + self, "equal_distances", label="Set Equal x and y Distances" + ), + ] + ) class RegularGridSettingsModel(RectGridSettingsModel): @@ -668,17 +679,21 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]): save_resolution=settings.capture_params.save_resolution, ) - @lt.property - def settings_ui(self) -> list[PropertyControl]: - """A list of PropertyControl objects to create the settings in the scan tab.""" - return [ - property_control_for( - self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05 - ), - property_control_for(self, "x_count", label="Number of columns"), - property_control_for(self, "y_count", label="Number of rows"), - property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"), - ] + @lt.endpoint("get", "settings_ui", responses=UI_ELEMENT_RESPONSE) + def settings_ui(self) -> UIElementList: + """Return the UI for the workflow's settings in the scan tab.""" + return UIElementList( + [ + property_control_for( + self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05 + ), + property_control_for(self, "x_count", label="Number of columns"), + property_control_for(self, "y_count", label="Number of rows"), + property_control_for( + self, "autofocus_dz", label="Autofocus Range (steps)" + ), + ] + ) class SnakeWorkflow(RegularGridWorkflow): diff --git a/src/openflexure_microscope_server/ui.py b/src/openflexure_microscope_server/ui.py index f24726d3..eb5051f1 100644 --- a/src/openflexure_microscope_server/ui.py +++ b/src/openflexure_microscope_server/ui.py @@ -4,7 +4,7 @@ from html import escape from html.parser import HTMLParser from typing import Annotated, Any, Literal, Optional -from pydantic import BaseModel, BeforeValidator +from pydantic import BaseModel, BeforeValidator, RootModel import labthings_fastapi as lt @@ -268,6 +268,28 @@ class Container(BaseModel): UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container +class UIElementList(RootModel[list[UIElementModels]]): + """A list of user interface elements. + + Note! Two important things to consider: + + * This cannot be the return of a lt.property. To fully describe the UI recursion + is used. Web of Things DataSchema doesn't support rectursion. Use a + lt.endpoint instead + * If the module using this class imoports future annotations then this will cause + havoc with pydantics forward references. + """ + + # Resolve forward references so Pydantic can build a sensible recursive schema Accordion.model_rebuild() Container.model_rebuild() +UIElementList.model_rebuild() + +# This constant can be used as the value for the `responses` argument of lt.endpoint. +UI_ELEMENT_RESPONSE = { + 200: { + "description": "A list of UI element specifications.", + "model": UIElementList, + } +} diff --git a/webapp/src/components/genericComponents/simpleAccordion.vue b/webapp/src/components/genericComponents/simpleAccordion.vue new file mode 100644 index 00000000..3fcecf8b --- /dev/null +++ b/webapp/src/components/genericComponents/simpleAccordion.vue @@ -0,0 +1,23 @@ + + + diff --git a/webapp/src/components/labThingsComponents/serverSpecifiedInterface.vue b/webapp/src/components/labThingsComponents/serverSpecifiedInterface.vue new file mode 100644 index 00000000..2afafcfe --- /dev/null +++ b/webapp/src/components/labThingsComponents/serverSpecifiedInterface.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/webapp/src/components/tabContentComponents/slideScanContent.vue b/webapp/src/components/tabContentComponents/slideScanContent.vue index 281954b7..19b6c98e 100644 --- a/webapp/src/components/tabContentComponents/slideScanContent.vue +++ b/webapp/src/components/tabContentComponents/slideScanContent.vue @@ -24,13 +24,7 @@
  • Scan Settings
    -
    - -
    +
  • @@ -125,7 +119,7 @@