# Source code for dipy.viz.horizon.tab.base

    from abc import ABC, abstractmethod
    from dataclasses import dataclass
    import logging
    from typing import Any
    import warnings

    import numpy as np

    from dipy.testing.decorators import warning_for_keywords
    from dipy.utils.optpkg import optional_package
    from dipy.viz.horizon.util import show_ellipsis

    fury, has_fury, setup_module = optional_package("fury", min_version="0.10.0")

    if has_fury:
        from fury import ui
        from fury.data import read_viz_icons

    @dataclass
    class HorizonUIElement:
        """Dataclass to define properties of horizon ui elements."""

        visibility: bool
        selected_value: Any
        obj: Any
        position = (0, 0)
        size = ("auto", "auto")

    class HorizonTab(ABC):
        """Base for different tabs available in horizon."""

        def __init__(self):
            self._elements = []
            self.hide = lambda *args: None
            self.show = lambda *args: None

        @abstractmethod
        def build(self, tab_id):
            """Build all the elements under the tab.

            Parameters
            ----------
            tab_id : int
                Id of the tab.
            """

        def _register_elements(self, *args):
            """Register elements for rendering.

            Parameters
            ----------
            *args : HorizonUIElement(s)
                Elements to be register for rendering.
            """
            self._elements += list(args)

        def _toggle_actors(self, checkbox):
            """Toggle actors in the scene. This helps removing the actor to
            interact with actors behind this actor.

            Parameters
            ----------
            checkbox : CheckBox2D
            """
            if "" in checkbox.checked_labels:
                self.show(*self.actors)
            else:
                self.hide(*self.actors)

        def on_tab_selected(self):
            """Implement if require to update something while the tab becomes
            active.
            """
            if hasattr(self, "_actor_toggle"):
                self._toggle_actors(self._actor_toggle.obj)

        @property
        @abstractmethod
        def name(self):
            """Name of the tab."""

        @property
        @abstractmethod
        def actors(self):
            """Name of the tab."""

        @property
        def tab_id(self):
            """Id of the tab. Reference for Tab Manager to identify the tab.

            Returns
            -------
            int
            """
            return self._tab_id

        @property
        def elements(self):
            """list of underlying FURY ui elements in the tab."""
            return self._elements

    class TabManager:
        """
        A Manager for tabs of the table panel.

        Attributes
        ----------

        tab_ui : TabUI
            Underlying FURY TabUI object.
        """

        @warning_for_keywords()
        def __init__(
            self,
            tabs,
            win_size,
            on_tab_changed,
            add_to_scene,
            *,
            remove_from_scene,
            sync_slices=False,
            sync_volumes=False,
            sync_peaks=False,
        ):
            num_tabs = len(tabs)
            self._tabs = tabs
            self._add_to_scene = add_to_scene
            self._remove_from_scene = remove_from_scene
            self._synchronize_slices = sync_slices
            self._synchronize_volumes = sync_volumes
            self._synchronize_peaks = sync_peaks

            win_width, _win_height = win_size

            self._tab_size = (1280, 240)
            x_pad = np.rint((win_width - self._tab_size[0]) / 2)

            self._active_tab_id = num_tabs - 1

            self._tab_ui = ui.TabUI(
                position=(x_pad, 5),
                size=self._tab_size,
                nb_tabs=num_tabs,
                active_color=(1, 1, 1),
                inactive_color=(0.5, 0.5, 0.5),
                draggable=True,
                startup_tab_id=self._active_tab_id,
            )

            self._tab_ui.on_change = self._tab_selected

            self.tab_changed = on_tab_changed

            slices_tabs = list(
                filter(lambda x: x.__class__.__name__ == "SlicesTab", self._tabs)
            )
            if not self._synchronize_slices and slices_tabs:
                msg = (
                    "Images are of different dimensions, "
                    + "synchronization of slices will not work"
                )
                logging.warning(msg)

            for tab_id, tab in enumerate(tabs):
                self._tab_ui.tabs[tab_id].title_font_size = 18
                tab.hide = self._hide_elements
                tab.show = self._show_elements
                tab.build(tab_id)
                if tab.__class__.__name__ == "SlicesTab":
                    tab.on_volume_change = self.synchronize_volumes
                if tab.__class__.__name__ in ["SlicesTab", "PeaksTab"]:
                    tab.on_slice_change = self.synchronize_slices
                self._render_tab_elements(tab.tab_id, tab.elements)

        def handle_text_overflows(self):
            for tab_id, tab in enumerate(self._tabs):
                self._handle_title_overflow(tab.name, self._tab_ui.tabs[tab_id].text_block)
                if tab.__class__.__name__ == "SlicesTab":
                    self._handle_label_text_overflow(tab.elements)

        def _handle_label_text_overflow(self, elements):
            for element in elements:
                if (
                    not element.size[0] == "auto"
                    and element.obj.__class__.__name__ == "TextBlock2D"
                    and isinstance(element.position, tuple)
                ):
                    element.obj.message = show_ellipsis(
                        element.selected_value, element.obj.size[0], element.size[0]
                    )

        def _handle_title_overflow(self, title_text, title_block):
            """Handle overflow of the tab title and show ellipsis if required.

            Parameters
            ----------
            title_text : str
                Text to be shown on the tab.
            title_block : TextBlock2D
                Fury UI element for holding the title of the tab.
            """
            tab_text = title_text.split(".", 1)[0]
            title_block.message = tab_text
            available_space, _ = self._tab_size
            text_size = title_block.size[0]
            max_width = (available_space / len(self._tabs)) - 15
            title_block.message = show_ellipsis(tab_text, text_size, max_width)

        def _render_tab_elements(self, tab_id, elements):
            for element in elements:
                if isinstance(element.position, list):
                    for i, position in enumerate(element.position):
                        self._tab_ui.add_element(tab_id, element.obj[i], position)
                else:
                    self._tab_ui.add_element(tab_id, element.obj, element.position)

        def _hide_elements(self, *args):
            """Hide elements from the scene.

            Parameters
            ----------
            *args : HorizonUIElement or FURY actors
                Elements to be hidden.
            """
            self._remove_from_scene(*self._get_vtkActors(*args))

        def _show_elements(self, *args):
            """Show elements in the scene.

            Parameters
            ----------
            *args : HorizonUIElement or FURY actors
                Elements to be hidden.
            """
            self._add_to_scene(*self._get_vtkActors(*args))

        def _get_vtkActors(self, *args):
            elements = []
            vtk_actors = []
            for element in args:
                if element.__class__.__name__ == "HorizonUIElement":
                    if isinstance(element.obj, list):
                        for obj in element.obj:
                            elements.append(obj)
                    else:
                        elements.append(element.obj)
                else:
                    elements.append(element)

            for element in elements:
                if hasattr(element, "_get_actors") and callable(element._get_actors):
                    vtk_actors += element.actors
                else:
                    vtk_actors.append(element)
            return vtk_actors

        def _tab_selected(self, tab_ui):
            if self._active_tab_id == tab_ui.active_tab_idx:
                self._active_tab_id = -1
                return

            self._active_tab_id = tab_ui.active_tab_idx

            current_tab = self._tabs[self._active_tab_id]
            self.tab_changed(current_tab.actors)
            current_tab.on_tab_selected()

        def reposition(self, win_size):
            """
            Reposition the tabs panel.

            Parameters
            ----------
            win_size : (float, float)
                size of the horizon window.
            """
            win_width, _win_height = win_size
            x_pad = np.rint((win_width - self._tab_size[0]) / 2)
            self._tab_ui.position = (x_pad, 5)

        def synchronize_slices(self, active_tab_id, x_value, y_value, z_value):
            """
            Synchronize slicers for all the images and peaks.

            Parameters
            ----------
            active_tab_id: int
                tab_id of the action performing tab
            x_value: float
                x-value of the active slicer
            y_value: float
                y-value of the active slicer
            z_value: float
                z-value of the active slicer
            """

            if not self._synchronize_slices and not self._synchronize_peaks:
                return

            for tab in self._get_non_active_tabs(active_tab_id, ["SlicesTab", "PeaksTab"]):
                tab.update_slices(x_value, y_value, z_value)

        def synchronize_volumes(self, active_tab_id, value):
            """Synchronize volumes for all the images with volumes.

            Parameters
            ----------
            active_tab_id : int
                tab_id of the action performing tab
            value : float
                volume value of the active volume slider

            """

            if not self._synchronize_volumes:
                return

            for slices_tab in self._get_non_active_tabs(active_tab_id):
                slices_tab.update_volume(value)

        def _get_non_active_tabs(self, active_tab_id, types=("SlicesTab",)):
            """Get tabs which are not active and slice tabs.

            Parameters
            ----------
            active_tab_id : int
            types : list(str), optional

            Returns
            -------
            list
            """
            return list(
                filter(
                    lambda x: x.__class__.__name__ in types
                    and not x.tab_id == active_tab_id,
                    self._tabs,
                )
            )

        @property
        def tab_ui(self):
            """FURY TabUI object."""
            return self._tab_ui

    @warning_for_keywords()
    def build_label(text, *, font_size=16, bold=False):
        """Simple utility function to build labels.

        Parameters
        ----------
        text : str
        font_size : int, optional
        bold : bool, optional

        Returns
        -------
        label : TextBlock2D
        """

        label = ui.TextBlock2D()
        label.message = text
        label.font_size = font_size
        label.font_family = "Arial"
        label.justification = "left"
        label.bold = bold
        label.italic = False
        label.shadow = False
        label.actor.GetTextProperty().SetBackgroundColor(0, 0, 0)
        label.actor.GetTextProperty().SetBackgroundOpacity(0.0)
        label.color = (0.7, 0.7, 0.7)

        return HorizonUIElement(True, text, label)

    @warning_for_keywords()
    def build_slider(
        initial_value,
        max_value,
        *,
        min_value=0,
        length=450,
        line_width=3,
        radius=8,
        font_size=16,
        text_template="{value:.1f} ({ratio:.0%})",
        on_moving_slider=lambda _slider: None,
        on_value_changed=lambda _slider: None,
        on_change=lambda _slider: None,
        on_handle_released=lambda _istyle, _obj, _slider: None,
        label="",
        label_font_size=16,
        label_style_bold=False,
        is_double_slider=False,
    ):
        """Create a horizon theme based disk-knob slider.

        Parameters
        ----------
        initial_value : float, (float, float)
            Initial value(s) of the slider.
        max_value : float
            Maximum value of the slider.
        min_value : float, optional
            Minimum value of the slider.
        length : int, optional
            Length of the slider.
        line_width : int, optional
            Width of the line on which the disk will slide.
        radius : int, optional
            Radius of the disk handle.
        font_size : int, optional
            Size of the text to display alongside the slider (pt).
        text_template : str, callable, optional
            If str, text template can contain one or multiple of the
            replacement fields: `{value:}`, `{ratio:}`.
            If callable, this instance of `:class:LineSlider2D` will be
            passed as argument to the text template function.
        on_moving_slider : callable, optional
            When the slider is interacted by the user.
        on_value_changed : callable, optional
            When value of the slider changed programmatically.
        on_change : callable, optional
            When value of the slider changed.
        on_handle_released: callable, optional
            When handle released.
        label : str, optional
            Label to ui element for slider
        label_font_size : int, optional
            Size of label text to display with slider
        label_style_bold : bool, optional
            Is label should have bold style.
        is_double_slider : bool, optional
            True if the slider allows to adjust two values.

        Returns
        -------
        label : HorizonUIElement
            Slider label.
        HorizonUIElement
            Slider.
        """

        if is_double_slider and "ratio" in text_template:
            warnings.warn("Double slider only support values and not ratio", stacklevel=2)
            return

        slider_label = build_label(label, font_size=label_font_size, bold=label_style_bold)

        if not is_double_slider:
            slider = ui.LineSlider2D(
                initial_value=initial_value,
                max_value=max_value,
                min_value=min_value,
                length=length,
                line_width=line_width,
                outer_radius=radius,
                font_size=font_size,
                text_template=text_template,
            )
        else:
            slider = ui.LineDoubleSlider2D(
                initial_values=initial_value,
                max_value=max_value,
                min_value=min_value,
                length=length,
                line_width=line_width,
                outer_radius=radius,
                font_size=font_size,
                text_template=text_template,
            )

        slider.on_moving_slider = on_moving_slider
        slider.on_value_changed = on_value_changed
        slider.on_change = on_change

        if not is_double_slider:
            slider.handle_events(slider.handle.actor)
            slider.on_left_mouse_button_released = on_handle_released

        slider.default_color = (1.0, 0.5, 0.0)
        slider.track.color = (0.8, 0.3, 0.0)
        slider.active_color = (0.9, 0.4, 0.0)
        if not is_double_slider:
            slider.handle.color = (1.0, 0.5, 0.0)
        else:
            slider.handles[0].color = (1.0, 0.5, 0.0)
            slider.handles[1].color = (1.0, 0.5, 0.0)

        return slider_label, HorizonUIElement(True, initial_value, slider)

    @warning_for_keywords()
    def build_checkbox(
        *,
        labels=None,
        checked_labels=None,
        padding=1,
        font_size=16,
        on_change=lambda _checkbox: None,
    ):
        """Create horizon theme checkboxes.

        Parameters
        ----------
        labels : list(str), optional
            List of labels of each option.
        checked_labels: list(str), optional
            List of labels that are checked on setting up.
        padding : float, optional
            The distance between two adjacent options element
        font_size : int, optional
            Size of the text font.
        on_change : callback, optional
            When checkbox value changed

        Returns
        -------
        checkbox : HorizonUIElement
        """

        if labels is None or not labels:
            warnings.warn(
                "At least one label needs to be to create checkboxes", stacklevel=2
            )
            return

        if checked_labels is None:
            checked_labels = ()

        checkboxes = ui.Checkbox(
            labels=labels,
            checked_labels=checked_labels,
            padding=padding,
            font_size=font_size,
        )

        checkboxes.on_change = on_change

        return HorizonUIElement(True, checked_labels, checkboxes)

    @warning_for_keywords()
    def build_radio_button(
        *,
        labels=None,
        checked_labels=None,
        padding=1,
        font_size=16,
        on_change=lambda _checkbox: None,
    ):
        """Create horizon theme radio buttons.

        Parameters
        ----------
        labels : list(str), optional
            List of labels of each option.
        checked_labels: list(str), optional
            List of labels that are checked on setting up.
        padding : float, optional
            The distance between two adjacent options element
        font_size : int, optional
            Size of the text font.
        on_change : callback, optional
            When radio button value changed

        Returns
        -------
        radio : HorizonUIElement
        """

        if labels is None or not labels:
            warnings.warn(
                "At least one label needs to be to create radio buttons", stacklevel=2
            )
            return

        if checked_labels is None:
            checked_labels = ()

        radio = ui.RadioButton(
            labels=labels,
            checked_labels=checked_labels,
            padding=padding,
            font_size=font_size,
        )

        radio.on_change = on_change

        return HorizonUIElement(True, checked_labels, radio)

    @warning_for_keywords()
    def build_switcher(
        *,
        items=None,
        label="",
        initial_selection=0,
        on_prev_clicked=lambda _selected_value: None,
        on_next_clicked=lambda _selected_value: None,
        on_value_changed=lambda _selected_idx, _selected_value: None,
    ):
        """Create horizon theme switcher.

        Parameters
        ----------
        items : list, optional
            dictionaries with keys 'label' and 'value'. Label will be used to show
            it to user and value will be used for selection.
        label : str, optional
            label for the switcher.
        initial_selection : int, optional
            index of the selected item initially.
        on_prev_clicked : callback, optional
            method providing a callback when prev value is selected in switcher.
        on_next_clicked : callback, optional
            method providing a callback when next value is selected in switcher.
        on_value_changed : callback, optional
            method providing a callback when either prev or next value selected in
            switcher.

        Returns
        -------
        HorizonCombineElement(
            label: HorizonUIElement,
            element(switcher): HorizonUIElement)

        Notes
        -----
        switcher: consists 'obj' which is an array providing FURY UI elements used.
        """
        if items is None:
            warnings.warn("No items passed in switcher", stacklevel=2)
            return

        num_items = len(items)

        if initial_selection >= num_items:
            initial_selection = 0

        switch_label = build_label(text=label)
        selection_label = build_label(text=items[initial_selection]["label"]).obj

        left_button = ui.Button2D(
            icon_fnames=[("left", read_viz_icons(fname="circle-left.png"))], size=(25, 25)
        )
        right_button = ui.Button2D(
            icon_fnames=[("right", read_viz_icons(fname="circle-right.png"))], size=(25, 25)
        )

        switcher = HorizonUIElement(
            True,
            [initial_selection, items[initial_selection]["value"]],
            [left_button, selection_label, right_button],
        )

        def left_clicked(_i_ren, _obj, _button):
            selected_id = switcher.selected_value[0] - 1
            if selected_id < 0:
                selected_id = num_items - 1
            value_changed(selected_id)
            on_prev_clicked(items[selected_id]["value"])
            on_value_changed(selected_id, items[selected_id]["value"])

        def right_clicked(_i_ren, _obj, _button):
            selected_id = switcher.selected_value[0] + 1
            if selected_id >= num_items:
                selected_id = 0
            value_changed(selected_id)
            on_next_clicked(items[selected_id]["value"])
            on_value_changed(selected_id, items[selected_id]["value"])

        def value_changed(selected_id):
            switcher.selected_value[0] = selected_id
            switcher.selected_value[1] = items[selected_id]["value"]
            selection_label.message = items[selected_id]["label"]

        left_button.on_left_mouse_button_clicked = left_clicked
        right_button.on_left_mouse_button_clicked = right_clicked

        return (switch_label, switcher)