def create_panel_widget(
            self, ui: Facade.UserInterface,
            document_controller: Facade.DocumentWindow) -> Facade.ColumnWidget:
        stem_controller_ = typing.cast(
            stem_controller.STEMController,
            Registry.get_component("stem_controller"))

        self.__scan_hardware_source_choice_model = ui._ui.create_persistent_string_model(
            "scan_acquisition_hardware_source_id")
        self.__scan_hardware_source_choice = HardwareSourceChoice.HardwareSourceChoice(
            self.__scan_hardware_source_choice_model,
            lambda hardware_source: hardware_source.features.get(
                "is_scanning", False))
        self.__camera_hardware_source_choice_model = ui._ui.create_persistent_string_model(
            "scan_acquisition_camera_hardware_source_id")
        self.__camera_hardware_source_choice = HardwareSourceChoice.HardwareSourceChoice(
            self.__camera_hardware_source_choice_model,
            lambda hardware_source: hardware_source.features.get(
                "is_camera", False))

        self.__scan_hardware_source_stream = HardwareSourceChoice.HardwareSourceChoiceStream(
            self.__scan_hardware_source_choice).add_ref()
        self.__camera_hardware_source_stream = HardwareSourceChoice.HardwareSourceChoiceStream(
            self.__camera_hardware_source_choice).add_ref()

        def clear_scan_context_fields() -> None:
            self.__roi_description.text = _("Scan context not active")
            self.__scan_label_widget.text = None
            self.__scan_specifier.scan_context = stem_controller.ScanContext()
            self.__scan_specifier.scan_count = 1
            self.__scan_specifier.size = None
            self.__scan_specifier.drift_interval_lines = 0
            self.__scan_specifier.drift_interval_scans = 0
            self.__acquire_button._widget.enabled = self.__acquisition_state == SequenceState.scanning  # focus will be on the SI data, so enable if scanning
            self.__scan_pixels = 0

        def update_context() -> None:
            assert self.__scan_hardware_source_choice
            scan_hardware_source = typing.cast(
                scan_base.ScanHardwareSource,
                self.__scan_hardware_source_choice.hardware_source)
            if not scan_hardware_source:
                clear_scan_context_fields()
                return

            scan_context = scan_hardware_source.scan_context

            scan_context_size = scan_context.size
            exposure_ms = self.__exposure_time_ms_value_model.value or 0.0 if self.__exposure_time_ms_value_model else 0.0
            if scan_context.is_valid and scan_hardware_source.line_scan_enabled and scan_hardware_source.line_scan_vector:
                assert scan_context_size
                calibration = scan_context.calibration
                start = Geometry.FloatPoint.make(
                    scan_hardware_source.line_scan_vector[0])
                end = Geometry.FloatPoint.make(
                    scan_hardware_source.line_scan_vector[1])
                length = int(
                    Geometry.distance(start, end) * scan_context_size.height)
                max_dim = max(scan_context_size.width,
                              scan_context_size.height)
                length_str = calibration.convert_to_calibrated_size_str(
                    length, value_range=(0, max_dim), samples=max_dim)
                line_str = _("Line Scan")
                self.__roi_description.text = f"{line_str} {length_str} ({length} px)"
                scan_str = _("Scan (1D)")
                scan_length = max(self.__scan_width, 1)
                self.__scan_label_widget.text = f"{scan_str} {scan_length} px"
                self.__scan_pixels = scan_length
                self.__scan_specifier.scan_context = copy.deepcopy(
                    scan_context)
                self.__scan_specifier.scan_count = max(self.__scan_count, 1)
                self.__scan_specifier.size = 1, scan_length
                self.__scan_specifier.drift_interval_lines = 0
                self.__scan_specifier.drift_interval_scans = 0
                self.__acquire_button._widget.enabled = True
            elif scan_context.is_valid and scan_hardware_source.subscan_enabled and scan_hardware_source.subscan_region:
                assert scan_context_size
                calibration = scan_context.calibration
                width = scan_hardware_source.subscan_region.width * scan_context_size.width
                height = scan_hardware_source.subscan_region.height * scan_context_size.height
                width_str = calibration.convert_to_calibrated_size_str(
                    width,
                    value_range=(0, scan_context_size.width),
                    samples=scan_context_size.width)
                height_str = calibration.convert_to_calibrated_size_str(
                    height,
                    value_range=(0, scan_context_size.height),
                    samples=scan_context_size.height)
                rect_str = _("Subscan")
                self.__roi_description.text = f"{rect_str} {width_str} x {height_str} ({int(width)} px x {int(height)} px)"
                scan_str = _("Scan (2D)")
                scan_width = self.__scan_width
                scan_height = int(self.__scan_width * height / width)
                drift_lines = scan_hardware_source.calculate_drift_lines(
                    scan_width, exposure_ms /
                    1000) if scan_hardware_source else 0
                drift_str = f" / Drift {drift_lines} lines" if drift_lines > 0 else str(
                )
                drift_scans = scan_hardware_source.calculate_drift_scans()
                drift_str = f" / Drift {drift_scans} scans" if drift_scans > 0 else drift_str
                self.__scan_label_widget.text = f"{scan_str} {scan_width} x {scan_height} px" + drift_str
                self.__scan_pixels = scan_width * scan_height
                self.__scan_specifier.scan_context = copy.deepcopy(
                    scan_context)
                self.__scan_specifier.scan_count = max(self.__scan_count, 1)
                self.__scan_specifier.size = scan_height, scan_width
                self.__scan_specifier.drift_interval_lines = drift_lines
                self.__scan_specifier.drift_interval_scans = drift_scans
                self.__acquire_button._widget.enabled = True
            elif scan_context.is_valid:
                assert scan_context_size
                calibration = scan_context.calibration
                width = scan_context_size.width
                height = scan_context_size.height
                width_str = calibration.convert_to_calibrated_size_str(
                    width,
                    value_range=(0, scan_context_size.width),
                    samples=scan_context_size.width)
                height_str = calibration.convert_to_calibrated_size_str(
                    height,
                    value_range=(0, scan_context_size.height),
                    samples=scan_context_size.height)
                data_str = _("Context Scan")
                self.__roi_description.text = f"{data_str} {width_str} x {height_str} ({int(width)} x {int(height)})"
                scan_str = _("Scan (2D)")
                scan_width = self.__scan_width
                scan_height = int(self.__scan_width * height / width)
                drift_lines = scan_hardware_source.calculate_drift_lines(
                    scan_width, exposure_ms /
                    1000) if scan_hardware_source else 0
                drift_str = f" / Drift {drift_lines} lines" if drift_lines > 0 else str(
                )
                drift_scans = scan_hardware_source.calculate_drift_scans()
                drift_str = f" / Drift {drift_scans} scans" if drift_scans > 0 else drift_str
                self.__scan_label_widget.text = f"{scan_str} {scan_width} x {scan_height} px" + drift_str
                self.__scan_pixels = scan_width * scan_height
                self.__scan_specifier.scan_context = copy.deepcopy(
                    scan_context)
                self.__scan_specifier.scan_count = max(self.__scan_count, 1)
                self.__scan_specifier.size = scan_height, scan_width
                self.__scan_specifier.drift_interval_lines = drift_lines
                self.__scan_specifier.drift_interval_scans = drift_scans
                self.__acquire_button._widget.enabled = True
            else:
                clear_scan_context_fields()

            self.__scan_count_widget.text = Converter.IntegerToStringConverter(
            ).convert(self.__scan_count)

            self.__scan_width_widget.text = Converter.IntegerToStringConverter(
            ).convert(self.__scan_width)

            self.__update_estimate()

        def stem_controller_property_changed(key: str) -> None:
            if key in ("subscan_state", "subscan_region", "subscan_rotation",
                       "line_scan_state", "line_scan_vector",
                       "drift_channel_id", "drift_region", "drift_settings"):
                document_controller._document_controller.event_loop.call_soon_threadsafe(
                    update_context)

        def scan_context_changed() -> None:
            # this can be triggered from a thread, so use call soon to transfer it to the UI thread.
            document_controller._document_controller.event_loop.call_soon_threadsafe(
                update_context)

        self.__stem_controller_property_listener = None
        self.__scan_context_changed_listener = None

        if stem_controller_:
            self.__stem_controller_property_listener = stem_controller_.property_changed_event.listen(
                stem_controller_property_changed)
            self.__scan_context_changed_listener = stem_controller_.scan_context_changed_event.listen(
                scan_context_changed)

        column = ui.create_column_widget()

        self.__styles_list_model = ListModel.ListModel[
            ScanAcquisitionProcessing](items=[
                ScanAcquisitionProcessing.SUM_PROJECT,
                ScanAcquisitionProcessing.NONE
            ])
        self.__styles_list_property_model = ListModel.ListPropertyModel(
            self.__styles_list_model)
        self.__style_combo_box = ui.create_combo_box_widget(
            self.__styles_list_property_model.value,
            item_text_getter=operator.attrgetter("value.display_name"))
        self.__style_combo_box._widget.set_property("min-width", 100)
        items_binding = Binding.PropertyBinding(
            self.__styles_list_property_model, "value")
        items_binding.source_setter = None
        typing.cast(UserInterfaceModule.ComboBoxWidget,
                    self.__style_combo_box._widget).bind_items(items_binding)
        self.__style_combo_box.current_index = 0

        self.__acquire_button = ui.create_push_button_widget(_("Acquire"))

        self.__progress_bar = ui.create_progress_bar_widget()
        # self.__progress_bar.enabled = False

        self.__roi_description = ui.create_label_widget()

        self.__scan_count_widget = ui.create_line_edit_widget()
        self.__scan_count_widget._widget.set_property("width", 72)

        self.__scan_processing_widget = ui.create_combo_box_widget(
            items=["Raw", "Sum", "Raw + Sum"])

        self.__scan_width_widget = ui.create_line_edit_widget()

        self.__exposure_time_widget = ui.create_line_edit_widget()

        self.__estimate_label_widget = ui.create_label_widget()

        self.__scan_label_widget = ui.create_label_widget()

        class ComboBoxWidget:
            def __init__(self,
                         widget: UserInterfaceModule.ComboBoxWidget) -> None:
                self.__combo_box_widget = widget

            @property
            def _widget(self) -> UserInterfaceModule.ComboBoxWidget:
                return self.__combo_box_widget

        camera_row = ui.create_row_widget()
        camera_row.add_spacing(12)
        camera_row.add(
            ComboBoxWidget(
                self.__camera_hardware_source_choice.create_combo_box(ui._ui)))
        camera_row.add_spacing(12)
        camera_row.add(self.__style_combo_box)
        camera_row.add_spacing(12)
        camera_row.add_stretch()

        scan_choice_row = ui.create_row_widget()
        scan_choice_row.add_spacing(12)
        scan_choice_row.add(
            ComboBoxWidget(
                self.__scan_hardware_source_choice.create_combo_box(ui._ui)))
        scan_choice_row.add_spacing(12)
        scan_choice_row.add_stretch()

        scan_count_row = ui.create_row_widget()
        scan_count_row.add_spacing(12)
        scan_count_row.add(ui.create_label_widget("Scan Count"))
        scan_count_row.add_spacing(12)
        scan_count_row.add(self.__scan_count_widget)
        scan_count_row.add_spacing(12)
        scan_count_row.add(self.__scan_processing_widget)
        scan_count_row.add_spacing(12)
        scan_count_row.add_stretch()

        roi_size_row = ui.create_row_widget()
        roi_size_row.add_spacing(12)
        roi_size_row.add(self.__roi_description)
        roi_size_row.add_spacing(12)
        roi_size_row.add_stretch()

        scan_spacing_pixels_row = ui.create_row_widget()
        scan_spacing_pixels_row.add_spacing(12)
        scan_spacing_pixels_row.add(
            ui.create_label_widget("Scan Width (pixels)"))
        scan_spacing_pixels_row.add_spacing(12)
        scan_spacing_pixels_row.add(self.__scan_width_widget)
        scan_spacing_pixels_row.add_spacing(12)
        scan_spacing_pixels_row.add_stretch()

        eels_exposure_row = ui.create_row_widget()
        eels_exposure_row.add_spacing(12)
        eels_exposure_row.add(
            ui.create_label_widget("Camera Exposure Time (ms)"))
        eels_exposure_row.add_spacing(12)
        eels_exposure_row.add(self.__exposure_time_widget)
        eels_exposure_row.add_spacing(12)
        eels_exposure_row.add_stretch()

        scan_row = ui.create_row_widget()
        scan_row.add_spacing(12)
        scan_row.add(self.__scan_label_widget)
        scan_row.add_stretch()

        estimate_row = ui.create_row_widget()
        estimate_row.add_spacing(12)
        estimate_row.add(self.__estimate_label_widget)
        estimate_row.add_stretch()

        acquire_sequence_button_row = ui.create_row_widget()
        acquire_sequence_button_row.add(self.__acquire_button)
        acquire_sequence_button_row.add_spacing(8)
        acquire_sequence_button_row.add(self.__progress_bar)
        acquire_sequence_button_row.add_spacing(8)

        if self.__scan_hardware_source_choice.hardware_source_count > 1:
            column.add_spacing(8)
            column.add(scan_choice_row)
        column.add_spacing(8)
        column.add(camera_row)
        column.add_spacing(8)
        column.add(scan_count_row)
        column.add_spacing(8)
        column.add(roi_size_row)
        column.add_spacing(8)
        column.add(scan_spacing_pixels_row)
        column.add_spacing(8)
        column.add(eels_exposure_row)
        column.add_spacing(8)
        column.add(scan_row)
        column.add_spacing(8)
        column.add(estimate_row)
        column.add_spacing(8)
        column.add(acquire_sequence_button_row)
        column.add_spacing(8)
        column.add_stretch()

        def camera_hardware_source_changed(
            hardware_source: typing.Optional[HardwareSource.HardwareSource]
        ) -> None:
            styles_list_model = self.__styles_list_model
            self.disconnect_camera_hardware_source()
            if hardware_source and styles_list_model:
                self.connect_camera_hardware_source(hardware_source)
                if hardware_source.features.get("has_masked_sum_option"):
                    styles_list_model.items = [
                        ScanAcquisitionProcessing.SUM_PROJECT,
                        ScanAcquisitionProcessing.NONE,
                        ScanAcquisitionProcessing.SUM_MASKED
                    ]
                else:
                    styles_list_model.items = [
                        ScanAcquisitionProcessing.SUM_PROJECT,
                        ScanAcquisitionProcessing.NONE
                    ]

        self.__camera_hardware_changed_event_listener = self.__camera_hardware_source_choice.hardware_source_changed_event.listen(
            camera_hardware_source_changed)
        camera_hardware_source_changed(
            self.__camera_hardware_source_choice.hardware_source)

        def style_current_item_changed(current_item: str) -> None:
            self.__update_estimate()

        self.__style_combo_box.on_current_item_changed = style_current_item_changed

        def scan_count_changed(text: str) -> None:
            scan_count = Converter.IntegerToStringConverter().convert_back(
                text) or 1
            scan_count = max(scan_count, 1)
            if scan_count != self.__scan_count:
                self.__scan_count = scan_count
                update_context()
            self.__scan_count_widget.request_refocus()

        self.__scan_count_widget.on_editing_finished = scan_count_changed

        def scan_width_changed(text: str) -> None:
            scan_width = Converter.IntegerToStringConverter().convert_back(
                text) or 1
            scan_width = max(scan_width, 1)
            if scan_width != self.__scan_width:
                self.__scan_width = scan_width
                update_context()
            self.__scan_width_widget.request_refocus()

        self.__scan_width_widget.on_editing_finished = scan_width_changed

        def acquisition_state_changed(
                acquisition_state: SequenceState) -> None:
            self.__acquisition_state = acquisition_state

            async def update_state(is_idle: bool) -> None:
                self.__acquire_button.text = _("Acquire") if is_idle else _(
                    "Cancel")
                # self.__progress_bar.enabled = not is_idle
                update_context()  # update the cancel button
                if is_idle and self.__progress_task:
                    self.__progress_task.cancel()
                    self.__progress_task = None
                    self.__progress_bar.value = 100
                if not is_idle and not self.__progress_task:

                    async def update_progress() -> None:
                        while True:
                            if self.__scan_acquisition_controller:
                                self.__progress_bar.value = int(
                                    100 *
                                    self.__scan_acquisition_controller.progress
                                )
                            await asyncio.sleep(0.25)

                    self.__progress_task = document_controller._document_window.event_loop.create_task(
                        update_progress())

            if acquisition_state == SequenceState.idle:
                self.__scan_acquisition_controller = None
                if self.__acquisition_state_changed_event_listener:
                    self.__acquisition_state_changed_event_listener.close()
                    self.__acquisition_state_changed_event_listener = None
                document_controller._document_window.event_loop.create_task(
                    update_state(True))
            else:
                document_controller._document_window.event_loop.create_task(
                    update_state(False))

        def acquire_sequence() -> None:
            if self.__scan_acquisition_controller:
                if self.__scan_acquisition_controller:
                    self.__scan_acquisition_controller.cancel()
            else:
                scan_hardware_source_choice = self.__scan_hardware_source_choice
                assert scan_hardware_source_choice
                if scan_hardware_source_choice.hardware_source:
                    scan_hardware_source = self.__api.get_hardware_source_by_id(
                        scan_hardware_source_choice.hardware_source.
                        hardware_source_id,
                        version="1.0")
                else:
                    scan_hardware_source = None

                camera_hardware_source_choice = self.__camera_hardware_source_choice
                assert camera_hardware_source_choice
                if camera_hardware_source_choice.hardware_source:
                    camera_hardware_source = self.__api.get_hardware_source_by_id(
                        camera_hardware_source_choice.hardware_source.
                        hardware_source_id,
                        version="1.0")
                else:
                    camera_hardware_source = None

                if scan_hardware_source and camera_hardware_source:
                    self.__scan_acquisition_controller = ScanAcquisitionController(
                        self.__api, document_controller, scan_hardware_source,
                        camera_hardware_source, self.__scan_specifier)
                    self.__acquisition_state_changed_event_listener = self.__scan_acquisition_controller.acquisition_state_changed_event.listen(
                        acquisition_state_changed)
                    scan_processing = ScanProcessing(
                        self.__scan_processing_widget.current_index in (0, 2),
                        self.__scan_processing_widget.current_index in (1, 2))
                    scan_acquisition_processing = self.__style_combo_box.current_item if self.__style_combo_box and self.__style_combo_box.current_item else ScanAcquisitionProcessing.NONE
                    self.__scan_acquisition_controller.start(
                        scan_acquisition_processing, scan_processing)

        self.__acquire_button.on_clicked = acquire_sequence

        self.__update_estimate()

        update_context()

        return column
 def test_refcounts(self) -> None:
     # list model
     model = ListModel.ListModel[typing.Any]("items")
     model_ref = weakref.ref(model)
     del model
     self.assertIsNone(model_ref())
     # filtered model
     l = ListModel.ListModel[typing.Any]("items")
     model2 = ListModel.FilteredListModel(container=l, items_key="items")
     model_ref2 = weakref.ref(model2)
     del model2
     self.assertIsNone(model_ref2())
     # nested filtered model
     l = ListModel.ListModel[typing.Any]("items")
     l2 = ListModel.FilteredListModel(container=l, items_key="items")
     model3 = ListModel.FilteredListModel(container=l2, items_key="items")
     model_ref3 = weakref.ref(model3)
     del model3
     self.assertIsNone(model_ref3())
     # filtered model with item changed event
     l = ListModel.ListModel[typing.Any]("items")
     l.append_item(C())
     model4 = ListModel.FilteredListModel(container=l, items_key="items")
     model_ref4 = weakref.ref(model4)
     del model4
     self.assertIsNone(model_ref4())
     # mapped model
     l = ListModel.ListModel[typing.Any]("items")
     model5 = ListModel.MappedListModel(container=l,
                                        master_items_key="items",
                                        items_key="items")
     model_ref5 = weakref.ref(model5)
     del model5
     self.assertIsNone(model_ref5())
     # mapped model of filtered model
     l = ListModel.ListModel[typing.Any]("items")
     l2 = ListModel.FilteredListModel(container=l, items_key="items")
     model6 = ListModel.MappedListModel(container=l2,
                                        master_items_key="items",
                                        items_key="items")
     model_ref6 = weakref.ref(model6)
     del model6
     self.assertIsNone(model_ref6())
     # flattened model
     l = ListModel.ListModel[typing.Any]("items")
     model7 = ListModel.FlattenedListModel(container=l,
                                           master_items_key="items",
                                           child_items_key="items",
                                           items_key="items")
     model_ref7 = weakref.ref(model7)
     del model7
     self.assertIsNone(model_ref7())
     # flattened model with items
     l = ListModel.ListModel[typing.Any]("items")
     l.append_item(ListModel.ListModel[typing.Any]("items"))
     model8 = ListModel.FlattenedListModel(container=l,
                                           master_items_key="items",
                                           child_items_key="items",
                                           items_key="items")
     model_ref8 = weakref.ref(model8)
     del model8
     self.assertIsNone(model_ref8())
     # list property model
     l = ListModel.ListModel[typing.Any]("items")
     model9 = ListModel.ListPropertyModel(l)
     model_ref9 = weakref.ref(model9)
     del model9
     self.assertIsNone(model_ref9())