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

from abc import ABC, abstractmethod
from dataclasses import dataclass
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


[docs] @dataclass class HorizonUIElement: """Dataclass to define properties of horizon ui elements.""" visibility: bool selected_value: Any obj: Any position = (0, 0) size = ("auto", "auto")
[docs] 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
[docs] @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)
[docs] 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
[docs] 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" ) warnings.warn(msg, stacklevel=2) 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)
[docs] 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()
[docs] 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)
[docs] 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)
[docs] 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
[docs] @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)
[docs] @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)
[docs] @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)
[docs] @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)
[docs] @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)