Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

display panel slider using reactor #993

Merged
merged 11 commits into from
Nov 15, 2023
28 changes: 28 additions & 0 deletions nion/swift/DisplayCanvasItem.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from __future__ import annotations

import abc
import types
import typing

from nion.swift import Undo
Expand All @@ -8,9 +11,31 @@
from nion.swift.model import Persistence
from nion.ui import CanvasItem
from nion.ui import UserInterface
from nion.ui import Window
from nion.utils import Geometry


class InteractiveTask:
def __init__(self) -> None:
pass

def __enter__(self) -> InteractiveTask:
return self

def __exit__(self, exception_type: typing.Optional[typing.Type[BaseException]], value: typing.Optional[BaseException], traceback: typing.Optional[types.TracebackType]) -> typing.Optional[bool]:
self.close()
return None

def close(self) -> None:
self._close()

def commit(self) -> None:
self._commit()

def _close(self) -> None: ...
def _commit(self) -> None: ...


class DisplayCanvasItemDelegate(typing.Protocol):

@property
Expand All @@ -31,6 +56,9 @@ def create_change_graphics_command(self) -> Undo.UndoableCommand: ...
def create_insert_graphics_command(self, graphics: typing.Sequence[Graphics.Graphic]) -> Undo.UndoableCommand: ...
def create_move_display_layer_command(self, display_item: DisplayItem.DisplayItem, src_index: int, target_index: int) -> Undo.UndoableCommand: ...
def push_undo_command(self, command: Undo.UndoableCommand) -> None: ...
def create_change_display_properties_task(self) -> InteractiveTask: ...
def create_create_graphic_task(self, graphic_type: str, start_position: Geometry.FloatPoint) -> InteractiveTask: ...
def create_change_graphics_task(self) -> InteractiveTask: ...
def add_index_to_selection(self, index: int) -> None: ...
def remove_index_from_selection(self, index: int) -> None: ...
def set_selection(self, index: int) -> None: ...
Expand Down
144 changes: 130 additions & 14 deletions nion/swift/DisplayPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,11 @@ class IndexValueSliderCanvasItem(CanvasItem.CanvasItemComposition):
def __init__(self, title: str, display_item_value_stream: Stream.ValueStream[DisplayItem.DisplayItem],
index_value_adapter: IndexValueAdapter,
get_font_metrics_fn: typing.Callable[[str, str], UserInterface.FontMetrics],
event_loop: typing.Optional[asyncio.AbstractEventLoop] = None,
play_button_handler: typing.Optional[typing.Callable[[], None]] = None,
play_button_model: typing.Optional[Model.PropertyModel[bool]] = None) -> None:
super().__init__()
self.__event_loop = event_loop
self.layout = CanvasItem.CanvasItemRowLayout()
self.update_sizing(self.sizing.with_preferred_height(0))
self.__slider_row = CanvasItem.CanvasItemComposition()
Expand All @@ -695,12 +697,12 @@ def __init__(self, title: str, display_item_value_stream: Stream.ValueStream[Dis
self.add_spacing(12)
self.__display_item_value_stream = display_item_value_stream.add_ref()
self.__get_font_metrics_fn = get_font_metrics_fn
self.__slider_value_action: typing.Optional[Stream.ValueStreamAction[Stream.ValueChange[float]]] = None
self.__value_change_stream_reactor: typing.Optional[Stream.ValueChangeStreamReactor[float]] = None
self.__title = title
self.__index_value_adapter = index_value_adapter
display_data_channel_value_stream = DisplayDataChannelValueStream(self.__display_item_value_stream)
index_value_stream = self.__index_value_adapter.get_index_value_stream(display_data_channel_value_stream)
combined_stream = Stream.CombineLatestStream[typing.Any, typing.Any]([self.__display_item_value_stream, display_data_channel_value_stream, index_value_stream])
self.__display_data_channel_value_stream = DisplayDataChannelValueStream(self.__display_item_value_stream)
index_value_stream = self.__index_value_adapter.get_index_value_stream(self.__display_data_channel_value_stream)
combined_stream = Stream.CombineLatestStream[typing.Any, typing.Any]([self.__display_item_value_stream, self.__display_data_channel_value_stream, index_value_stream])
self.__stream_action = Stream.ValueStreamAction[typing.Tuple[DisplayItem.DisplayItem, DisplayItem.DisplayDataChannel, int]](combined_stream, self.__index_changed)
self.__play_button_handler = play_button_handler
self.__play_button_model = play_button_model
Expand All @@ -709,17 +711,37 @@ def __init__(self, title: str, display_item_value_stream: Stream.ValueStream[Dis
def close(self) -> None:
self.__stream_action.close()
self.__stream_action = typing.cast(typing.Any, None)
if self.__slider_value_action:
self.__slider_value_action.close()
self.__slider_value_action = None
self.__display_item_value_stream.remove_ref()
self.__display_item_value_stream = typing.cast(typing.Any, None)
self.__value_change_stream_reactor = None
super().close()

def __index_changed(self, args: typing.Optional[typing.Tuple[DisplayItem.DisplayItem, DisplayItem.DisplayDataChannel, typing.Optional[int]]]) -> None:
display_item, display_data_channel, index_value = args if args else (None, None, 0)
if display_data_channel and index_value is not None:
if not self.__slider_row.canvas_items:

# async loop to track a value change stream from the slider canvas item.
# the value change stream will be produced when the user changes the value
# of the slider by either dragging or paging the thumb.
async def track_slider_canvas_item_value(index_value_adapter: IndexValueAdapter,
display_data_channel_value_stream: Stream.ValueStream[DisplayItem.DisplayDataChannel],
r: Stream.ValueChangeStreamReactorInterface[float]) -> None:
while True:
value_change = await r.next_value_change()
if value_change.is_end:
break
display_data_channel = display_data_channel_value_stream.value
if display_data_channel:
index_value_adapter.apply_index_value_change(display_data_channel, value_change)
else:
break

self.__value_change_stream_reactor = Stream.ValueChangeStreamReactor[float](
self.__slider_canvas_item.value_change_stream,
functools.partial(track_slider_canvas_item_value, self.__index_value_adapter, self.__display_data_channel_value_stream),
self.__event_loop)

label = CanvasItem.StaticTextCanvasItem("WWW")
label.size_to_content(self.__get_font_metrics_fn)
label.text = self.__title
Expand All @@ -740,16 +762,12 @@ def play_button_model_changed(value: typing.Optional[bool]) -> None:
self.__slider_row.add_spacing(0)
self.__slider_row.add_canvas_item(self.__slider_canvas_item)
self.__slider_row.add_canvas_item(self.__slider_text)
# display_data_channel may have changed, so do this every time
self.__slider_value_action = Stream.ValueStreamAction(self.__slider_canvas_item.value_change_stream, functools.partial(self.__index_value_adapter.apply_index_value_change, display_data_channel))
self.__slider_text.text = self.__index_value_adapter.get_index_str(display_data_channel)
self.__slider_text.size_to_content(self.__get_font_metrics_fn)
self.__slider_canvas_item.value = self.__index_value_adapter.get_index_value(display_data_channel)
self.__slider_canvas_item.update_sizing(self.__slider_canvas_item.sizing.with_preferred_width(360))
else:
if self.__slider_value_action:
self.__slider_value_action.close()
self.__slider_value_action = None
self.__value_change_stream_reactor = None
self.__slider_row.remove_all_canvas_items()
self.__slider_canvas_item = CanvasItem.SliderCanvasItem()
self.__slider_text = CanvasItem.StaticTextCanvasItem("9999")
Expand Down Expand Up @@ -1723,6 +1741,92 @@ def display_data_channel(self, value: typing.Optional[DisplayItem.DisplayDataCha
self.__display_data_channel = value


class ChangeGraphicsInteractiveTask(DisplayCanvasItem.InteractiveTask):
def __init__(self, display_panel: DisplayPanel) -> None:
super().__init__()
self.__display_panel = display_panel
display_item = display_panel.display_item
assert display_item
self.__display_item = display_item
self.__undo_command: typing.Optional[Undo.UndoableCommand] = self.__display_panel.create_change_graphics_command()
self.__display_panel.begin_mouse_tracking()

def _close(self) -> None:
self.__display_panel.end_mouse_tracking(None)
if self.__undo_command:
self.__undo_command.close()
self.__undo_command = None

def _commit(self) -> None:
undo_command = self.__undo_command
self.__undo_command = None
assert undo_command
undo_command.perform()
self.__display_panel.document_controller.push_undo_command(undo_command)


class ChangeDisplayPropertiesInteractiveTask(DisplayCanvasItem.InteractiveTask):
def __init__(self, display_panel: DisplayPanel) -> None:
super().__init__()
self.__display_panel = display_panel
display_item = display_panel.display_item
assert display_item
self.__display_item = display_item
self.__display_properties = copy.copy(display_item.display_properties)
self.__undo_command: typing.Optional[Undo.UndoableCommand] = self.__display_panel.create_change_display_command()
self.__display_panel.begin_mouse_tracking()

def _close(self) -> None:
self.__display_panel.end_mouse_tracking(None)
if self.__undo_command:
self.__undo_command.close()
self.__undo_command = None

def _commit(self) -> None:
keys = set(self.__display_properties.keys()).union(set(self.__display_item.display_properties.keys()))
changed_properties = dict[str, typing.Any]()
for key in keys:
if self.__display_properties.get(key) != self.__display_item.display_properties.get(key):
changed_properties[key] = self.__display_item.display_properties.get(key)
self.__display_panel.update_display_properties(changed_properties)
undo_command = self.__undo_command
self.__undo_command = None
assert undo_command
undo_command.perform()
self.__display_panel.document_controller.push_undo_command(undo_command)


class CreateGraphicInteractiveTask(DisplayCanvasItem.InteractiveTask):
def __init__(self, display_panel: DisplayPanel, graphic_type: str, start_position: Geometry.FloatPoint) -> None:
super().__init__()
self.__display_panel = display_panel
display_item = display_panel.display_item
assert display_item
self.__display_item = display_item
self.__graphic_type = graphic_type
self.__graphic_properties = dict[str, typing.Any]()
self.__display_panel.begin_mouse_tracking()
from nion.swift import DocumentController # avoid circular reference. needs rethinking.
graphic_factory = DocumentController.graphic_factory_table[graphic_type]
graphic_properties = graphic_factory.get_graphic_properties_from_position(self.__display_item, start_position)
graphic = graphic_factory.create_graphic_in_display_item(display_panel.document_controller, display_item, graphic_properties)
self.__undo_command: typing.Optional[Undo.UndoableCommand] = display_panel.create_insert_graphics_command([graphic])
self._graphic = graphic

def _close(self) -> None:
self.__display_panel.end_mouse_tracking(None)
if self.__undo_command:
self.__undo_command.close()
self.__undo_command = None

def _commit(self) -> None:
undo_command = self.__undo_command
self.__undo_command = None
assert undo_command
undo_command.perform()
self.__display_panel.document_controller.push_undo_command(undo_command)


class DisplayPanel(CanvasItem.LayerCanvasItem):
"""A canvas item to display a library item. Allows library item to be changed."""

Expand Down Expand Up @@ -2308,16 +2412,19 @@ def add_display_controls(display_canvas_item: DisplayCanvasItem.DisplayCanvasIte
self.__display_item_value_stream,
SequenceIndexAdapter(self.document_controller),
self.ui.get_font_metrics,
self.__document_controller.event_loop,
self.__playback_controller.handle_play_button,
self.__playback_controller.is_movie_playing)
c0_slider_row = IndexValueSliderCanvasItem(_("C0"),
self.__display_item_value_stream,
CollectionIndexAdapter(self.document_controller, 0),
self.ui.get_font_metrics)
self.ui.get_font_metrics,
self.__document_controller.event_loop)
c1_slider_row = IndexValueSliderCanvasItem(_("C1"),
self.__display_item_value_stream,
CollectionIndexAdapter(self.document_controller, 1),
self.ui.get_font_metrics)
self.ui.get_font_metrics,
self.__document_controller.event_loop)
display_canvas_item.add_display_control(related_icons_canvas_item, "related_icons")
display_canvas_item.add_display_control(sequence_slider_row)
display_canvas_item.add_display_control(c0_slider_row)
Expand Down Expand Up @@ -2813,6 +2920,15 @@ def create_change_graphics_command(self) -> ChangeGraphicsCommand:
def push_undo_command(self, command: Undo.UndoableCommand) -> None:
self.__document_controller.push_undo_command(command)

def create_change_display_properties_task(self) -> DisplayCanvasItem.InteractiveTask:
return ChangeDisplayPropertiesInteractiveTask(self)

def create_change_graphics_task(self) -> DisplayCanvasItem.InteractiveTask:
return ChangeGraphicsInteractiveTask(self)

def create_create_graphic_task(self, graphic_type: str, start_position: Geometry.FloatPoint) -> DisplayCanvasItem.InteractiveTask:
return CreateGraphicInteractiveTask(self, graphic_type, start_position)

def create_rectangle(self, pos: Geometry.FloatPoint) -> Graphics.RectangleGraphic:
assert self.__display_item
self.__display_item.graphic_selection.clear()
Expand Down
Loading