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 @@
+
+