"""UI components module."""
__all__ = [
"TexturedButton2D",
"TextButton2D",
"LineSlider2D",
# "TextBox2D",
# "LineSlider2D",
# "LineDoubleSlider2D",
"RingSlider2D",
# "RangeSlider",
# "Checkbox",
# "Option",
# "RadioButton",
# "ComboBox2D",
# "ListBox2D",
# "ListBoxItem2D",
# "FileMenu2D",
# "DrawShape",
# "DrawPanel",
"PlaybackPanel",
# "Card2D",
# "SpinBox",
]
import numpy as np
from fury.actor import create_mesh
from fury.data import read_viz_icons
from fury.io import load_image_texture
from fury.lib import (
plane_geometry,
)
from fury.material import _create_mesh_material
from fury.ui.containers import Panel2D
from fury.ui.core import (
UI,
Anchor,
Button2D,
Disk2D,
Rectangle2D,
Slider2D,
TextBlock2D,
)
TWO_PI = 2.0 * np.pi
[docs]
class TexturedButton2D(Button2D):
"""
A button component that swaps textures based on interaction state.
Parameters
----------
states : dict
A mapping of state names to image file paths.
position : (float, float)
Absolute coordinates (x, y) for placement.
size : (int, int)
Width and height in pixels.
is_toggle : bool, optional
If True, the button behaves as a toggle switch.
"""
[docs]
def __init__(self, states, position=(0, 0), size=(30, 30), is_toggle=False):
"""Initialize the textured button instance."""
self.texture_map = self._load_textures(states)
super().__init__(position=position, size=size, is_toggle=is_toggle)
def _load_textures(self, states):
"""
Load image files into PyGfx textures.
Parameters
----------
states : dict
Dictionary of state names and file paths.
Returns
-------
dict
A dictionary containing loaded Texture objects.
"""
loaded = {}
for name, fname in states.items():
loaded[name] = load_image_texture(fname)
return loaded
def _setup(self):
"""Set up the internal mesh actor."""
geo = plane_geometry(width=1, height=1)
mat = _create_mesh_material(material="basic")
self.child = create_mesh(geometry=geo, material=mat)
self.handle_events(self.child)
[docs]
def update_visual_state(self):
"""Update the mesh texture based on the current button state."""
if not self.child:
return
key = self.resolve_state_key(self.texture_map)
if key:
tex = self.texture_map[key]
self.child.material = _create_mesh_material(
material="basic", texture=tex, mode="auto"
)
self.child.material.color = np.array([1.0, 1.0, 1.0, 1.0])
else:
tint = 0.5 if self.is_pressed else (0.8 if self.is_hovered else 1.0)
self.child.material.color = np.array([tint, tint, tint, 1.0])
[docs]
class TextButton2D(Button2D):
"""
A button component that updates text and color based on state.
Parameters
----------
label : str
The default text to display on the button.
states : dict
Configuration for visual states. Supports mapping keys to RGB
tuples or dictionaries containing 'text' and 'color' keys.
position : (float, float)
Absolute coordinates (x, y) for placement.
size : (int, int)
Width and height in pixels for the button background.
font_size : int
Size of the text font.
is_toggle : bool, optional
If True, the button behaves as a toggle switch.
"""
[docs]
def __init__(
self,
label,
states=None,
position=(0, 0),
size=(100, 40),
font_size=25,
is_toggle=False,
):
"""Initialize the text button instance."""
self.default_label = label
self.font_size = font_size
self.states = states or {
"default": (1, 1, 1),
"hover": (0.9, 0.9, 0.9),
"pressed": (0.5, 0.5, 0.5),
"disabled": (0.2, 0.2, 0.2),
}
super().__init__(position=position, size=size, is_toggle=is_toggle)
def _setup(self):
"""
Set up the internal TextBlock2D component.
Initializes the child text block with the default label and font
settings.
"""
self.child = TextBlock2D(
text=self.default_label,
color=(0, 0, 0),
bg_color=(1, 1, 1),
font_size=self.font_size,
size=self._dims,
)
self.handle_events(self.child.actor)
self.handle_events(self.child.background.actor)
[docs]
def update_visual_state(self):
"""Update the text message and background color based on state."""
if not self.child:
return
key = self.resolve_state_key(self.states)
if not key:
return
data = self.states[key]
target_color = (1, 1, 1)
target_text = self.default_label
if isinstance(data, (tuple, list, np.ndarray)):
target_color = data
elif isinstance(data, dict):
target_color = data.get("color", target_color)
target_text = data.get("text", target_text)
self.child.background.color = target_color
if self.child.message != target_text:
self.child.message = target_text
[docs]
class LineSlider2D(Slider2D):
"""
A 2D Line Slider component.
Parameters
----------
position : (float, float), optional
Absolute coordinates (x, y) for placement.
initial_value : float, optional
The starting value of the slider.
min_value : float, optional
The minimum value of the slider range.
max_value : float, optional
The maximum value of the slider range.
length : int, optional
The length of the slider track in pixels.
line_width : int, optional
The thickness of the slider track.
inner_radius : int, optional
The inner radius for disk-shaped handles (for rings).
outer_radius : int, optional
The outer radius for disk-shaped handles.
handle_side : int, optional
The side length for square-shaped handles.
font_size : int, optional
The font size for the value label.
orientation : str, optional
The slider orientation: "horizontal" or "vertical".
text_template : str, optional
A formatting string for the label. Supports {value} and {ratio}.
shape : str, optional
The handle shape: "disk" or "square".
z_order : int, optional
The stacking priority. The handle is assigned z_order + 1.
"""
[docs]
def __init__(
self,
*,
position=(0, 0),
initial_value=50,
min_value=0,
max_value=100,
length=200,
line_width=5,
inner_radius=0,
outer_radius=10,
handle_side=20,
font_size=16,
orientation="horizontal",
text_template="{value:.1f} ({ratio:.0%})",
shape="disk",
z_order=0,
):
"""Initialize the slider instance."""
self.orientation = orientation.lower().strip()
self._length = length
self._line_width = line_width
super(LineSlider2D, self).__init__(
position=position,
initial_value=initial_value,
min_value=min_value,
max_value=max_value,
handle_inner_radius=inner_radius,
handle_outer_radius=outer_radius,
handle_side=handle_side,
font_size=font_size,
text_template=text_template,
shape=shape,
z_order=z_order,
)
self.value = initial_value
def _setup(self):
"""Set up the internal actors."""
super(LineSlider2D, self)._setup()
track_size = (
(self._length, self._line_width)
if self.orientation == "horizontal"
else (self._line_width, self._length)
)
self.track = Rectangle2D(size=track_size)
self.track.color = (1, 0, 0)
self.track.z_order = self.z_order
self.handle.color = self.default_color
self.track.on_left_mouse_button_pressed = self.track_click_callback
self.track.on_left_mouse_button_dragged = self.handle_move_callback
self.track.on_left_mouse_button_released = self.handle_release_callback
self.handle.on_left_mouse_button_dragged = self.handle_move_callback
self.handle.on_left_mouse_button_released = self.handle_release_callback
self._children.extend([self.track, self.handle, self.text])
def _get_actors(self):
"""
Get the actors composing this UI component.
Returns
-------
list
Empty list as this UI uses other UI elements as children
instead of direct actors.
"""
return []
def _get_size(self):
"""
Calculate the total bounding box size of the slider.
Returns
-------
numpy.ndarray
The (width, height) in pixels.
"""
if self.orientation == "horizontal":
width = self._length
height = max(self._line_width, self.handle.size[1])
else:
width = max(self._line_width, self.handle.size[0])
height = self._length
return np.array([width, height])
def _update_actors_position(self):
"""Update the position of the track and handle actors."""
pos = self.get_position(x_anchor=Anchor.LEFT, y_anchor=Anchor.TOP)
self.track.set_position(
pos + self.size / 2, x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER
)
self._update_handle_position()
def _update_handle_position(self):
"""Calculate specific coordinates for the handle and text label."""
track_origin = self.track.get_position(
x_anchor=Anchor.LEFT, y_anchor=Anchor.TOP
)
if self.orientation == "horizontal":
offset = self.ratio * self._length
handle_center = track_origin + np.array([offset, self._line_width / 2])
text_pos = handle_center + np.array(
[0, -(self.handle.size[1] + self.text.size[1] / 2)]
)
else:
offset = self.ratio * self._length
handle_center = track_origin + np.array([self._line_width / 2, offset])
text_pos = handle_center + np.array(
[self.handle.size[0] + self.text.size[0] / 2, 0]
)
self.handle.set_position(
handle_center, x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER
)
self.text.set_position(text_pos, x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
self.text.message = self.format_text()
[docs]
def handle_move_callback(self, event):
"""
Handle mouse drag events to update the slider state.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
self.handle.color = self.active_color
left_x = self.track.get_position(x_anchor=Anchor.LEFT)[0]
bottom_y = self.track.get_position(y_anchor=Anchor.BOTTOM)[1]
top_y = self.track.get_position(y_anchor=Anchor.TOP)[1]
if self.orientation == "horizontal":
new_ratio = (event.x - left_x) / self._length
else:
total_dist = bottom_y - top_y
current_dist = event.y - top_y
if total_dist != 0:
new_ratio = current_dist / total_dist
else:
new_ratio = 0
self.ratio = new_ratio
self.on_moving_slider(self)
[docs]
class PlaybackPanel(UI):
"""
A playback controller designed for FURY v2.
Parameters
----------
loop : bool, optional
If True, the playback starts in looping mode.
position : (float, float), optional
Absolute coordinates (x, y) for placement.
width : int, optional
The total width of the playback panel in pixels.
z_order : int, optional
The stacking priority of the panel.
"""
[docs]
def __init__(self, *, loop=False, position=(0, 0), width=900, z_order=0):
"""
Initialize the playback panel instance.
Parameters
----------
loop : bool, optional
If True, the playback starts in looping mode.
position : (float, float), optional
Absolute coordinates (x, y) for placement.
width : int, optional
The total width of the playback panel in pixels.
z_order : int, optional
The stacking priority of the panel.
"""
self._drag_offset = None
self._width = width
self._playing = False
self._loop = None
self.on_play_pause_toggle = lambda state: None
self.on_play = lambda: None
self.on_pause = lambda: None
self.on_stop = lambda: None
self.on_loop_toggle = lambda is_looping: None
self.on_progress_bar_changed = lambda x: None
self.on_speed_changed = lambda x: None
super(PlaybackPanel, self).__init__(position=position, z_order=z_order)
self.loop() if loop else self.play_once()
self.current_time = 0
self.speed = 1.0
def _setup(self):
"""
Set up internal components including buttons, slider, and text labels.
"""
self.panel = Panel2D(
size=(220, 45),
color=(1, 1, 1),
has_border=True,
border_color=(0, 0.3, 0),
border_width=2,
)
self.time_text = TextBlock2D(
text="00:00.00",
font_size=16,
color=(1, 1, 1),
justification="left",
vertical_justification="middle",
dynamic_bbox=True,
)
self.speed_text = TextBlock2D(
text="1x",
font_size=21,
color=(0.2, 0.2, 0.2),
bold=True,
justification="center",
vertical_justification="middle",
dynamic_bbox=True,
)
icon_play_pause = {
"default": read_viz_icons(fname="play3.png"),
"pressed": read_viz_icons(fname="pause2.png"),
}
icon_loop = {
"default": read_viz_icons(fname="checkmark.png"),
"pressed": read_viz_icons(fname="infinite.png"),
}
self._play_pause_btn = TexturedButton2D(
states=icon_play_pause, size=(25, 25), is_toggle=True
)
self._stop_btn = TexturedButton2D(
states={"default": read_viz_icons(fname="stop2.png")}, size=(25, 25)
)
self._loop_btn = TexturedButton2D(
states=icon_loop, size=(25, 25), is_toggle=True
)
self._speed_up_btn = TexturedButton2D(
states={"default": read_viz_icons(fname="plus.png")}, size=(15, 15)
)
self._slow_down_btn = TexturedButton2D(
states={"default": read_viz_icons(fname="minus.png")}, size=(15, 15)
)
self._progress_bar = LineSlider2D(
initial_value=0,
length=self._width - 330,
line_width=9,
text_template="",
shape="disk",
outer_radius=10,
)
self._progress_bar.track.color = (1, 0, 0)
self.panel.add_element(self._play_pause_btn, (10, 10))
self.panel.add_element(self._stop_btn, (45, 10))
self.panel.add_element(self._loop_btn, (80, 10))
self.panel.add_element(self._slow_down_btn, (125, 15))
self.panel.add_element(self.speed_text, (157, 15), anchor="center")
self.panel.add_element(self._speed_up_btn, (195, 15))
self._play_pause_btn.on_clicked = self._play_pause_callback
self._stop_btn.on_clicked = lambda e: self.stop()
self._loop_btn.on_clicked = self._loop_callback
self._speed_up_btn.on_clicked = self._speed_up_callback
self._slow_down_btn.on_clicked = self._slow_down_callback
self._progress_bar.on_moving_slider = self._on_progress_change
self.panel.on_left_mouse_button_pressed = self.left_button_pressed
self.panel.on_left_mouse_button_dragged = self.left_button_dragged
self.panel.background.on_left_mouse_button_pressed = self.left_button_pressed
self.panel.background.on_left_mouse_button_dragged = self.left_button_dragged
def _update_actors_position(self):
"""Update internal actor positions."""
pos = self.get_position(x_anchor=Anchor.LEFT, y_anchor=Anchor.TOP)
self.panel.set_position(pos + (5, 5))
pbar_length = max(self._width - 330, 10.0)
self._progress_bar._length = pbar_length
self._progress_bar.set_position(
(pos[0] + 240, pos[1] + 27), x_anchor=Anchor.LEFT, y_anchor=Anchor.CENTER
)
self.time_text.set_position(
(pos[0] + 250 + pbar_length, pos[1] + 27),
x_anchor=Anchor.LEFT,
y_anchor=Anchor.CENTER,
)
self._children.extend([self.panel, self._progress_bar, self.time_text])
def _get_actors(self):
"""
Get the actors composing this UI component.
Returns
-------
list
Empty list as this UI uses other UI elements as children
instead of direct actors.
"""
return []
def _get_size(self):
"""
Get the total width and height of the playback panel.
Returns
-------
numpy.ndarray
The (width, height) in pixels.
"""
return np.array([self._width, 55])
def _play_pause_callback(self, event):
"""
Handle toggle logic between play and pause states.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
self._playing = not self._playing
self.play() if self._playing else self.pause()
self.on_play_pause_toggle(self._playing)
def _loop_callback(self, event):
"""
Handle toggle logic for the looping state.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
self._loop = not self._loop
self.loop() if self._loop else self.play_once()
self.on_loop_toggle(self._loop)
def _speed_up_callback(self, event):
"""
Increment the playback speed.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
inc = 10 ** np.floor(np.log10(self.speed))
self.speed = round(self.speed + inc, 13)
self.on_speed_changed(self._speed)
def _slow_down_callback(self, event):
"""
Decrement the playback speed.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
safe_speed = max(self.speed - self.speed / 10, 0.01)
dec = 10 ** np.floor(np.log10(safe_speed))
self.speed = round(self.speed - dec, 13)
self.on_speed_changed(self._speed)
def _on_progress_change(self, slider):
"""
Update time tracking based on slider movement.
Parameters
----------
slider : LineSlider2D
The slider component instance.
"""
self.on_progress_bar_changed(slider.value)
self.current_time = slider.value
[docs]
def play(self):
"""Set the controller to playing state."""
self._playing = True
self._play_pause_btn.toggled = True
self.on_play()
[docs]
def pause(self):
"""Set the controller to paused state."""
self._playing = False
self._play_pause_btn.toggled = False
self.on_pause()
[docs]
def stop(self):
"""Stop the playback and reset the timer."""
self._playing = False
self.current_time = 0
self._play_pause_btn.toggled = False
self.on_stop()
[docs]
def loop(self):
"""Enable looping mode."""
self._loop = True
self._loop_btn.toggled = True
[docs]
def play_once(self):
"""Disable looping mode."""
self._loop = False
self._loop_btn.toggled = False
@property
def current_time(self):
"""
Get the current playback time.
Returns
-------
float
Current time in seconds.
"""
return self._progress_bar.value
@current_time.setter
def current_time(self, t):
"""
Set the current playback time.
Parameters
----------
t : float
New time in seconds.
"""
self._progress_bar.value = t
self.current_time_str = t
@property
def final_time(self):
"""
Get the total duration of the playback.
Returns
-------
float
Total duration in seconds.
"""
return self._progress_bar.max_value
@final_time.setter
def final_time(self, t):
"""
Set the total duration of the playback.
Parameters
----------
t : float
New total duration.
"""
self._progress_bar.max_value = t
@property
def current_time_str(self):
"""
Get the formatted string representation of current time.
Returns
-------
str
Formatted time string.
"""
return self.time_text.message
@current_time_str.setter
def current_time_str(self, t):
"""
Update the time label string based on seconds.
Parameters
----------
t : float
Time in seconds.
"""
t = np.clip(t, 0, self.final_time)
m, s = divmod(t, 60)
if self.final_time < 3600:
t_str = f"{int(m):02d}:{s:05.2f}"
else:
h, m = divmod(m, 60)
t_str = f"{int(h):02d}:{int(m):02d}:{s:02d}"
self.time_text.message = t_str
@property
def speed(self):
"""
Get the current playback speed.
Returns
-------
float
Playback speed multiplier.
"""
return self._speed
@speed.setter
def speed(self, val):
"""
Set the playback speed multiplier.
Parameters
----------
val : float
New speed value.
"""
self._speed = max(val, 0.01)
speed_str = f"{self._speed}".strip("0").rstrip(".") + "x"
self.speed_text.message = speed_str if speed_str and speed_str != "." else "0"
# class TextBox2D(UI):
# """An editable 2D text box that behaves as a UI component.
# Currently supports:
# - Basic text editing.
# - Cursor movements.
# - Single and multi-line text boxes.
# - Pre text formatting (text needs to be formatted beforehand).
# Attributes
# ----------
# text : str
# The current text state.
# actor : :class:`vtkActor2d`
# The text actor.
# width : int
# The number of characters in a single line of text.
# height : int
# The number of lines in the textbox.
# window_left : int
# Left limit of visible text in the textbox.
# window_right : int
# Right limit of visible text in the textbox.
# caret_pos : int
# Position of the caret in the text.
# init : bool
# Flag which says whether the textbox has just been initialized.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# width,
# height,
# *,
# text="Enter Text",
# position=(100, 10),
# color=(0, 0, 0),
# font_size=18,
# font_family="Arial",
# justification="left",
# bold=False,
# italic=False,
# shadow=False,
# ):
# """Init this UI element.
# Parameters
# ----------
# width : int
# The number of characters in a single line of text.
# height : int
# The number of lines in the textbox.
# text : str
# The initial text while building the actor.
# position : (float, float)
# (x, y) in pixels.
# color : (float, float, float)
# RGB: Values must be between 0-1.
# font_size : int
# Size of the text font.
# font_family : str
# Currently only supports Arial.
# justification : str
# left, right or center.
# bold : bool
# Makes text bold.
# italic : bool
# Makes text italicised.
# shadow : bool
# Adds text shadow.
# """
# super(TextBox2D, self).__init__(position=position)
# self.message = text
# self.text.message = text
# self.text.font_size = font_size
# self.text.font_family = font_family
# self.text.justification = justification
# self.text.bold = bold
# self.text.italic = italic
# self.text.shadow = shadow
# self.text.color = color
# self.text.background_color = (1, 1, 1)
# self.width = width
# self.height = height
# self.window_left = 0
# self.window_right = 0
# self.caret_pos = 0
# self.init = True
# self.off_focus = lambda ui: None
# def _setup(self):
# """Setup this UI component.
# Create the TextBlock2D component used for the textbox.
# """
# self.text = TextBlock2D(dynamic_bbox=True)
# # Add default events listener for this UI component.
# self.text.on_left_mouse_button_pressed = self.left_button_press
# self.text.on_key_press = self.key_press
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.text.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.text.add_to_scene(scene)
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.text.position = coords
# def _get_size(self):
# return self.text.size
# def set_message(self, message):
# """Set custom text to textbox.
# Parameters
# ----------
# message: str
# The custom message to be set.
# """
# self.message = message
# self.text.message = message
# self.init = False
# self.window_right = len(self.message)
# self.window_left = 0
# self.caret_pos = self.window_right
# def width_set_text(self, text):
# """Add newlines to text where necessary.
# This is needed for multi-line text boxes.
# Parameters
# ----------
# text : str
# The final text to be formatted.
# Returns
# -------
# str
# A multi line formatted text.
# """
# multi_line_text = ""
# for i, t in enumerate(text):
# multi_line_text += t
# if (i + 1) % self.width == 0:
# multi_line_text += "\n"
# return multi_line_text.rstrip("\n")
# def handle_character(self, key, key_char):
# """Handle button events.
# # TODO: Need to handle all kinds of characters like !, +, etc.
# Parameters
# ----------
# character : str
# """
# if key.lower() == "return":
# self.render_text(show_caret=False)
# self.off_focus(self)
# return True
# elif key_char != "" and key_char in printable:
# self.add_character(key_char)
# if key.lower() == "backspace":
# self.remove_character()
# elif key.lower() == "left":
# self.move_left()
# elif key.lower() == "right":
# self.move_right()
# self.render_text()
# return False
# def move_caret_right(self):
# """Move the caret towards right."""
# self.caret_pos = min(self.caret_pos + 1, len(self.message))
# def move_caret_left(self):
# """Move the caret towards left."""
# self.caret_pos = max(self.caret_pos - 1, 0)
# def right_move_right(self):
# """Move right boundary of the text window right-wards."""
# if self.window_right <= len(self.message):
# self.window_right += 1
# def right_move_left(self):
# """Move right boundary of the text window left-wards."""
# if self.window_right > 0:
# self.window_right -= 1
# def left_move_right(self):
# """Move left boundary of the text window right-wards."""
# if self.window_left <= len(self.message):
# self.window_left += 1
# def left_move_left(self):
# """Move left boundary of the text window left-wards."""
# if self.window_left > 0:
# self.window_left -= 1
# def add_character(self, character):
# """Insert a character into the text and moves window and caret.
# Parameters
# ----------
# character : str
# """
# if len(character) > 1 and character.lower() != "space":
# return
# if character.lower() == "space":
# character = " "
# self.message = (
#
# self.message[: self.caret_pos] + character + self.message[self.caret_pos :]
# )
# self.move_caret_right()
# if self.window_right - self.window_left == self.height * self.width - 1:
# self.left_move_right()
# self.right_move_right()
# def remove_character(self):
# """Remove a character and moves window and caret accordingly."""
# if self.caret_pos == 0:
# return
# self.message = (
# self.message[: self.caret_pos - 1] + self.message[self.caret_pos :]
# )
# self.move_caret_left()
# if len(self.message) < self.height * self.width - 1:
# self.right_move_left()
# if self.window_right - self.window_left == self.height * self.width - 1:
# if self.window_left > 0:
# self.left_move_left()
# self.right_move_left()
# def move_left(self):
# """Handle left button press."""
# self.move_caret_left()
# if self.caret_pos == self.window_left - 1:
# if self.window_right - self.window_left == self.height * self.width - 1:
# self.left_move_left()
# self.right_move_left()
# def move_right(self):
# """Handle right button press."""
# self.move_caret_right()
# if self.caret_pos == self.window_right + 1:
# if self.window_right - self.window_left == self.height * self.width - 1:
# self.left_move_right()
# self.right_move_right()
# def showable_text(self, show_caret):
# """Chop out text to be shown on the screen.
# Parameters
# ----------
# show_caret : bool
# Whether or not to show the caret.
# """
# if show_caret:
# ret_text = (
# self.message[: self.caret_pos] + "_" + self.message[self.caret_pos :]
# )
# else:
# ret_text = self.message
# ret_text = ret_text[self.window_left : self.window_right + 1]
# return ret_text
# @warn_on_args_to_kwargs()
# def render_text(self, *, show_caret=True):
# """Render text after processing.
# Parameters
# ----------
# show_caret : bool
# Whether or not to show the caret.
# """
# text = self.showable_text(show_caret)
# if text == "":
# text = "Enter Text"
# self.text.message = self.width_set_text(text)
# def edit_mode(self):
# """Turn on edit mode."""
# if self.init:
# self.message = ""
# self.init = False
# self.caret_pos = 0
# self.render_text()
# def left_button_press(self, i_ren, _obj, _textbox_object):
# """Handle left button press for textbox.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _textbox_object: :class:`TextBox2D`
# """
# i_ren.add_active_prop(self.text.actor)
# self.edit_mode()
# i_ren.force_render()
# def key_press(self, i_ren, _obj, _textbox_object):
# """Handle Key press for textboxself.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _textbox_object: :class:`TextBox2D`
# """
# key = i_ren.event.key
# key_char = i_ren.event.key_char
# is_done = self.handle_character(key, key_char)
# if is_done:
# i_ren.remove_active_prop(self.text.actor)
# i_ren.force_render()
# class LineDoubleSlider2D(UI):
# """A 2D Line Slider with two sliding rings.
# Useful for setting min and max values for something.
# Currently supports:
# - Setting positions of both disks.
# Attributes
# ----------
# line_width : int
# Width of the line on which the disk will slide.
# length : int
# Length of the slider.
# track : :class:`vtkActor`
# The line on which the handles move.
# handles : [:class:`vtkActor`, :class:`vtkActor`]
# The moving slider disks.
# text : [:class:`TextBlock2D`, :class:`TextBlock2D`]
# The texts that show the values of the disks.
# shape : string
# Describes the shape of the handle.
# Currently supports 'disk' and 'square'.
# default_color : (float, float, float)
# Color of the handles when in unpressed state.
# active_color : (float, float, float)
# Color of the handles when they are pressed.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# *,
# line_width=5,
# inner_radius=0,
# outer_radius=10,
# handle_side=20,
# center=(450, 300),
# length=200,
# initial_values=(0, 100),
# min_value=0,
# max_value=100,
# font_size=16,
# text_template="{value:.1f}",
# orientation="horizontal",
# shape="disk",
# ):
# """Init this UI element.
# Parameters
# ----------
# line_width : int
# Width of the line on which the disk will slide.
# inner_radius : int
# Inner radius of the handles (if disk).
# outer_radius : int
# Outer radius of the handles (if disk).
# handle_side : int
# Side length of the handles (if square).
# center : (float, float)
# Center of the slider.
# length : int
# Length of the slider.
# initial_values : (float, float)
# Initial values of the two handles.
# min_value : float
# Minimum value of the slider.
# max_value : float
# Maximum value of the slider.
# font_size : int
# Size of the text to display alongside the slider (pt).
# text_template : str, callable
# If str, text template can contain one or multiple of the
# replacement fields: `{value:}`, `{ratio:}`.
# If callable, this instance of `:class:LineDoubleSlider2D` will be
# passed as argument to the text template function.
# orientation : str
# horizontal or vertical
# shape : string
# Describes the shape of the handle.
# Currently supports 'disk' and 'square'.
# """
# self.shape = shape
# self.default_color = (1, 1, 1)
# self.active_color = (0, 0, 1)
# self.orientation = orientation.lower()
# super(LineDoubleSlider2D, self).__init__()
# if self.orientation == "horizontal":
# self.track.width = length
# self.track.height = line_width
# elif self.orientation == "vertical":
# self.track.width = line_width
# self.track.height = length
# else:
# raise ValueError("Unknown orientation")
# self.center = center
# if shape == "disk":
# self.handles[0].inner_radius = inner_radius
# self.handles[0].outer_radius = outer_radius
# self.handles[1].inner_radius = inner_radius
# self.handles[1].outer_radius = outer_radius
# elif shape == "square":
# self.handles[0].width = handle_side
# self.handles[0].height = handle_side
# self.handles[1].width = handle_side
# self.handles[1].height = handle_side
# self.min_value = min_value
# self.max_value = max_value
# self.text[0].font_size = font_size
# self.text[1].font_size = font_size
# self.text_template = text_template
# # Offer some standard hooks to the user.
# self.on_change = lambda ui: None
# self.on_value_changed = lambda ui: None
# self.on_moving_slider = lambda ui: None
# # Setting the handle positions will also update everything.
# self._values = [initial_values[0], initial_values[1]]
# self._ratio = [None, None]
# self.left_disk_value = initial_values[0]
# self.right_disk_value = initial_values[1]
# self.bottom_disk_value = initial_values[0]
# self.top_disk_value = initial_values[1]
# def _setup(self):
# """Setup this UI component.
# Create the slider's track (Rectangle2D), the handles (Disk2D) and
# the text (TextBlock2D).
# """
# # Slider's track
# self.track = Rectangle2D()
# self.track.color = (1, 0, 0)
# # Handles
# self.handles = []
# if self.shape == "disk":
# self.handles.append(Disk2D(outer_radius=1))
# self.handles.append(Disk2D(outer_radius=1))
# elif self.shape == "square":
# self.handles.append(Rectangle2D(size=(1, 1)))
# self.handles.append(Rectangle2D(size=(1, 1)))
# self.handles[0].color = self.default_color
# self.handles[1].color = self.default_color
# # Slider Text
# self.text = [
# TextBlock2D(justification="center", vertical_justification="top"),
# TextBlock2D(justification="center", vertical_justification="top"),
# ]
# # Add default events listener for this UI component.
# self.track.on_left_mouse_button_dragged = self.handle_move_callback
# self.handles[0].on_left_mouse_button_dragged = self.handle_move_callback
# self.handles[1].on_left_mouse_button_dragged = self.handle_move_callback
# self.handles[0].on_left_mouse_button_released = self.handle_release_callback
# self.handles[1].on_left_mouse_button_released = self.handle_release_callback
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return (
# self.track.actors
# + self.handles[0].actors
# + self.handles[1].actors
# + self.text[0].actors
# + self.text[1].actors
# )
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.track.add_to_scene(scene)
# self.handles[0].add_to_scene(scene)
# self.handles[1].add_to_scene(scene)
# self.text[0].add_to_scene(scene)
# self.text[1].add_to_scene(scene)
# def _get_size(self):
# # Consider the handle's size when computing the slider's size.
# width = None
# height = None
# if self.orientation == "horizontal":
# width = self.track.width + self.handles[0].size[0]
# height = max(self.track.height, self.handles[0].size[1])
# else:
# width = max(self.track.width, self.handles[0].size[0])
# height = self.track.height + self.handles[0].size[1]
# return np.array([width, height])
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# # Offset the slider line by the handle's radius.
# track_position = coords
# if self.orientation == "horizontal":
# # Offset the slider line height by half the slider line width.
# track_position[1] -= self.track.size[1] / 2.0
# else:
# # Offset the slider line width by half the slider line height.
# track_position[0] -= self.track.size[0] / 2.0
# self.track.position = track_position
# self.handles[0].position = self.handles[0].position.astype(float)
# self.handles[1].position = self.handles[1].position.astype(float)
# self.handles[0].position += coords - self.position
# self.handles[1].position += coords - self.position
# if self.orientation == "horizontal":
# # Position the text below the handles.
# self.text[0].position = (
# self.handles[0].center[0],
# self.handles[0].position[1] - 10,
# )
# self.text[1].position = (
# self.handles[1].center[0],
# self.handles[1].position[1] - 10,
# )
# else:
# # Position the text to the left of the handles.
# self.text[0].position = (
# self.handles[0].center[0] - 35,
# self.handles[0].position[1],
# )
# self.text[1].position = (
# self.handles[1].center[0] - 35,
# self.handles[1].position[1],
# )
# @property
# def bottom_y_position(self):
# return self.track.position[1]
# @property
# def top_y_position(self):
# return self.track.position[1] + self.track.size[1]
# @property
# def left_x_position(self):
# return self.track.position[0]
# @property
# def right_x_position(self):
# return self.track.position[0] + self.track.size[0]
# def value_to_ratio(self, value):
# """Convert the value of a disk to the ratio.
# Parameters
# ----------
# value : float
# """
# value_range = self.max_value - self.min_value
# return (value - self.min_value) / value_range if value_range else 0
# def ratio_to_coord(self, ratio):
# """Convert the ratio to the absolute coordinate.
# Parameters
# ----------
# ratio : float
# """
# if self.orientation == "horizontal":
# return self.left_x_position + ratio * self.track.width
# return self.bottom_y_position + ratio * self.track.height
# def coord_to_ratio(self, coord):
# """Convert the x coordinate of a disk to the ratio.
# Parameters
# ----------
# coord : float
# """
# if self.orientation == "horizontal":
# return (coord - self.left_x_position) / float(self.track.width)
# return (coord - self.bottom_y_position) / float(self.track.height)
# def ratio_to_value(self, ratio):
# """Convert the ratio to the value of the disk.
# Parameters
# ----------
# ratio : float
# """
# value_range = self.max_value - self.min_value
# return self.min_value + ratio * value_range
# def set_position(self, position, disk_number):
# """Set the disk's position.
# Parameters
# ----------
# position : (float, float)
# The absolute position of the disk (x, y).
# disk_number : int
# The index of disk being moved.
# """
# if self.orientation == "horizontal":
# x_position = position[0]
# if disk_number == 0 and x_position >= self.handles[1].center[0]:
# x_position = self.ratio_to_coord(
# self.value_to_ratio(self._values[1] - 1)
# )
# if disk_number == 1 and x_position <= self.handles[0].center[0]:
# x_position = self.ratio_to_coord(
# self.value_to_ratio(self._values[0] + 1)
# )
# x_position = max(x_position, self.left_x_position)
# x_position = min(x_position, self.right_x_position)
# self.handles[disk_number].center = (x_position, self.track.center[1])
# else:
# y_position = position[1]
# if disk_number == 0 and y_position >= self.handles[1].center[1]:
# y_position = self.ratio_to_coord(
# self.value_to_ratio(self._values[1] - 1)
# )
# if disk_number == 1 and y_position <= self.handles[0].center[1]:
# y_position = self.ratio_to_coord(
# self.value_to_ratio(self._values[0] + 1)
# )
# y_position = max(y_position, self.bottom_y_position)
# y_position = min(y_position, self.top_y_position)
# self.handles[disk_number].center = (self.track.center[0], y_position)
# self.update(disk_number)
# @property
# def bottom_disk_value(self):
# """Return the value of the bottom disk."""
# return self._values[0]
# @bottom_disk_value.setter
# def bottom_disk_value(self, bottom_disk_value):
# """Set the value of the bottom disk.
# Parameters
# ----------
# bottom_disk_value : float
# New value for the bottom disk.
# """
# self.bottom_disk_ratio = self.value_to_ratio(bottom_disk_value)
# @property
# def top_disk_value(self):
# """Return the value of the top disk."""
# return self._values[1]
# @top_disk_value.setter
# def top_disk_value(self, top_disk_value):
# """Set the value of the top disk.
# Parameters
# ----------
# top_disk_value : float
# New value for the top disk.
# """
# self.top_disk_ratio = self.value_to_ratio(top_disk_value)
# @property
# def left_disk_value(self):
# """Return the value of the left disk."""
# return self._values[0]
# @left_disk_value.setter
# def left_disk_value(self, left_disk_value):
# """Set the value of the left disk.
# Parameters
# ----------
# left_disk_value : float
# New value for the left disk.
# """
# self.left_disk_ratio = self.value_to_ratio(left_disk_value)
# self.on_value_changed(self)
# @property
# def right_disk_value(self):
# """Return the value of the right disk."""
# return self._values[1]
# @right_disk_value.setter
# def right_disk_value(self, right_disk_value):
# """Set the value of the right disk.
# Parameters
# ----------
# right_disk_value : float
# New value for the right disk.
# """
# self.right_disk_ratio = self.value_to_ratio(right_disk_value)
# self.on_value_changed(self)
# @property
# def bottom_disk_ratio(self):
# """Return the ratio of the bottom disk."""
# return self._ratio[0]
# @bottom_disk_ratio.setter
# def bottom_disk_ratio(self, bottom_disk_ratio):
# """Set the ratio of the bottom disk.
# Parameters
# ----------
# bottom_disk_ratio : float
# New ratio for the bottom disk.
# """
# position_x = self.ratio_to_coord(bottom_disk_ratio)
# position_y = self.ratio_to_coord(bottom_disk_ratio)
# self.set_position((position_x, position_y), 0)
# @property
# def top_disk_ratio(self):
# """Return the ratio of the top disk."""
# return self._ratio[1]
# @top_disk_ratio.setter
# def top_disk_ratio(self, top_disk_ratio):
# """Set the ratio of the top disk.
# Parameters
# ----------
# top_disk_ratio : float
# New ratio for the top disk.
# """
# position_x = self.ratio_to_coord(top_disk_ratio)
# position_y = self.ratio_to_coord(top_disk_ratio)
# self.set_position((position_x, position_y), 1)
# @property
# def left_disk_ratio(self):
# """Return the ratio of the left disk."""
# return self._ratio[0]
# @left_disk_ratio.setter
# def left_disk_ratio(self, left_disk_ratio):
# """Set the ratio of the left disk.
# Parameters
# ----------
# left_disk_ratio : float
# New ratio for the left disk.
# """
# position_x = self.ratio_to_coord(left_disk_ratio)
# position_y = self.ratio_to_coord(left_disk_ratio)
# self.set_position((position_x, position_y), 0)
# @property
# def right_disk_ratio(self):
# """Return the ratio of the right disk."""
# return self._ratio[1]
# @right_disk_ratio.setter
# def right_disk_ratio(self, right_disk_ratio):
# """Set the ratio of the right disk.
# Parameters
# ----------
# right_disk_ratio : float
# New ratio for the right disk.
# """
# position_x = self.ratio_to_coord(right_disk_ratio)
# position_y = self.ratio_to_coord(right_disk_ratio)
# self.set_position((position_x, position_y), 1)
# def format_text(self, disk_number):
# """Return formatted text to display along the slider.
# Parameters
# ----------
# disk_number : int
# Index of the disk.
# """
# if callable(self.text_template):
# return self.text_template(self)
# return self.text_template.format(value=self._values[disk_number])
# def update(self, disk_number):
# """Update the slider.
# Parameters
# ----------
# disk_number : int
# Index of the disk to be updated.
# """
# # Compute the ratio determined by the position of the slider disk.
# if self.orientation == "horizontal":
# self._ratio[disk_number] = self.coord_to_ratio(
# self.handles[disk_number].center[0]
# )
# else:
# self._ratio[disk_number] = self.coord_to_ratio(
# self.handles[disk_number].center[1]
# )
# # Compute the selected value considering min_value and max_value.
# self._values[disk_number] = self.ratio_to_value(self._ratio[disk_number])
# # Update text.
# text = self.format_text(disk_number)
# self.text[disk_number].message = text
# if self.orientation == "horizontal":
# self.text[disk_number].position = (
# self.handles[disk_number].center[0],
# self.text[disk_number].position[1],
# )
# else:
# self.text[disk_number].position = (
# self.text[disk_number].position[0],
# self.handles[disk_number].center[1],
# )
# self.on_change(self)
# def handle_move_callback(self, i_ren, vtkactor, _slider):
# """Handle movement.
# Parameters
# ----------
# i_ren : :class:`CustomInteractorStyle`
# vtkactor : :class:`vtkActor`
# The picked actor
# _slider : :class:`LineDoubleSlider2D`
# """
# position = i_ren.event.position
# if vtkactor == self.handles[0].actors[0]:
# self.set_position(position, 0)
# self.handles[0].color = self.active_color
# elif vtkactor == self.handles[1].actors[0]:
# self.set_position(position, 1)
# self.handles[1].color = self.active_color
# self.on_moving_slider(self)
# i_ren.force_render()
# i_ren.event.abort() # Stop propagating the event.
# def handle_release_callback(self, i_ren, vtkactor, _slider):
# """Change color when handle is released.
# Parameters
# ----------
# i_ren : :class:`CustomInteractorStyle`
# vtkactor : :class:`vtkActor`
# The picked actor
# _slider : :class:`LineDoubleSlider2D`
# """
# if vtkactor == self.handles[0].actors[0]:
# self.handles[0].color = self.default_color
# elif vtkactor == self.handles[1].actors[0]:
# self.handles[1].color = self.default_color
# i_ren.force_render()
[docs]
class RingSlider2D(Slider2D):
"""
A disk slider.
A disk moves along the boundary of a ring.
Goes from 0-360 degrees.
Parameters
----------
center : (float, float), optional
Position (x, y) of the slider's center.
initial_value : float, optional
Initial value of the slider.
min_value : float, optional
Minimum value of the slider.
max_value : float, optional
Maximum value of the slider.
slider_inner_radius : int, optional
Inner radius of the base disk.
slider_outer_radius : int, optional
Outer radius of the base disk.
handle_inner_radius : int, optional
Inner radius of the slider's handle.
handle_outer_radius : int, optional
Outer radius of the slider's handle.
handle_side : int, optional
The side length of the square handle when shape="square".
font_size : int, optional
Size of the text to display alongside the slider (pt).
text_template : str or callable, optional
If str, text template can contain one or multiple of the
replacement fields: `{value:}`, `{ratio:}`, `{angle:}`.
If callable, this instance of `:class:RingSlider2D` will be
passed as argument to the text template function.
shape : str, optional
The handle shape. Supported values are "disk" and "square".
z_order : int, optional
Stacking priority of the slider. The handle and text
are placed above the track.
Attributes
----------
track : :class:`Disk2D`
The circle on which the slider's handle moves.
handle : :class:`Disk2D`
The moving part of the slider.
text : :class:`TextBlock2D`
The text that shows percentage.
default_color : (float, float, float)
Color of the handle when in unpressed state.
active_color : (float, float, float)
Color of the handle when it is pressed.
"""
[docs]
def __init__(
self,
*,
center=(0, 0),
initial_value=0,
min_value=0,
max_value=360,
slider_inner_radius=40,
slider_outer_radius=44,
handle_inner_radius=0,
handle_outer_radius=10,
handle_side=20,
font_size=16,
text_template="{ratio:.0%}",
shape="disk",
z_order=0,
):
"""
Init this UI element.
Parameters
----------
center : (float, float), optional
Position (x, y) of the slider's center.
initial_value : float, optional
Initial value of the slider.
min_value : float, optional
Minimum value of the slider.
max_value : float, optional
Maximum value of the slider.
slider_inner_radius : int, optional
Inner radius of the base disk.
slider_outer_radius : int, optional
Outer radius of the base disk.
handle_inner_radius : int, optional
Inner radius of the slider's handle.
handle_outer_radius : int, optional
Outer radius of the slider's handle.
handle_side : int, optional
The side length of the square handle when shape="square".
font_size : int, optional
Size of the text to display alongside the slider (pt).
text_template : str or callable, optional
If str, text template can contain one or multiple of the
replacement fields: `{value:}`, `{ratio:}`, `{angle:}`.
If callable, this instance of `:class:RingSlider2D` will be
passed as argument to the text template function.
shape : str, optional
The handle shape. Supported values are "disk" and "square".
z_order : int, optional
Stacking priority of the slider. The handle and text
are placed above the track.
"""
self._track_inner_radius = slider_inner_radius
self._track_outer_radius = slider_outer_radius
self._angle = 0.0
super(RingSlider2D, self).__init__(
position=center,
initial_value=initial_value,
min_value=min_value,
max_value=max_value,
handle_inner_radius=handle_inner_radius,
handle_outer_radius=handle_outer_radius,
handle_side=handle_side,
font_size=font_size,
text_template=text_template,
shape=shape,
z_order=z_order,
)
self.value = initial_value
def _setup(self):
"""
Setup this UI component.
Create the slider's circle (Disk2D), the handle (Disk2D) and the
text (TextBlock2D).
"""
super(RingSlider2D, self)._setup()
self.track = Disk2D(
outer_radius=self._track_outer_radius,
inner_radius=self._track_inner_radius,
)
self.track.color = (1, 0, 0)
self.track.z_order = self.z_order
self.handle.color = self.default_color
self.track.on_left_mouse_button_pressed = self.track_click_callback
self.track.on_left_mouse_button_dragged = self.handle_move_callback
self.track.on_left_mouse_button_released = self.handle_release_callback
self.handle.on_left_mouse_button_dragged = self.handle_move_callback
self.handle.on_left_mouse_button_released = self.handle_release_callback
self._children.append(self.track)
def _get_size(self):
"""
Get the size of this UI component.
Returns
-------
ndarray
The size of the component.
"""
diameter = 2 * (self._track_outer_radius + self._handle_outer_radius)
return np.array([diameter, diameter])
def _update_actors_position(self):
"""Update the position of the internal actors."""
center = self.get_position(x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
self.track.set_position(center, x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
self._update_handle_position()
@property
def mid_track_radius(self):
"""
Return the distance from the center of the slider to the track middle.
Returns
-------
float
The mid track radius.
"""
return (self.track.inner_radius + self.track.outer_radius) / 2.0
def _update_handle_position(self):
"""
Place the handle and the text according to the current angle / ratio.
"""
center = self.track.get_position(x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
angle = self.angle
x = self.mid_track_radius * np.sin(angle) + center[0]
y = center[1] - self.mid_track_radius * np.cos(angle)
self.handle.set_position((x, y), x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
self.text.message = self.format_text()
self.text.set_position(center, x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
@property
def angle(self):
"""
Return Angle (in rad) the handle makes with the y-axis.
Returns
-------
float
The angle.
"""
angle = self.ratio * TWO_PI
if np.isclose(angle, TWO_PI):
angle = 0.0
return angle
[docs]
def handle_move_callback(self, event):
"""
Handle mouse drag events to update the slider state.
Parameters
----------
event : PointerEvent
The PyGfx pointer event.
"""
self.handle.color = self.active_color
center = self.get_position(x_anchor=Anchor.CENTER, y_anchor=Anchor.CENTER)
x, y = event.x - center[0], center[1] - event.y
angle = np.arctan2(x, y) % TWO_PI
ratio = angle / TWO_PI
if np.isclose(ratio, 1.0):
ratio = 0.0
angle = 0.0
self._angle = angle
self.ratio = ratio
self.on_moving_slider(self)
# class RangeSlider(UI):
# """A set of a LineSlider2D and a LineDoubleSlider2D.
# The double slider is used to set the min and max value
# for the LineSlider2D
# Attributes
# ----------
# range_slider_center : (float, float)
# Center of the LineDoubleSlider2D object.
# value_slider_center : (float, float)
# Center of the LineSlider2D object.
# range_slider : :class:`LineDoubleSlider2D`
# The line slider which sets the min and max values
# value_slider : :class:`LineSlider2D`
# The line slider which sets the value
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# *,
# line_width=5,
# inner_radius=0,
# outer_radius=10,
# handle_side=20,
# range_slider_center=(450, 400),
# value_slider_center=(450, 300),
# length=200,
# min_value=0,
# max_value=100,
# font_size=16,
# range_precision=1,
# orientation="horizontal",
# value_precision=2,
# shape="disk",
# ):
# """Init this class instance.
# Parameters
# ----------
# line_width : int
# Width of the slider tracks
# inner_radius : int
# Inner radius of the handles.
# outer_radius : int
# Outer radius of the handles.
# handle_side : int
# Side length of the handles (if square).
# range_slider_center : (float, float)
# Center of the LineDoubleSlider2D object.
# value_slider_center : (float, float)
# Center of the LineSlider2D object.
# length : int
# Length of the sliders.
# min_value : float
# Minimum value of the double slider.
# max_value : float
# Maximum value of the double slider.
# font_size : int
# Size of the text to display alongside the sliders (pt).
# range_precision : int
# Number of decimal places to show the min and max values set.
# orientation : str
# horizontal or vertical
# value_precision : int
# Number of decimal places to show the value set on slider.
# shape : string
# Describes the shape of the handle.
# Currently supports 'disk' and 'square'.
# """
# self.min_value = min_value
# self.max_value = max_value
# self.inner_radius = inner_radius
# self.outer_radius = outer_radius
# self.handle_side = handle_side
# self.length = length
# self.line_width = line_width
# self.font_size = font_size
# self.shape = shape
# self.orientation = orientation.lower()
# self.range_slider_text_template = "{value:." + str(range_precision) + "f}"
# self.value_slider_text_template = "{value:." + str(value_precision) + "f}"
# self.range_slider_center = range_slider_center
# self.value_slider_center = value_slider_center
# super(RangeSlider, self).__init__()
# def _setup(self):
# """Setup this UI component."""
# self.range_slider = LineDoubleSlider2D(
# line_width=self.line_width,
# inner_radius=self.inner_radius,
# outer_radius=self.outer_radius,
# handle_side=self.handle_side,
# center=self.range_slider_center,
# length=self.length,
# min_value=self.min_value,
# max_value=self.max_value,
# initial_values=(self.min_value, self.max_value),
# font_size=self.font_size,
# shape=self.shape,
# orientation=self.orientation,
# text_template=self.range_slider_text_template,
# )
# self.value_slider = LineSlider2D(
# line_width=self.line_width,
# length=self.length,
# inner_radius=self.inner_radius,
# outer_radius=self.outer_radius,
# handle_side=self.handle_side,
# center=self.value_slider_center,
# min_value=self.min_value,
# max_value=self.max_value,
# initial_value=(self.min_value + self.max_value) / 2,
# font_size=self.font_size,
# shape=self.shape,
# orientation=self.orientation,
# text_template=self.value_slider_text_template,
# )
# # Add default events listener for this UI component.
# self.range_slider.handles[
# 0
# ].on_left_mouse_button_dragged = self.range_slider_handle_move_callback
# self.range_slider.handles[
# 1
# ].on_left_mouse_button_dragged = self.range_slider_handle_move_callback
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.range_slider.actors + self.value_slider.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.range_slider.add_to_scene(scene)
# self.value_slider.add_to_scene(scene)
# def _get_size(self):
# return self.range_slider.size + self.value_slider.size
# def _set_position(self, coords):
# pass
# def range_slider_handle_move_callback(self, i_ren, obj, _slider):
# """Update range_slider's handles.
# Parameters
# ----------
# i_ren : :class:`CustomInteractorStyle`
# obj : :class:`vtkActor`
# The picked actor
# _slider : :class:`RangeSlider`
# """
# position = i_ren.event.position
# if obj == self.range_slider.handles[0].actors[0]:
# self.range_slider.handles[0].color = self.range_slider.active_color
# self.range_slider.set_position(position, 0)
# self.value_slider.min_value = self.range_slider.left_disk_value
# self.value_slider.update()
# elif obj == self.range_slider.handles[1].actors[0]:
# self.range_slider.handles[1].color = self.range_slider.active_color
# self.range_slider.set_position(position, 1)
# self.value_slider.max_value = self.range_slider.right_disk_value
# self.value_slider.update()
# i_ren.force_render()
# i_ren.event.abort() # Stop propagating the event.
# class Option(UI):
# """A set of a Button2D and a TextBlock2D to act as a single option
# for checkboxes and radio buttons.
# Clicking the button toggles its checked/unchecked status.
# Attributes
# ----------
# label : str
# The label for the option.
# font_size : int
# Font Size of the label.
# """
# @warn_on_args_to_kwargs()
# def __init__(self, label, *, position=(0, 0), font_size=18, checked=False):
# """Init this class instance.
# Parameters
# ----------
# label : str
# Text to be displayed next to the option's button.
# position : (float, float)
# Absolute coordinates (x, y) of the lower-left corner of
# the button of the option.
# font_size : int
# Font size of the label.
# checked : bool, optional
# Boolean value indicates the initial state of the option
# """
# self.label = label
# self.font_size = font_size
# self.checked = checked
# self.button_size = (font_size * 1.2, font_size * 1.2)
# self.button_label_gap = 10
# super(Option, self).__init__(position=position)
# # Offer some standard hooks to the user.
# self.on_change = lambda obj: None
# def _setup(self):
# """Setup this UI component."""
# # Option's button
# self.button_icons = []
# self.button_icons.append(("unchecked", read_viz_icons(fname="stop2.png")))
# self.button_icons.append(("checked", read_viz_icons(fname="checkmark.png")))
# self.button = Button2D(icon_fnames=self.button_icons, size=self.button_size)
# self.text = TextBlock2D(text=self.label, font_size=self.font_size)
# # Display initial state
# if self.checked:
# self.button.set_icon_by_name("checked")
# # Add callbacks
# self.button.on_left_mouse_button_clicked = self.toggle
# self.text.on_left_mouse_button_clicked = self.toggle
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.button.actors + self.text.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.button.add_to_scene(scene)
# self.text.add_to_scene(scene)
# def _get_size(self):
# width = self.button.size[0] + self.button_label_gap + self.text.size[0]
# height = max(self.button.size[1], self.text.size[1])
# return np.array([width, height])
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# num_newlines = self.label.count("\n")
# self.button.position = coords + (0, num_newlines * self.font_size * 0.5)
# offset = (self.button.size[0] + self.button_label_gap, 0)
# self.text.position = coords + offset
# def toggle(self, i_ren, _obj, _element):
# if self.checked:
# self.deselect()
# else:
# self.select()
# self.on_change(self)
# i_ren.force_render()
# def select(self):
# self.checked = True
# self.button.set_icon_by_name("checked")
# def deselect(self):
# self.checked = False
# self.button.set_icon_by_name("unchecked")
# class Checkbox(UI):
# """A 2D set of :class:'Option' objects.
# Multiple options can be selected.
# Attributes
# ----------
# labels : list(string)
# List of labels of each option.
# options : dict(Option)
# Dictionary of all the options in the checkbox set.
# padding : float
# Distance between two adjacent options
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# labels,
# *,
# checked_labels=(),
# padding=1,
# font_size=18,
# font_family="Arial",
# position=(0, 0),
# ):
# """Init this class instance.
# Parameters
# ----------
# labels : list(str)
# 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
# font_size : int, optional
# Size of the text font.
# font_family : str, optional
# Currently only supports Arial.
# position : (float, float), optional
# Absolute coordinates (x, y) of the lower-left corner of
# the button of the first option.
# """
# self.labels = list(reversed(list(labels)))
# self._padding = padding
# self._font_size = font_size
# self.font_family = font_family
# self.checked_labels = list(checked_labels)
# super(Checkbox, self).__init__(position=position)
# self.on_change = lambda checkbox: None
# def _setup(self):
# """Setup this UI component."""
# self.options = OrderedDict()
# button_y = self.position[1]
# for label in self.labels:
# option = Option(
# label=label,
# font_size=self.font_size,
# position=(self.position[0], button_y),
# checked=(label in self.checked_labels),
# )
# line_spacing = option.text.actor.GetTextProperty().GetLineSpacing()
# button_y = (
# button_y
# + self.font_size * (label.count("\n") + 1) * (line_spacing + 0.1)
# + self.padding
# )
# self.options[label] = option
# # Set callback
# option.on_change = self._handle_option_change
# def _get_actors(self):
# """Get the actors composing this UI component."""
# actors = []
# for option in self.options.values():
# actors = actors + option.actors
# return actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# for option in self.options.values():
# option.add_to_scene(scene)
# def _get_size(self):
# option_width, option_height = self.options.values()[0].get_size()
# height = len(self.labels) * (option_height + self.padding) - self.padding
# return np.asarray([option_width, height])
# def _handle_option_change(self, option):
# """Update whenever an option changes.
# Parameters
# ----------
# option : :class:`Option`
# """
# if option.checked:
# self.checked_labels.append(option.label)
# else:
# self.checked_labels.remove(option.label)
# self.on_change(self)
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# button_y = coords[1]
# for option_no, option in enumerate(self.options.values()):
# option.position = (coords[0], button_y)
# line_spacing = option.text.actor.GetTextProperty().GetLineSpacing()
# button_y = (
# button_y
# + self.font_size
# * (self.labels[option_no].count("\n") + 1)
# * (line_spacing + 0.1)
# + self.padding
# )
# @property
# def font_size(self):
# """Gets the font size of text."""
# return self._font_size
# @property
# def padding(self):
# """Get the padding between options."""
# return self._padding
# class RadioButton(Checkbox):
# """A 2D set of :class:'Option' objects.
# Only one option can be selected.
# Attributes
# ----------
# labels : list(string)
# List of labels of each option.
# options : dict(Option)
# Dictionary of all the options in the checkbox set.
# padding : float
# Distance between two adjacent options
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# labels,
# checked_labels,
# *,
# padding=1,
# font_size=18,
# font_family="Arial",
# position=(0, 0),
# ):
# """Init class instance.
# Parameters
# ----------
# labels : list(str)
# 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
# font_size : int, optional
# Size of the text font.
# font_family : str, optional
# Currently only supports Arial.
# position : (float, float), optional
# Absolute coordinates (x, y) of the lower-left corner of
# the button of the first option.
# """
# if len(checked_labels) > 1:
# err_msg = "Only one option can be preselected for radio buttons."
# raise ValueError(err_msg)
# super(RadioButton, self).__init__(
# labels=labels,
# position=position,
# padding=padding,
# font_size=font_size,
# font_family=font_family,
# checked_labels=checked_labels,
# )
# def _handle_option_change(self, option):
# for option_ in self.options.values():
# option_.deselect()
# option.select()
# self.checked_labels = [option.label]
# self.on_change(self)
# class ComboBox2D(UI):
# """UI element to create drop-down menus.
# Attributes
# ----------
# selection_box: :class: 'TextBox2D'
# Display selection and placeholder text.
# drop_down_button: :class: 'Button2D'
# Button to show or hide menu.
# drop_down_menu: :class: 'ListBox2D'
# Container for item list.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# *,
# items=None,
# position=(0, 0),
# size=(300, 200),
# placeholder="Choose selection...",
# draggable=True,
# selection_text_color=(0, 0, 0),
# selection_bg_color=(1, 1, 1),
# menu_text_color=(0.2, 0.2, 0.2),
# selected_color=(0.9, 0.6, 0.6),
# unselected_color=(0.6, 0.6, 0.6),
# scroll_bar_active_color=(0.6, 0.2, 0.2),
# scroll_bar_inactive_color=(0.9, 0.0, 0.0),
# menu_opacity=1.0,
# reverse_scrolling=False,
# font_size=20,
# line_spacing=1.4,
# ):
# """Init class Instance.
# Parameters
# ----------
# items: list(string)
# List of items to be displayed as choices.
# position : (float, float)
# Absolute coordinates (x, y) of the lower-left corner of this
# UI component.
# size : (int, int)
# Width and height in pixels of this UI component.
# placeholder : str
# Holds the default text to be displayed.
# draggable: {True, False}
# Whether the UI element is draggable or not.
# selection_text_color : tuple of 3 floats
# Color of the selected text to be displayed.
# selection_bg_color : tuple of 3 floats
# Background color of the selection text.
# menu_text_color : tuple of 3 floats.
# Color of the options displayed in drop down menu.
# selected_color : tuple of 3 floats.
# Background color of the selected option in drop down menu.
# unselected_color : tuple of 3 floats.
# Background color of the unselected option in drop down menu.
# scroll_bar_active_color : tuple of 3 floats.
# Color of the scrollbar when in active use.
# scroll_bar_inactive_color : tuple of 3 floats.
# Color of the scrollbar when inactive.
# reverse_scrolling: {True, False}
# If True, scrolling up will move the list of files down.
# font_size: int
# The font size of selected text in pixels.
# line_spacing: float
# Distance between drop down menu's items in pixels.
# """
# if items is None:
# items = []
# self.items = items.copy()
# self.font_size = font_size
# self.reverse_scrolling = reverse_scrolling
# self.line_spacing = line_spacing
# self.panel_size = size
# self._selection = placeholder
# self._menu_visibility = False
# self._selection_ID = None
# self.draggable = draggable
# self.sel_text_color = selection_text_color
# self.sel_bg_color = selection_bg_color
# self.menu_txt_color = menu_text_color
# self.selected_color = selected_color
# self.unselected_color = unselected_color
# self.scroll_active_color = scroll_bar_active_color
# self.scroll_inactive_color = scroll_bar_inactive_color
# self.menu_opacity = menu_opacity
# # Define subcomponent sizes.
# self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1]))
# self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1]))
# self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1]))
# self._icon_files = [
# ("left", read_viz_icons(fname="circle-left.png")),
# ("down", read_viz_icons(fname="circle-down.png")),
# ]
# super(ComboBox2D, self).__init__()
# self.position = position
# def _setup(self):
# """Setup this UI component.
# Create the ListBox filled with empty slots (ListBoxItem2D).
# Create TextBox with placeholder text.
# Create Button for toggling drop down menu.
# """
# self.selection_box = TextBlock2D(
# size=self.text_block_size,
# color=self.sel_text_color,
# bg_color=self.sel_bg_color,
# text=self._selection,
# )
# self.drop_down_button = Button2D(
# icon_fnames=self._icon_files, size=self.drop_button_size
# )
# self.drop_down_menu = ListBox2D(
# values=self.items,
# multiselection=False,
# font_size=self.font_size,
# line_spacing=self.line_spacing,
# text_color=self.menu_txt_color,
# selected_color=self.selected_color,
# unselected_color=self.unselected_color,
# scroll_bar_active_color=self.scroll_active_color,
# scroll_bar_inactive_color=self.scroll_inactive_color,
# background_opacity=self.menu_opacity,
# reverse_scrolling=self.reverse_scrolling,
# size=self.drop_menu_size,
# )
# self.drop_down_menu.set_visibility(False)
# self.panel = Panel2D(self.panel_size, opacity=0.0)
# self.panel.add_element(self.selection_box, (0.001, 0.7))
# self.panel.add_element(self.drop_down_button, (0.8, 0.7))
# self.panel.add_element(self.drop_down_menu, (0, 0))
# if self.draggable:
# self.drop_down_button.on_left_mouse_button_dragged = (
# self.left_button_dragged
# )
# self.drop_down_menu.panel.background.on_left_mouse_button_dragged = (
# self.left_button_dragged
# )
# self.selection_box.on_left_mouse_button_dragged = self.left_button_dragged
# self.selection_box.background.on_left_mouse_button_dragged = (
# self.left_button_dragged
# )
# self.drop_down_button.on_left_mouse_button_pressed = (
# self.left_button_pressed
# )
# self.drop_down_menu.panel.background.on_left_mouse_button_pressed = (
# self.left_button_pressed
# )
# self.selection_box.on_left_mouse_button_pressed = self.left_button_pressed
# self.selection_box.background.on_left_mouse_button_pressed = (
# self.left_button_pressed
# )
# else:
# self.panel.background.on_left_mouse_button_dragged = (
# lambda i_ren, _obj, _comp: i_ren.force_render
# )
# self.drop_down_menu.panel.background.on_left_mouse_button_dragged = (
# lambda i_ren, _obj, _comp: i_ren.force_render
# )
# # Handle mouse wheel events on the slots.
# for slot in self.drop_down_menu.slots:
# slot.add_callback(
# slot.textblock.actor,
# "LeftButtonPressEvent",
# self.select_option_callback,
# )
# slot.add_callback(
# slot.background.actor,
# "LeftButtonPressEvent",
# self.select_option_callback,
# )
# self.drop_down_button.on_left_mouse_button_clicked = self.menu_toggle_callback
# # Offer some standard hooks to the user.
# self.on_change = lambda ui: None
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.panel.actors
# def resize(self, size):
# """Resize ComboBox2D.
# Parameters
# ----------
# size : (int, int)
# ComboBox size(width, height) in pixels.
# """
# self.panel.resize(size)
# self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1]))
# self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1]))
# self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1]))
# self.panel.update_element(self.selection_box, (0.001, 0.7))
# self.panel.update_element(self.drop_down_button, (0.8, 0.7))
# self.panel.update_element(self.drop_down_menu, (0, 0))
# self.drop_down_button.resize(self.drop_button_size)
# self.drop_down_menu.resize(self.drop_menu_size)
# self.selection_box.resize(self.text_block_size)
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.panel.position = coords
# self.panel.position = (
# self.panel.position[0],
# self.panel.position[1] - self.drop_menu_size[1],
# )
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.panel.add_to_scene(scene)
# self.selection_box.font_size = self.font_size
# def _get_size(self):
# return self.panel.size
# @property
# def selected_text(self):
# return self._selection
# @property
# def selected_text_index(self):
# return self._selection_ID
# def set_visibility(self, visibility):
# super().set_visibility(visibility)
# if not self._menu_visibility:
# self.drop_down_menu.set_visibility(False)
# def append_item(self, *items):
# """Append additional options to the menu.
# Parameters
# ----------
# items : n-d list, n-d tuple, Number or str
# Additional options.
# """
# for item in items:
# if isinstance(item, (list, tuple)):
# # Useful when n-d lists/tuples are used.
# self.append_item(*item)
# elif isinstance(item, (str, Number)):
# self.items.append(str(item))
# else:
# raise TypeError("Invalid item instance {}".format(type(item)))
# self.drop_down_menu.update_scrollbar()
# if not self._menu_visibility:
# self.drop_down_menu.scroll_bar.set_visibility(False)
# def select_option_callback(self, i_ren, _obj, listboxitem):
# """Select the appropriate option
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# listboxitem: :class:`ListBoxItem2D`
# """
# # Set the Text of TextBlock2D to the text of listboxitem
# self._selection = listboxitem.element
# self._selection_ID = self.items.index(self._selection)
# self.selection_box.message = self._selection
# clip_overflow(self.selection_box, self.selection_box.background.size[0])
# self.drop_down_menu.set_visibility(False)
# self._menu_visibility = False
# self.drop_down_button.next_icon()
# self.on_change(self)
# i_ren.force_render()
# i_ren.event.abort()
# def menu_toggle_callback(self, i_ren, _vtkactor, _combobox):
# """Toggle visibility of drop down menu list.
# Parameters
# ----------
# i_ren : :class:`CustomInteractorStyle`
# vtkactor : :class:`vtkActor`
# The picked actor
# combobox : :class:`ComboBox2D`
# """
# self._menu_visibility = not self._menu_visibility
# self.drop_down_menu.set_visibility(self._menu_visibility)
# self.drop_down_button.next_icon()
# i_ren.force_render()
# i_ren.event.abort() # Stop propagating the event.
# def left_button_pressed(self, i_ren, _obj, _sub_component):
# click_pos = np.array(i_ren.event.position)
# self._click_position = click_pos
# i_ren.event.abort() # Stop propagating the event.
# def left_button_dragged(self, i_ren, _obj, _sub_component):
# click_position = np.array(i_ren.event.position)
# change = click_position - self._click_position
# self.panel.position += change
# self._click_position = click_position
# i_ren.force_render()
# class ListBox2D(UI):
# """UI component that allows the user to select items from a list.
# Attributes
# ----------
# on_change: function
# Callback function for when the selected items have changed.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# values,
# *,
# position=(0, 0),
# size=(100, 300),
# multiselection=True,
# reverse_scrolling=False,
# font_size=20,
# line_spacing=1.4,
# text_color=(0.2, 0.2, 0.2),
# selected_color=(0.9, 0.6, 0.6),
# unselected_color=(0.6, 0.6, 0.6),
# scroll_bar_active_color=(0.6, 0.2, 0.2),
# scroll_bar_inactive_color=(0.9, 0.0, 0.0),
# background_opacity=1.0,
# ):
# """Init class instance.
# Parameters
# ----------
# values: list of objects
# Values used to populate this listbox. Objects must be castable
# to string.
# position : (float, float)
# Absolute coordinates (x, y) of the lower-left corner of this
# UI component.
# size : (int, int)
# Width and height in pixels of this UI component.
# multiselection: {True, False}
# Whether multiple values can be selected at once.
# reverse_scrolling: {True, False}
# If True, scrolling up will move the list of files down.
# font_size: int
# The font size in pixels.
# line_spacing: float
# Distance between listbox's items in pixels.
# text_color : tuple of 3 floats
# selected_color : tuple of 3 floats
# unselected_color : tuple of 3 floats
# scroll_bar_active_color : tuple of 3 floats
# scroll_bar_inactive_color : tuple of 3 floats
# background_opacity : float
# """
# self.view_offset = 0
# self.slots = []
# self.selected = []
# self.panel_size = size
# self.font_size = font_size
# self.line_spacing = line_spacing
# self.slot_height = int(self.font_size * self.line_spacing)
# self.text_color = text_color
# self.selected_color = selected_color
# self.unselected_color = unselected_color
# self.background_opacity = background_opacity
# # self.panel.resize(size)
# self.values = values
# self.multiselection = multiselection
# self.last_selection_idx = 0
# self.reverse_scrolling = reverse_scrolling
# super(ListBox2D, self).__init__()
# denom = len(self.values) - self.nb_slots
# if not denom:
# denom += 1
# self.scroll_step_size = (
# self.slot_height * self.nb_slots - self.scroll_bar.height
# ) / denom
# self.scroll_bar_active_color = scroll_bar_active_color
# self.scroll_bar_inactive_color = scroll_bar_inactive_color
# self.scroll_bar.color = self.scroll_bar_inactive_color
# self.scroll_bar.opacity = self.background_opacity
# self.position = position
# self.scroll_init_position = 0
# self.update()
# # Offer some standard hooks to the user.
# self.on_change = lambda: None
# def _setup(self):
# """Setup this UI component.
# Create the ListBox (Panel2D) filled with empty slots (ListBoxItem2D).
# """
# self.margin = 10
# size = self.panel_size
# font_size = self.font_size
# # Calculating the number of slots.
# self.nb_slots = int((size[1] - 2 * self.margin) // self.slot_height)
# # This panel facilitates adding slots at the right position.
# self.panel = Panel2D(size=size, color=(1, 1, 1))
# # Add a scroll bar
# scroll_bar_height = (
# self.nb_slots * (size[1] - 2 * self.margin) / len(self.values)
# )
# self.scroll_bar = Rectangle2D(size=(int(size[0] / 20), scroll_bar_height))
# if len(self.values) <= self.nb_slots:
# self.scroll_bar.set_visibility(False)
# self.scroll_bar.height = 0
# self.panel.add_element(
# self.scroll_bar, size - self.scroll_bar.size - self.margin
# )
# # Initialisation of empty text actors
# self.slot_width = (
# size[0] - self.scroll_bar.size[0] - 2 * self.margin - self.margin
# )
# x = self.margin
# y = size[1] - self.margin
# for _ in range(self.nb_slots):
# y -= self.slot_height
# item = ListBoxItem2D(
# list_box=self,
# size=(self.slot_width, self.slot_height),
# text_color=self.text_color,
# selected_color=self.selected_color,
# unselected_color=self.unselected_color,
# background_opacity=self.background_opacity,
# )
# item.textblock.font_size = font_size
# self.slots.append(item)
# self.panel.add_element(item, (x, y + self.margin))
# # Add default events listener for this UI component.
# self.scroll_bar.on_left_mouse_button_pressed = self.scroll_click_callback
# self.scroll_bar.on_left_mouse_button_released = self.scroll_release_callback
# self.scroll_bar.on_left_mouse_button_dragged = self.scroll_drag_callback
# # Handle mouse wheel events on the panel.
# up_event = "MouseWheelForwardEvent"
# down_event = "MouseWheelBackwardEvent"
# if self.reverse_scrolling:
# up_event, down_event = down_event, up_event # Swap events
# self.add_callback(
# self.panel.background.actor, up_event, self.up_button_callback
# )
# self.add_callback(
# self.panel.background.actor, down_event, self.down_button_callback
# )
# # Handle mouse wheel events on the slots.
# for slot in self.slots:
# self.add_callback(
# slot.background.actor, up_event, self.up_button_callback
# )
# self.add_callback(
# slot.background.actor, down_event, self.down_button_callback
# )
# self.add_callback(slot.textblock.actor, up_event, self.up_button_callback)
# self.add_callback(
# slot.textblock.actor, down_event, self.down_button_callback
# )
# def resize(self, size):
# pass
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.panel.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.panel.add_to_scene(scene)
# for slot in self.slots:
# clip_overflow(slot.textblock, self.slot_width)
# def _get_size(self):
# return self.panel.size
# def _set_position(self, coords):
# """Position the lower-left corner of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.panel.position = coords
# def up_button_callback(self, i_ren, _obj, _list_box):
# """Pressing up button scrolls up in the combo box.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _list_box: :class:`ListBox2D`
# """
# if self.view_offset > 0:
# self.view_offset -= 1
# self.update()
# scroll_bar_idx = self.panel._elements.index(self.scroll_bar)
# self.scroll_bar.center = (
# self.scroll_bar.center[0],
# self.scroll_bar.center[1] + self.scroll_step_size,
# )
# self.panel.element_offsets[scroll_bar_idx] = (
# self.scroll_bar,
# (self.scroll_bar.position - self.panel.position),
# )
# i_ren.force_render()
# i_ren.event.abort() # Stop propagating the event.
# def down_button_callback(self, i_ren, _obj, _list_box):
# """Pressing down button scrolls down in the combo box.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _list_box: :class:`ListBox2D`
# """
# view_end = self.view_offset + self.nb_slots
# if view_end < len(self.values):
# self.view_offset += 1
# self.update()
# scroll_bar_idx = self.panel._elements.index(self.scroll_bar)
# self.scroll_bar.center = (
# self.scroll_bar.center[0],
# self.scroll_bar.center[1] - self.scroll_step_size,
# )
# self.panel.element_offsets[scroll_bar_idx] = (
# self.scroll_bar,
# (self.scroll_bar.position - self.panel.position),
# )
# i_ren.force_render()
# i_ren.event.abort() # Stop propagating the event.
# def scroll_click_callback(self, i_ren, _obj, _rect_obj):
# """Callback to change the color of the bar when it is clicked.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _rect_obj: :class:`Rectangle2D`
# """
# self.scroll_bar.color = self.scroll_bar_active_color
# self.scroll_init_position = i_ren.event.position[1]
# i_ren.force_render()
# i_ren.event.abort()
# def scroll_release_callback(self, i_ren, _obj, _rect_obj):
# """Callback to change the color of the bar when it is released.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# rect_obj: :class:`Rectangle2D`
# """
# self.scroll_bar.color = self.scroll_bar_inactive_color
# i_ren.force_render()
# def scroll_drag_callback(self, i_ren, _obj, _rect_obj):
# """Drag scroll bar in the combo box.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# rect_obj: :class:`Rectangle2D`
# """
# position = i_ren.event.position
# offset = int(
# (position[1] - self.scroll_init_position) / self.scroll_step_size)
# if offset > 0 and self.view_offset > 0:
# offset = min(offset, self.view_offset)
# elif offset < 0 and (self.view_offset + self.nb_slots < len(self.values)):
# offset = min(-offset, len(self.values) - self.nb_slots - self.view_offset)
# offset = -offset
# else:
# return
# self.view_offset -= offset
# self.update()
# scroll_bar_idx = self.panel._elements.index(self.scroll_bar)
# self.scroll_bar.center = (
# self.scroll_bar.center[0],
# self.scroll_bar.center[1] + offset * self.scroll_step_size,
# )
# self.scroll_init_position += offset * self.scroll_step_size
# self.panel.element_offsets[scroll_bar_idx] = (
# self.scroll_bar,
# (self.scroll_bar.position - self.panel.position),
# )
# i_ren.force_render()
# i_ren.event.abort()
# def update(self):
# """Refresh listbox's content."""
# view_start = self.view_offset
# view_end = view_start + self.nb_slots
# values_to_show = self.values[view_start:view_end]
# # Populate slots according to the view.
# for i, choice in enumerate(values_to_show):
# slot = self.slots[i]
# slot.element = choice
# if slot.textblock.scene is not None:
# clip_overflow(slot.textblock, self.slot_width)
# slot.set_visibility(True)
# if slot.size[1] != self.slot_height:
# slot.resize((self.slot_width, self.slot_height))
# if slot.element in self.selected:
# slot.select()
# else:
# slot.deselect()
# # Flush remaining slots.
# for slot in self.slots[len(values_to_show) :]:
# slot.element = None
# slot.set_visibility(False)
# slot.resize((self.slot_width, 0))
# slot.deselect()
# def update_scrollbar(self):
# """Change the scroll-bar height when the values
# in the listbox change
# """
# self.scroll_bar.set_visibility(True)
# self.scroll_bar.height = (
# self.nb_slots * (self.panel_size[1] - 2 * self.margin) / len(self.values)
# )
# self.scroll_step_size = (
# self.slot_height * self.nb_slots - self.scroll_bar.height
# ) / (len(self.values) - self.nb_slots)
# self.panel.update_element(
# self.scroll_bar, self.panel_size - self.scroll_bar.size - self.margin
# )
# if len(self.values) <= self.nb_slots:
# self.scroll_bar.set_visibility(False)
# self.scroll_bar.height = 0
# def clear_selection(self):
# del self.selected[:]
# @warn_on_args_to_kwargs()
# def select(self, item, *, multiselect=False, range_select=False):
# """Select the item.
# Parameters
# ----------
# item: ListBoxItem2D's object
# Item to select.
# multiselect: {True, False}
# If True and multiselection is allowed, the item is added to the
# selection.
# Otherwise, the selection will only contain the provided item unless
# range_select is True.
# range_select: {True, False}
# If True and multiselection is allowed, all items between the last
# selected item and the current one will be added to the selection.
# Otherwise, the selection will only contain the provided item unless
# multi_select is True.
# """
# selection_idx = self.values.index(item.element)
# if self.multiselection and range_select:
# self.clear_selection()
# step = 1 if selection_idx >= self.last_selection_idx else -1
# for i in range(self.last_selection_idx, selection_idx + step, step):
# self.selected.append(self.values[i])
# elif self.multiselection and multiselect:
# if item.element in self.selected:
# self.selected.remove(item.element)
# else:
# self.selected.append(item.element)
# self.last_selection_idx = selection_idx
# else:
# self.clear_selection()
# self.selected.append(item.element)
# self.last_selection_idx = selection_idx
# self.on_change() # Call hook.
# self.update()
# class ListBoxItem2D(UI):
# """The text displayed in a listbox."""
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# list_box,
# size,
# *,
# text_color=(1.0, 0.0, 0.0),
# selected_color=(0.4, 0.4, 0.4),
# unselected_color=(0.9, 0.9, 0.9),
# background_opacity=1.0,
# ):
# """Init ListBox Item instance.
# Parameters
# ----------
# list_box : :class:`ListBox`
# The ListBox reference this text belongs to.
# size : tuple of 2 ints
# The size of the listbox item.
# text_color : tuple of 3 floats
# unselected_color : tuple of 3 floats
# selected_color : tuple of 3 floats
# background_opacity : float
# """
# super(ListBoxItem2D, self).__init__()
# self._element = None
# self.list_box = list_box
# self.background.resize(size)
# self.background_opacity = background_opacity
# self.selected = False
# self.text_color = text_color
# self.textblock.color = self.text_color
# self.selected_color = selected_color
# self.unselected_color = unselected_color
# self.background.opacity = self.background_opacity
# self.deselect()
# def _setup(self):
# """Setup this UI component.
# Create the ListBoxItem2D with its background (Rectangle2D) and its
# label (TextBlock2D).
# """
# self.background = Rectangle2D()
# self.textblock = TextBlock2D(
# justification="left", vertical_justification="middle"
# )
# # Add default events listener for this UI component.
# self.add_callback(
# self.textblock.actor, "LeftButtonPressEvent", self.left_button_clicked
# )
# self.add_callback(
# self.background.actor, "LeftButtonPressEvent", self.left_button_clicked
# )
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.background.actors + self.textblock.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.background.add_to_scene(scene)
# self.textblock.add_to_scene(scene)
# def _get_size(self):
# return self.background.size
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.textblock.position = coords
# # Center background underneath the text.
# position = coords
# self.background.position = (
# position[0],
# position[1] - self.background.size[1] / 2.0,
# )
# def deselect(self):
# self.background.color = self.unselected_color
# self.textblock.bold = False
# self.selected = False
# def select(self):
# self.textblock.bold = True
# self.background.color = self.selected_color
# self.selected = True
# @property
# def element(self):
# return self._element
# @element.setter
# def element(self, element):
# self._element = element
# self.textblock.message = "" if self._element is None else str(element)
# def left_button_clicked(self, i_ren, _obj, _list_box_item):
# """Handle left click for this UI element.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _list_box_item: :class:`ListBoxItem2D`
# """
# multiselect = i_ren.event.ctrl_key
# range_select = i_ren.event.shift_key
# self.list_box.select(
# item=self, multiselect=multiselect, range_select=range_select
# )
# i_ren.force_render()
# def resize(self, size):
# self.background.resize(size)
# class FileMenu2D(UI):
# """A menu to select files in the current folder.
# Can go to new folder, previous folder and select multiple files.
# Attributes
# ----------
# extensions: ['extension1', 'extension2', ....]
# To show all files, extensions=["*"] or [""]
# List of extensions to be shown as files.
# listbox : :class: 'ListBox2D'
# Container for the menu.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# directory_path,
# *,
# extensions=None,
# position=(0, 0),
# size=(100, 300),
# multiselection=True,
# reverse_scrolling=False,
# font_size=20,
# line_spacing=1.4,
# ):
# """Init class instance.
# Parameters
# ----------
# extensions: list(string)
# List of extensions to be shown as files.
# directory_path: string
# Path of the directory where this dialog should open.
# position : (float, float)
# Absolute coordinates (x, y) of the lower-left corner of this
# UI component.
# size : (int, int)
# Width and height in pixels of this UI component.
# multiselection: {True, False}
# Whether multiple values can be selected at once.
# reverse_scrolling: {True, False}
# If True, scrolling up will move the list of files down.
# font_size: int
# The font size in pixels.
# line_spacing: float
# Distance between listbox's items in pixels.
# """
# self.font_size = font_size
# self.multiselection = multiselection
# self.reverse_scrolling = reverse_scrolling
# self.line_spacing = line_spacing
# self.extensions = extensions or ["*"]
# self.current_directory = directory_path
# self.menu_size = size
# self.directory_contents = []
# super(FileMenu2D, self).__init__()
# self.position = position
# self.set_slot_colors()
# def _setup(self):
# """Setup this UI component.
# Create the ListBox (Panel2D) filled with empty slots (ListBoxItem2D).
# """
# self.directory_contents = self.get_all_file_names()
# content_names = [x[0] for x in self.directory_contents]
# self.listbox = ListBox2D(
# values=content_names,
# multiselection=self.multiselection,
# font_size=self.font_size,
# line_spacing=self.line_spacing,
# reverse_scrolling=self.reverse_scrolling,
# size=self.menu_size,
# )
# self.add_callback(
# self.listbox.scroll_bar.actor, "MouseMoveEvent", self.scroll_callback
# )
# # Handle mouse wheel events on the panel.
# up_event = "MouseWheelForwardEvent"
# down_event = "MouseWheelBackwardEvent"
# if self.reverse_scrolling:
# up_event, down_event = down_event, up_event # Swap events
# self.add_callback(
# self.listbox.panel.background.actor, up_event, self.scroll_callback
# )
# self.add_callback(
# self.listbox.panel.background.actor, down_event, self.scroll_callback
# )
# # Handle mouse wheel events on the slots.
# for slot in self.listbox.slots:
# self.add_callback(slot.background.actor, up_event, self.scroll_callback)
# self.add_callback(slot.background.actor, down_event, self.scroll_callback)
# self.add_callback(slot.textblock.actor, up_event, self.scroll_callback)
# self.add_callback(slot.textblock.actor, down_event, self.scroll_callback)
# slot.add_callback(
# slot.textblock.actor,
# "LeftButtonPressEvent",
# self.directory_click_callback,
# )
# slot.add_callback(
# slot.background.actor,
# "LeftButtonPressEvent",
# self.directory_click_callback,
# )
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.listbox.actors
# def resize(self, size):
# pass
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.listbox.position = coords
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.listbox.add_to_scene(scene)
# def _get_size(self):
# return self.listbox.size
# def get_all_file_names(self):
# """Get file and directory names.
# Returns
# -------
# all_file_names: list((string, {"directory", "file"}))
# List of all file and directory names as string.
# """
# all_file_names = []
# directory_names = self.get_directory_names()
# for directory_name in directory_names:
# all_file_names.append((directory_name, "directory"))
# file_names = self.get_file_names()
# for file_name in file_names:
# all_file_names.append((file_name, "file"))
# return all_file_names
# def get_directory_names(self):
# """Find names of all directories in the current_directory
# Returns
# -------
# directory_names: list(string)
# List of all directory names as string.
# """
# # A list of directory names in the current directory
# directory_names = []
# for _, dirnames, _ in os.walk(self.current_directory):
# directory_names += dirnames
# break
# directory_names.sort(key=lambda s: s.lower())
# directory_names.insert(0, "../")
# return directory_names
# def get_file_names(self):
# """Find names of all files in the current_directory
# Returns
# -------
# file_names: list(string)
# List of all file names as string.
# """
# # A list of file names with extension in the current directory
# files = []
# for _, _, f in os.walk(self.current_directory):
# files += f
# break
# file_names = []
# if "*" in self.extensions or "" in self.extensions:
# file_names = files
# else:
# for ext in self.extensions:
# for file in files:
# if file.endswith("." + ext):
# file_names.append(file)
# file_names.sort(key=lambda s: s.lower())
# return file_names
# def set_slot_colors(self):
# """Set the text color of the slots based on the type of element
# they show. Blue for directories and green for files.
# """
# for idx, slot in enumerate(self.listbox.slots):
# list_idx = min(
# self.listbox.view_offset + idx, len(self.directory_contents) - 1
# )
# if self.directory_contents[list_idx][1] == "directory":
# slot.textblock.color = (0, 0.6, 0)
# elif self.directory_contents[list_idx][1] == "file":
# slot.textblock.color = (0, 0, 0.7)
# def scroll_callback(self, i_ren, _obj, _filemenu_item):
# """Handle scroll and change the slot text colors.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# _filemenu_item: :class:`FileMenu2D`
# """
# self.set_slot_colors()
# i_ren.force_render()
# i_ren.event.abort()
# def directory_click_callback(self, i_ren, _obj, listboxitem):
# """Handle the move into a directory if it has been clicked.
# Parameters
# ----------
# i_ren: :class:`CustomInteractorStyle`
# obj: :class:`vtkActor`
# The picked actor
# listboxitem: :class:`ListBoxItem2D`
# """
# if (listboxitem.element, "directory") in self.directory_contents:
# new_directory_path = os.path.join(
# self.current_directory, listboxitem.element
# )
# if os.access(new_directory_path, os.R_OK):
# self.current_directory = new_directory_path
# self.directory_contents = self.get_all_file_names()
# content_names = [x[0] for x in self.directory_contents]
# self.listbox.clear_selection()
# self.listbox.values = content_names
# self.listbox.view_offset = 0
# self.listbox.update()
# self.listbox.update_scrollbar()
# self.set_slot_colors()
# i_ren.force_render()
# i_ren.event.abort()
# class DrawShape(UI):
# """Create and Manage 2D Shapes."""
# @warn_on_args_to_kwargs()
# def __init__(self, shape_type, *, drawpanel=None, position=(0, 0)):
# """Init this UI element.
# Parameters
# ----------
# shape_type : string
# Type of shape to be created.
# drawpanel : DrawPanel, optional
# Reference to the main canvas on which it is drawn.
# position : (float, float), optional
# (x, y) in pixels.
# """
# self.shape = None
# self.shape_type = shape_type.lower()
# self.drawpanel = drawpanel
# self.max_size = None
# self.rotation = 0
# super(DrawShape, self).__init__(position=position)
# self.shape.color = np.random.random(3)
# def _setup(self):
# """Setup this UI component.
# Create a Shape.
# """
# if self.shape_type == "line":
# self.shape = Rectangle2D(size=(3, 3))
# elif self.shape_type == "quad":
# self.shape = Rectangle2D(size=(3, 3))
# elif self.shape_type == "circle":
# self.shape = Disk2D(outer_radius=2)
# else:
# raise IOError("Unknown shape type: {}.".format(self.shape_type))
# self.shape.on_left_mouse_button_pressed = self.left_button_pressed
# self.shape.on_left_mouse_button_dragged = self.left_button_dragged
# self.shape.on_left_mouse_button_released = self.left_button_released
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.shape
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self._scene = scene
# self.shape.add_to_scene(scene)
# def _get_size(self):
# return self.shape.size
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# if self.shape_type == "circle":
# self.shape.center = coords
# else:
# self.shape.position = coords
# def update_shape_position(self, center_position):
# """Update the center position on the canvas.
# Parameters
# ----------
# center_position: (float, float)
# Absolute pixel coordinates (x, y).
# """
# new_center = self.clamp_position(center=center_position)
# self.drawpanel.canvas.update_element(self, new_center, anchor="center")
# self.cal_bounding_box()
# @property
# def center(self):
# return self._bounding_box_min + self._bounding_box_size // 2
# @center.setter
# def center(self, coords):
# """Position the center of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# new_center = np.array(coords)
# new_lower_left_corner = new_center - self._bounding_box_size // 2
# self.position = new_lower_left_corner + self._bounding_box_offset
# self.cal_bounding_box()
# @property
# def is_selected(self):
# return self._is_selected
# @is_selected.setter
# def is_selected(self, value):
# if self.drawpanel and value:
# self.drawpanel.current_shape = self
# self._is_selected = value
# self.selection_change()
# def selection_change(self):
# if self.is_selected:
# self.drawpanel.rotation_slider.value = self.rotation
# else:
# self.drawpanel.rotation_slider.set_visibility(False)
# def rotate(self, angle):
# """Rotate the vertices of the UI component using specific angle.
# Parameters
# ----------
# angle: float
# Value by which the vertices are rotated in radian.
# """
# if self.shape_type == "circle":
# return
# points_arr = vertices_from_actor(self.shape.actor)
# new_points_arr = rotate_2d(points_arr, angle)
# set_polydata_vertices(self.shape._polygonPolyData, new_points_arr)
# update_actor(self.shape.actor)
# self.cal_bounding_box()
# def cal_bounding_box(self):
# """Calculate the min, max position and the size of the bounding box."""
# vertices = self.position + vertices_from_actor(self.shape.actor)[:, :-1]
# (
# self._bounding_box_min,
# self._bounding_box_max,
# self._bounding_box_size,
# ) = cal_bounding_box_2d(vertices)
# self._bounding_box_offset = self.position - self._bounding_box_min
# @warn_on_args_to_kwargs()
# def clamp_position(self, *, center=None):
# """Clamp the given center according to the DrawPanel canvas.
# Parameters
# ----------
# center : (float, float)
# (x, y) in pixels.
# Returns
# -------
# new_center: ndarray(int)
# New center for the shape.
# """
# center = self.center if center is None else center
# new_center = np.clip(
# center,
# self._bounding_box_size // 2,
# self.drawpanel.canvas.size - self._bounding_box_size // 2,
# )
# return new_center.astype(int)
# def resize(self, size):
# """Resize the UI."""
# if self.shape_type == "line":
# hyp = np.hypot(size[0], size[1])
# self.shape.resize((hyp, 3))
# self.rotate(angle=np.arctan2(size[1], size[0]))
# elif self.shape_type == "quad":
# self.shape.resize(size)
# elif self.shape_type == "circle":
# hyp = np.hypot(size[0], size[1])
# if self.max_size and hyp > self.max_size:
# hyp = self.max_size
# self.shape.outer_radius = hyp
# self.cal_bounding_box()
# def remove(self):
# """Remove the Shape and all related actors."""
# self._scene.rm(self.shape.actor)
# self.drawpanel.rotation_slider.set_visibility(False)
# def left_button_pressed(self, i_ren, _obj, shape):
# mode = self.drawpanel.current_mode
# if mode == "selection":
# self.drawpanel.update_shape_selection(self)
# click_pos = np.array(i_ren.event.position)
# self._drag_offset = click_pos - self.center
# self.drawpanel.show_rotation_slider()
# i_ren.event.abort()
# elif mode == "delete":
# self.remove()
# else:
# self.drawpanel.left_button_pressed(i_ren, _obj, self.drawpanel)
# i_ren.force_render()
# def left_button_dragged(self, i_ren, _obj, shape):
# if self.drawpanel.current_mode == "selection":
# self.drawpanel.rotation_slider.set_visibility(False)
# if self._drag_offset is not None:
# click_position = i_ren.event.position
# relative_center_position = (
#
# click_position - self._drag_offset - self.drawpanel.canvas.position
# )
# self.update_shape_position(relative_center_position)
# i_ren.force_render()
# else:
# self.drawpanel.left_button_dragged(i_ren, _obj, self.drawpanel)
# def left_button_released(self, i_ren, _obj, shape):
# if self.drawpanel.current_mode == "selection":
# self.drawpanel.show_rotation_slider()
# i_ren.force_render()
# class DrawPanel(UI):
# """The main Canvas(Panel2D) on which everything would be drawn."""
# @warn_on_args_to_kwargs()
# def __init__(self, *, size=(400, 400), position=(0, 0), is_draggable=False):
# """Init this UI element.
# Parameters
# ----------
# size : (int, int), optional
# Width and height in pixels of this UI component.
# position : (float, float), optional
# (x, y) in pixels.
# is_draggable : bool, optional
# Whether the background canvas will be draggble or not.
# """
# self.panel_size = size
# super(DrawPanel, self).__init__(position=position)
# self.is_draggable = is_draggable
# self.current_mode = None
# if is_draggable:
# self.current_mode = "selection"
# self.shape_list = []
# self.current_shape = None
# def _setup(self):
# """Setup this UI component.
# Create a Canvas(Panel2D).
# """
# self.canvas = Panel2D(size=self.panel_size)
# self.canvas.background.on_left_mouse_button_pressed = self.left_button_pressed
# self.canvas.background.on_left_mouse_button_dragged = self.left_button_dragged
# # Todo
# # Convert mode_data into a private variable and make it read-only
# # Then add the ability to insert user-defined mode
# mode_data = {
# "selection": ["selection.png", "selection-pressed.png"],
# "line": ["line.png", "line-pressed.png"],
# "quad": ["quad.png", "quad-pressed.png"],
# "circle": ["circle.png", "circle-pressed.png"],
# "delete": ["delete.png", "delete-pressed.png"],
# }
# padding = 5
# # Todo
# # Add this size to __init__
# mode_panel_size = (len(mode_data) * 35 + 2 * padding, 40)
# self.mode_panel = Panel2D(size=mode_panel_size, color=(0.5, 0.5, 0.5))
# btn_pos = np.array([0, 0])
# for mode, fname in mode_data.items():
# icon_files = []
# icon_files.append(
# (mode, read_viz_icons(style="new_icons", fname=fname[0])))
# icon_files.append(
# (mode + "-pressed", read_viz_icons(style="new_icons", fname=fname[1]))
# )
# btn = Button2D(icon_fnames=icon_files)
# def mode_selector(i_ren, _obj, btn):
# self.current_mode = btn.icon_names[0]
# i_ren.force_render()
# btn.on_left_mouse_button_pressed = mode_selector
# self.mode_panel.add_element(btn, btn_pos + padding)
# btn_pos[0] += btn.size[0] + padding
# self.canvas.add_element(self.mode_panel, (0, -mode_panel_size[1]))
# self.mode_text = TextBlock2D(
# text="Select appropriate drawing mode using below icon"
# )
# self.canvas.add_element(self.mode_text, (0.0, 1.0))
# self.rotation_slider = RingSlider2D(
# initial_value=0, text_template="{angle:5.1f}°"
# )
# self.rotation_slider.set_visibility(False)
# def rotate_shape(slider):
# angle = slider.value
# previous_angle = slider.previous_value
# rotation_angle = angle - previous_angle
# current_center = self.current_shape.center
# self.current_shape.rotate(np.deg2rad(rotation_angle))
# self.current_shape.rotation = slider.value
# self.current_shape.update_shape_position(
# current_center - self.canvas.position
# )
# self.rotation_slider.on_moving_slider = rotate_shape
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.canvas.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self._scene = scene
# self.canvas.add_to_scene(scene)
# def _get_size(self):
# return self.canvas.size
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.canvas.position = coords + [0, self.mode_panel.size[1]]
# slider_position = self.canvas.position + [
# self.canvas.size[0] - self.rotation_slider.size[0] / 2,
# self.rotation_slider.size[1] / 2,
# ]
# self.rotation_slider.center = slider_position
# def resize(self, size):
# """Resize the UI."""
# pass
# @property
# def current_mode(self):
# return self._current_mode
# @current_mode.setter
# def current_mode(self, mode):
# self.update_button_icons(mode)
# self._current_mode = mode
# if mode is not None:
# self.mode_text.message = f"Mode: {mode}"
# def cal_min_boundary_distance(self, position):
# """Calculate minimum distance between the current
# position and canvas boundary.
# Parameters
# ----------
# position: (float,float)
# current position of the shape.
# Returns
# -------
# float
# Minimum distance from the boundary.
# """
# distance_list = []
# # calculate distance from element to left and lower boundary
# distance_list.extend(position - self.canvas.position)
# # calculate distance from element to upper and right boundary
# distance_list.extend(self.canvas.position + self.canvas.size - position)
# return min(distance_list)
# def draw_shape(self, shape_type, current_position):
# """Draw the required shape at the given position.
# Parameters
# ----------
# shape_type: string
# Type of shape - line, quad, circle.
# current_position: (float,float)
# Lower left corner position for the shape.
# """
# shape = DrawShape(
# shape_type=shape_type, drawpanel=self, position=current_position
# )
# if shape_type == "circle":
# shape.max_size = self.cal_min_boundary_distance(current_position)
# self.shape_list.append(shape)
# self._scene.add(shape)
# self.canvas.add_element(shape, current_position - self.canvas.position)
# self.update_shape_selection(shape)
# def resize_shape(self, current_position):
# """Resize the shape.
# Parameters
# ----------
# current_position: (float,float)
# Lower left corner position for the shape.
# """
# self.current_shape = self.shape_list[-1]
# size = current_position - self.current_shape.position
# self.current_shape.resize(size)
# def update_shape_selection(self, selected_shape):
# for shape in self.shape_list:
# if selected_shape == shape:
# shape.is_selected = True
# else:
# shape.is_selected = False
# def show_rotation_slider(self):
# """Display the RingSlider2D to allow rotation of shape from the center."""
# self._scene.rm(*self.rotation_slider.actors)
# self.rotation_slider.add_to_scene(self._scene)
# self.rotation_slider.set_visibility(True)
# def update_button_icons(self, current_mode):
# """Update the button icon.
# Parameters
# ----------
# current_mode: string
# Current mode of the UI.
# """
# for btn in self.mode_panel._elements[1:]:
# if btn.icon_names[0] == current_mode:
# btn.next_icon()
# elif btn.current_icon_id == 1:
# btn.next_icon()
# def clamp_mouse_position(self, mouse_position):
# """Restrict the mouse position to the canvas boundary.
# Parameters
# ----------
# mouse_position: (float,float)
# Current mouse position.
# Returns
# -------
# list(float)
# New clipped position.
# """
# return np.clip(
# mouse_position,
# self.canvas.position,
# self.canvas.position + self.canvas.size,
# )
# def handle_mouse_click(self, position):
# if self.current_mode == "selection":
# if self.is_draggable:
# self._drag_offset = position - self.position
# self.current_shape.is_selected = False
# if self.current_mode in ["line", "quad", "circle"]:
# self.draw_shape(self.current_mode, position)
# def left_button_pressed(self, i_ren, _obj, element):
# self.handle_mouse_click(i_ren.event.position)
# i_ren.force_render()
# def handle_mouse_drag(self, position):
# if self.is_draggable and self.current_mode == "selection":
# if self._drag_offset is not None:
# new_position = position - self._drag_offset
# self.position = new_position
# if self.current_mode in ["line", "quad", "circle"]:
# self.resize_shape(position)
# def left_button_dragged(self, i_ren, _obj, element):
# mouse_position = self.clamp_mouse_position(i_ren.event.position)
# self.handle_mouse_drag(mouse_position)
# i_ren.force_render()
# class Card2D(UI):
# """Card element to show image and related text
# Attributes
# ----------
# image: :class: 'ImageContainer2D'
# Renders the image on the card.
# title_box: :class: 'TextBlock2D'
# Displays the title on card.
# body_box: :class: 'TextBLock2D'
# Displays the body text.
# """
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# image_path,
# *,
# body_text="",
# draggable=True,
# title_text="",
# padding=10,
# position=(0, 0),
# size=(400, 400),
# image_scale=0.5,
# bg_color=(0.5, 0.5, 0.5),
# bg_opacity=1,
# title_color=(0.0, 0.0, 0.0),
# body_color=(0.0, 0.0, 0.0),
# border_color=(1.0, 1.0, 1.0),
# border_width=0,
# maintain_aspect=False,
# ):
# """Parameters
# ----------
# image_path: str
# Path of the image, supports png and jpg/jpeg images
# body_text: str, optional
# Card body text
# draggable: Bool, optional
# If the card should be draggable
# title_text: str, optional
# Card title text
# padding: int, optional
# Padding between image, title, body
# position : (float, float), optional
# Absolute coordinates (x, y) of the lower-left corner of the
# UI component
# size : (int, int), optional
# Width and height of the pixels of this UI component.
# image_scale: float, optional
# fraction of size taken by the image (between 0 , 1)
# bg_color: (float, float, float), optional
# Background color of card
# bg_opacity: float, optional
# Background opacity
# title_color: (float, float, float), optional
# Title text color
# body_color: (float, float, float), optional
# Body text color
# border_color: (float, float, float), optional
# Border color
# border_width: int, optional
# Width of the border
# maintain_aspect: bool, optional
# If the image should be scaled to maintain aspect ratio
# """
# self.image_path = image_path
# self._basename = os.path.basename(self.image_path)
# self._extension = self._basename.split(".")[-1]
# if self._extension not in ["jpg", "jpeg", "png"]:
# raise UnidentifiedImageError(
# f"Image extension {self._extension} not supported"
# )
# self.body_text = body_text
# self.title_text = title_text
# self.draggable = draggable
# self.card_size = size
# self.padding = padding
# self.title_color = [np.clip(value, 0, 1) for value in title_color]
# self.body_color = [np.clip(value, 0, 1) for value in body_color]
# self.bg_color = [np.clip(value, 0, 1) for value in bg_color]
# self.border_color = [np.clip(value, 0, 1) for value in border_color]
# self.bg_opacity = bg_opacity
# self.text_scale = np.clip(1 - image_scale, 0, 1)
# self.image_scale = np.clip(image_scale, 0, 1)
# self.maintain_aspect = maintain_aspect
# if self.maintain_aspect:
# self._true_image_size = Image.open(urlopen(self.image_path)).size
# self._image_size = (self.card_size[0], self.card_size[1] * self.image_scale)
# self.border_width = border_width
# self.has_border = bool(border_width)
# super(Card2D, self).__init__()
# self.position = position
# if self.maintain_aspect:
# self._new_size = (
# self._true_image_size[0],
# self._true_image_size[1] // self.image_scale,
# )
# self.resize(self._new_size)
# else:
# self.resize(size)
# def _setup(self):
# """Setup this UI component
# Create the image.
# Create the title and body.
# Create a Panel2D widget to hold image, title, body.
# """
# self.image = ImageContainer2D(img_path=self.image_path, size=self._image_size)
# self.body_box = TextBlock2D(text=self.body_text, color=self.body_color)
# self.title_box = TextBlock2D(
# text=self.title_text, bold=True, color=self.title_color
# )
# self.panel = Panel2D(
# self.card_size,
# color=self.bg_color,
# opacity=self.bg_opacity,
# border_color=self.border_color,
# border_width=self.border_width,
# has_border=self.has_border,
# )
# self.panel.add_element(self.image, (0.0, 0.0))
# self.panel.add_element(self.title_box, (0.0, 0.0))
# self.panel.add_element(self.body_box, (0.0, 0.0))
# if self.draggable:
# self.panel.background.on_left_mouse_button_dragged = (
# self.left_button_dragged
# )
# self.panel.background.on_left_mouse_button_pressed = (
# self.left_button_pressed
# )
# self.image.on_left_mouse_button_dragged = self.left_button_dragged
# self.image.on_left_mouse_button_pressed = self.left_button_pressed
# else:
# self.panel.background.on_left_mouse_button_dragged = (
# lambda i_ren, _obj, _comp: i_ren.force_render
# )
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.panel.actors
# def _add_to_scene(self, _scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : scene
# """
# self.panel.add_to_scene(_scene)
# if self.size[0] <= 200:
# clip_overflow(self.body_box, self.size[0] - 2 * self.padding)
# else:
# wrap_overflow(self.body_box, self.size[0] - 2 * self.padding)
# wrap_overflow(self.title_box, self.size[0] - 2 * self.padding)
# def _get_size(self):
# return self.panel.size
# def resize(self, size):
# """Resize Card2D.
# Parameters
# ----------
# size : (int, int)
# Card2D size(width, height) in pixels.
# """
# _width, _height = size
# self.panel.resize(size)
# self._image_size = (
# size[0] - int(self.border_width),
# int(self.image_scale * size[1]),
# )
# _title_box_size = (
# _width - 2 * self.padding,
# _height * 0.34 * self.text_scale / 2,
# )
# _body_box_size = (_width - 2 * self.padding, _height * self.text_scale / 2)
# _img_coords = (int(self.border_width), int(size[1] - self._image_size[1]))
# _title_coords = (
# self.padding,
# int(_img_coords[1] - _title_box_size[1] - \
# self.padding + self.border_width),
# )
# _text_coords = (
# self.padding,
# int(
# _title_coords[1] - _body_box_size[1] - \
# self.padding + self.border_width
# ),
# )
# self.panel.update_element(self.image, _img_coords)
# self.panel.update_element(self.body_box, _text_coords)
# self.panel.update_element(self.title_box, _title_coords)
# self.image.resize(self._image_size)
# self.title_box.resize(_title_box_size)
# def _set_position(self, _coords):
# """Position the lower-left corner of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.panel.position = _coords
# @property
# def color(self):
# """Returns the background color of card."""
# return self.panel.color
# @color.setter
# def color(self, color):
# """Sets background color of card.
# Parameters
# ----------
# color : list of 3 floats.
# """
# self.panel.color = color
# @property
# def body(self):
# """Returns the body text of the card."""
# return self.body_box.message
# @body.setter
# def body(self, text):
# self.body_box.message = text
# @property
# def title(self):
# """Returns the title text of the card"""
# return self.title_box.message
# @title.setter
# def title(self, text):
# self.title_box.message = text
# def left_button_pressed(self, i_ren, _obj, _sub_component):
# click_pos = np.array(i_ren.event.position)
# self._click_position = click_pos
# i_ren.event.abort()
# def left_button_dragged(self, i_ren, _obj, _sub_component):
# click_position = np.array(i_ren.event.position)
# change = click_position - self._click_position
# self.panel.position += change
# self._click_position = click_position
# i_ren.force_render()
# class SpinBox(UI):
# """SpinBox UI."""
# @warn_on_args_to_kwargs()
# def __init__(
# self,
# *,
# position=(350, 400),
# size=(300, 100),
# padding=10,
# panel_color=(1, 1, 1),
# min_val=0,
# max_val=100,
# initial_val=50,
# step=1,
# max_column=10,
# max_line=2,
# ):
# """Init this UI element.
# Parameters
# ----------
# position : (int, int), optional
# Absolute coordinates (x, y) of the lower-left corner of this
# UI component.
# size : (int, int), optional
# Width and height in pixels of this UI component.
# padding : int, optional
# Distance between TextBox and Buttons.
# panel_color : (float, float, float), optional
# Panel color of SpinBoxUI.
# min_val: int, optional
# Minimum value of SpinBoxUI.
# max_val: int, optional
# Maximum value of SpinBoxUI.
# initial_val: int, optional
# Initial value of SpinBoxUI.
# step: int, optional
# Step value of SpinBoxUI.
# max_column: int, optional
# Max number of characters in a line.
# max_line: int, optional
# Max number of lines in the textbox.
# """
# self.panel_size = size
# self.padding = padding
# self.panel_color = panel_color
# self.min_val = min_val
# self.max_val = max_val
# self.step = step
# self.max_column = max_column
# self.max_line = max_line
# super(SpinBox, self).__init__(position=position)
# self.value = initial_val
# self.resize(size)
# self.on_change = lambda ui: None
# def _setup(self):
# """Setup this UI component.
# Create the SpinBoxUI with Background (Panel2D) and InputBox (TextBox2D)
# and Increment,Decrement Button (Button2D).
# """
# self.panel = Panel2D(size=self.panel_size, color=self.panel_color)
# self.textbox = TextBox2D(width=self.max_column, height=self.max_line)
# self.textbox.text.dynamic_bbox = False
# self.textbox.text.auto_font_scale = True
# self.increment_button = Button2D(
# icon_fnames=[("up", read_viz_icons(fname="circle-up.png"))]
# )
# self.decrement_button = Button2D(
# icon_fnames=[("down", read_viz_icons(fname="circle-down.png"))]
# )
# self.panel.add_element(self.textbox, (0, 0))
# self.panel.add_element(self.increment_button, (0, 0))
# self.panel.add_element(self.decrement_button, (0, 0))
# # Adding button click callbacks
# self.increment_button.on_left_mouse_button_pressed = self.increment_callback
# self.decrement_button.on_left_mouse_button_pressed = self.decrement_callback
# self.textbox.off_focus = self.textbox_update_value
# def resize(self, size):
# """Resize SpinBox.
# Parameters
# ----------
# size : (float, float)
# SpinBox size(width, height) in pixels.
# """
# self.panel_size = size
# self.textbox_size = (int(0.7 * size[0]), int(0.8 * size[1]))
# self.button_size = (int(0.2 * size[0]), int(0.3 * size[1]))
# self.padding = int(0.03 * self.panel_size[0])
# self.panel.resize(size)
# self.textbox.text.resize(self.textbox_size)
# self.increment_button.resize(self.button_size)
# self.decrement_button.resize(self.button_size)
# textbox_pos = (self.padding, int((size[1] - self.textbox_size[1]) / 2))
# inc_btn_pos = (
# size[0] - self.padding - self.button_size[0],
# int((1.5 * size[1] - self.button_size[1]) / 2),
# )
# dec_btn_pos = (
# size[0] - self.padding - self.button_size[0],
# int((0.5 * size[1] - self.button_size[1]) / 2),
# )
# self.panel.update_element(self.textbox, textbox_pos)
# self.panel.update_element(self.increment_button, inc_btn_pos)
# self.panel.update_element(self.decrement_button, dec_btn_pos)
# def _get_actors(self):
# """Get the actors composing this UI component."""
# return self.panel.actors
# def _add_to_scene(self, scene):
# """Add all subcomponents or VTK props that compose this UI component.
# Parameters
# ----------
# scene : Scene
# """
# self.panel.add_to_scene(scene)
# def _get_size(self):
# return self.panel.size
# def _set_position(self, coords):
# """Set the lower-left corner position of this UI component.
# Parameters
# ----------
# coords: (float, float)
# Absolute pixel coordinates (x, y).
# """
# self.panel.center = coords
# def increment_callback(self, i_ren, _obj, _button):
# self.increment()
# i_ren.force_render()
# i_ren.event.abort()
# def decrement_callback(self, i_ren, _obj, _button):
# self.decrement()
# i_ren.force_render()
# i_ren.event.abort()
# @property
# def value(self):
# return self._value
# @value.setter
# def value(self, value):
# if value >= self.max_val:
# self._value = self.max_val
# elif value <= self.min_val:
# self._value = self.min_val
# else:
# self._value = value
# self.textbox.set_message(str(self._value))
# def validate_value(self, value):
# """Validate and convert the given value into integer.
# Parameters
# ----------
# value : str
# Input value received from the textbox.
# Returns
# -------
# int
# If valid return converted integer else the previous value.
# """
# if value.isnumeric():
# return int(value)
# return self.value
# def increment(self):
# """Increment the current value by the step."""
# current_val = self.validate_value(self.textbox.message)
# self.value = current_val + self.step
# self.on_change(self)
# def decrement(self):
# """Decrement the current value by the step."""
# current_val = self.validate_value(self.textbox.message)
# self.value = current_val - self.step
# self.on_change(self)
# def textbox_update_value(self, textbox):
# self.value = self.validate_value(textbox.message)
# self.on_change(self)