From c4f9bf4d7aa3e4409110f95c7510da764bf66b53 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Sat, 11 Nov 2023 11:32:50 -0800 Subject: [PATCH 01/11] Minor code change. --- nion/swift/model/Symbolic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nion/swift/model/Symbolic.py b/nion/swift/model/Symbolic.py index d530dda68..fedf9890c 100644 --- a/nion/swift/model/Symbolic.py +++ b/nion/swift/model/Symbolic.py @@ -248,10 +248,8 @@ def __init__(self, entity_type: typing.Optional[Schema.EntityType] = None, self._set_field_value("reference", reference) def __eq__(self, other: typing.Any) -> bool: - # written like this to work around regression in mypy 1.7 - if other: - if isinstance(other, self.__class__): - return self.write() == other.write() + if isinstance(other, self.__class__): + return self.write() == other.write() return False def __hash__(self) -> typing.Any: From 8c4623b0b45a170e838bf2902fef3abf652ccb6b Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Sat, 11 Nov 2023 11:33:20 -0800 Subject: [PATCH 02/11] Add raster-set-display-limits action and use it in Histogram. --- nion/swift/DocumentController.py | 25 +++++++++++++++++++++++++ nion/swift/HistogramPanel.py | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index eb33b665e..3220f1940 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -4004,6 +4004,30 @@ def is_enabled(self, context: Window.ActionContext) -> bool: return context.display_panel is not None and context.display_panel.display_canvas_item is not None +class RasterDisplaySetDisplayLimitsAction(Window.Action): + action_id = "raster_display.set_display_limits" + action_name = _("Set Display Limits") + + def execute(self, context: Window.ActionContext) -> Window.ActionResult: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + display_item = context.display_item + display_data_channel = display_item.display_data_channel if display_item else None + if display_data_channel: + display_limits = context.parameters["display_limits"] + command = DisplayPanel.ChangeDisplayDataChannelCommand(context.model, + display_data_channel, + display_limits=display_limits, + title=_("Change Display Limits")) + command.perform() + window.push_undo_command(command) + return Window.ActionResult(Window.ActionStatus.FINISHED) + + def is_enabled(self, context: Window.ActionContext) -> bool: + context = typing.cast(DocumentController.ActionContext, context) + return context.display_panel is not None + + Window.register_action(LineProfileGraphicAction("line_profile.expand", _("Expand Line Profile Width"), 1.0)) Window.register_action(LineProfileGraphicAction("line_profile.contract", _("Contract Line Profile Width"), -1.0)) Window.register_action(RasterDisplayFitToViewAction()) @@ -4031,6 +4055,7 @@ def is_enabled(self, context: Window.ActionContext) -> bool: Window.register_action(RasterDisplayMoveAction("raster_display.move_down", _("Move Display Down"), Geometry.FloatSize(width=0, height=-100))) Window.register_action(RasterDisplayNudgeSliceAction("raster_display.nudge_slice_left", _("Nudge Slice Left"), -1)) Window.register_action(RasterDisplayNudgeSliceAction("raster_display.nudge_slice_right", _("Nudge Slice Right"), 1)) +Window.register_action(RasterDisplaySetDisplayLimitsAction()) class LinePlotDisplayAutoDisplayAction(Window.Action): diff --git a/nion/swift/HistogramPanel.py b/nion/swift/HistogramPanel.py index 32a7afac6..4ebe2bf68 100644 --- a/nion/swift/HistogramPanel.py +++ b/nion/swift/HistogramPanel.py @@ -396,9 +396,9 @@ def set_display_limits(display_limits: typing.Optional[typing.Tuple[float, float upper_display_limit = data_min + display_limits[1] * (data_max - data_min) new_display_limits = (lower_display_limit, upper_display_limit) - command = DisplayPanel.ChangeDisplayDataChannelCommand(document_controller.document_model, display_data_channel, display_limits=new_display_limits, title=_("Change Display Limits")) - command.perform() - document_controller.push_undo_command(command) + action_context = document_controller._get_action_context() + action_context.parameters["display_limits"] = new_display_limits + document_controller.perform_action_in_context("raster_display.set_display_limits", action_context) def cursor_changed(canvas_x: typing.Optional[float]) -> None: if callable(cursor_changed_fn): From 673ffa9b2e72115e2321f2f6f32579036df79e59 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Fri, 16 Jul 2021 16:49:48 -0700 Subject: [PATCH 03/11] Use reactor style tracking for display panel slider. --- nion/swift/DisplayPanel.py | 49 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index fd9b2303c..992acf99d 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -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() @@ -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 @@ -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 @@ -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") @@ -2308,16 +2326,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) From c723a7f70532b6fd2e5dc87e2dc93988ec55c175 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Sat, 11 Nov 2023 14:57:47 -0800 Subject: [PATCH 04/11] Use reactor for image canvas item hand tool. --- nion/swift/ImageCanvasItem.py | 85 ++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index b3b4bca44..c3277bb16 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -439,6 +439,50 @@ def wheel_changed(self, x: int, y: int, dx: int, dy: int, is_horizontal: bool) - return False +class HandMouseHandler: + def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop) -> None: + self.__image_canvas_item = image_canvas_item + self.__mouse_value_stream = Stream.ValueStream[ + typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]() + self.__mouse_value_change_stream = Stream.ValueChangeStream(self.__mouse_value_stream) + self.__undo_command: typing.Optional[Undo.UndoableCommand] = None + self.__last_drag_pos: typing.Optional[Geometry.IntPoint] = None + self.__reactor = Stream.ValueChangeStreamReactor[typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]( + self.__mouse_value_change_stream, + self.hand_reactor, + event_loop) + + def mouse_pressed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: + self.__mouse_value_stream.value = mouse_pos, modifiers + self.__last_drag_pos = mouse_pos + self.__mouse_value_change_stream.begin() + if self.__image_canvas_item.delegate: + self.__image_canvas_item.delegate.begin_mouse_tracking() + + def mouse_position_changed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: + self.__mouse_value_stream.value = (mouse_pos, modifiers) + + def mouse_released(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: + self.__mouse_value_stream.value = mouse_pos, modifiers + self.__mouse_value_change_stream.end() + if self.__image_canvas_item.delegate: + self.__image_canvas_item.delegate.end_mouse_tracking(self.__undo_command) + + async def hand_reactor(self, r: Stream.ValueChangeStreamReactorInterface[typing.Tuple[Geometry.IntPoint, UserInterface.KeyboardModifiers]]) -> None: + while True: + value_change = await r.next_value_change() + if value_change.is_end: + break + assert self.__last_drag_pos + if value_change.value is not None: + mouse_pos, modifiers = value_change.value + if not self.__undo_command and self.__image_canvas_item.delegate: + self.__undo_command = self.__image_canvas_item.delegate.create_change_display_command() + delta = mouse_pos - self.__last_drag_pos + self.__image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) + self.__last_drag_pos = mouse_pos + + class ImageCanvasItem(DisplayCanvasItem.DisplayCanvasItem): """A canvas item to paint an image. @@ -537,10 +581,9 @@ def __init__(self, ui_settings: UISettings.UISettings, self.__graphic_drag_item: typing.Optional[Graphics.Graphic] = None self.__graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() self.__graphic_drag_indexes: typing.Set[int] = set() - self.__last_drag_pos: typing.Optional[Geometry.FloatPoint] = None self.__last_mouse: typing.Optional[Geometry.IntPoint] = None - self.__is_dragging = False self.__mouse_in = False + self.__mouse_handler: typing.Optional[HandMouseHandler] = None # frame rate and latency self.__display_frame_rate_id: typing.Optional[str] = None @@ -712,7 +755,7 @@ def handle_auto_display(self) -> bool: return True # update the image canvas position by the widget delta amount. called on main thread. - def __update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> None: + def _update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> None: # create a widget mapping to get from image norm to widget coordinates and back delegate = self.delegate widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds, list()) @@ -761,7 +804,13 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie if delegate.image_mouse_pressed(image_position, modifiers): return True self.__undo_command = None - delegate.begin_mouse_tracking() + if delegate.tool_mode == "hand": + assert not self.__mouse_handler + assert self.__event_loop + self.__mouse_handler = HandMouseHandler(self, self.__event_loop) + self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) + else: + delegate.begin_mouse_tracking() # figure out clicked graphic self.__graphic_drag_items = list() self.__graphic_drag_item = None @@ -999,9 +1048,7 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie self.__graphic_drag_items.append(graphic) self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "hand": - self.__last_drag_pos = mouse_pos - self.__is_dragging = True + return True def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool: @@ -1032,14 +1079,17 @@ def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifi delegate.remove_index_from_selection(graphic_index) else: delegate.add_index_to_selection(graphic_index) - delegate.end_mouse_tracking(self.__undo_command) + if self.__mouse_handler: + self.__mouse_handler.mouse_released(Geometry.IntPoint(y, x), modifiers) + self.__mouse_handler = None + else: + # mouse handler will do this part + delegate.end_mouse_tracking(self.__undo_command) self.__undo_command = None self.__graphic_drag_items = list() self.__graphic_drag_item = None self.__graphic_part_data = dict() self.__graphic_drag_indexes = set() - self.__last_drag_pos = None - self.__is_dragging = False if delegate.tool_mode != "hand": delegate.tool_mode = "pointer" return True @@ -1112,13 +1162,8 @@ def get_pointer_tool_shape() -> str: delegate.adjust_graphics(widget_mapping, self.__graphic_drag_items, self.__graphic_drag_part, self.__graphic_part_data, self.__graphic_drag_start_pos, mouse_pos, modifiers) self.__graphic_drag_changed = True - elif self.__is_dragging: - assert self.__last_drag_pos - if not self.__undo_command: - self.__undo_command = delegate.create_change_display_command() - delta = mouse_pos - self.__last_drag_pos - self.__update_image_canvas_position(-delta.as_size()) - self.__last_drag_pos = mouse_pos + if self.__mouse_handler: + self.__mouse_handler.mouse_position_changed(Geometry.IntPoint(y, x), modifiers) return True def wheel_changed(self, x: int, y: int, dx: int, dy: int, is_horizontal: bool) -> bool: @@ -1127,13 +1172,13 @@ def wheel_changed(self, x: int, y: int, dx: int, dy: int, is_horizontal: bool) - dx = dx if is_horizontal else 0 dy = dy if not is_horizontal else 0 command = delegate.create_change_display_command(command_id="image_position", is_mergeable=True) - self.__update_image_canvas_position(Geometry.FloatSize(-dy, -dx)) + self._update_image_canvas_position(Geometry.FloatSize(-dy, -dx)) delegate.push_undo_command(command) return True return False def pan_gesture(self, dx: int, dy: int) -> bool: - self.__update_image_canvas_position(Geometry.FloatSize(dy, dx)) + self._update_image_canvas_position(Geometry.FloatSize(dy, dx)) return True def context_menu_event(self, x: int, y: int, gx: int, gy: int) -> bool: @@ -1300,7 +1345,7 @@ def apply_move_command(self, delta: Geometry.FloatSize) -> None: delegate = self.delegate if delegate: command = delegate.create_change_display_command(command_id="image_nudge", is_mergeable=True) - self.__update_image_canvas_position(delta) + self._update_image_canvas_position(delta) delegate.push_undo_command(command) def set_fit_mode(self) -> None: From f437ec1f4ff60f8ae8c8d728209f2b27cc38e374 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Sat, 11 Nov 2023 16:11:16 -0800 Subject: [PATCH 05/11] Integrate action into hand tool. First pass. --- nion/swift/DisplayCanvasItem.py | 3 ++ nion/swift/DisplayPanel.py | 13 ++++++ nion/swift/DocumentController.py | 36 ++++++++++++++++- nion/swift/ImageCanvasItem.py | 59 +++++++++++++++++----------- nion/swift/test/DisplayPanel_test.py | 36 +++++++++++++++++ 5 files changed, 124 insertions(+), 23 deletions(-) diff --git a/nion/swift/DisplayCanvasItem.py b/nion/swift/DisplayCanvasItem.py index 4fcef63e6..914b24016 100644 --- a/nion/swift/DisplayCanvasItem.py +++ b/nion/swift/DisplayCanvasItem.py @@ -8,6 +8,7 @@ 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 @@ -31,6 +32,8 @@ 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 perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: ... + def prepare_command_action(self, action_or_action_id: typing.Union[str, Window.Action], **kwargs: typing.Any) -> typing.Optional[Window.ActionContext]: ... 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: ... diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index 992acf99d..ed5698141 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -2834,6 +2834,19 @@ def create_change_graphics_command(self) -> ChangeGraphicsCommand: def push_undo_command(self, command: Undo.UndoableCommand) -> None: self.__document_controller.push_undo_command(command) + # naming to avoid conflict without older perform_action method above + def perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: + for key, value in iter(kwargs.items()): + action_context.parameters[key] = value + self.__document_controller.perform_action_in_context(action_or_action_id, action_context) + + def prepare_command_action(self, action_or_action_id: typing.Union[str, Window.Action], **kwargs: typing.Any) -> typing.Optional[DocumentController.DocumentController.ActionContext]: + action_context = self.__document_controller._get_action_context() + for key, value in iter(kwargs.items()): + action_context.parameters[key] = value + self.__document_controller.prepare_action_in_context(action_or_action_id, action_context) + return action_context + def create_rectangle(self, pos: Geometry.FloatPoint) -> Graphics.RectangleGraphic: assert self.__display_item self.__display_item.graphic_selection.clear() diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index 3220f1940..4c70b7162 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -60,6 +60,7 @@ from nion.ui import PreferencesDialog from nion.ui import Window from nion.ui import UserInterface +from nion.ui.Window import ActionContext from nion.utils import Color from nion.utils import Event from nion.utils import Geometry @@ -3897,6 +3898,38 @@ def is_enabled(self, context: Window.ActionContext) -> bool: return context.display_item is not None and context.display_item.used_display_type == "image" +class RasterDisplaySetImagePositionAction(Window.Action): + action_id = "raster_display.set_image_position" + action_name = _("Set Image Position") + + def __init__(self) -> None: + super().__init__() + + def execute(self, context: Window.ActionContext) -> Window.ActionResult: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + if context.display_panel: + image_position = context.parameters["image_position"] + command = getattr(context, "_undo_command") + setattr(context, "_undo_command", None) + self.__undo_command = None + if not command: + command = context.display_panel.create_change_display_command() + context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) + command.perform() + window.push_undo_command(command) + return Window.ActionResult(Window.ActionStatus.FINISHED) + + def invoke_prepare(self, context: ActionContext) -> None: + context = typing.cast(DocumentController.ActionContext, context) + if context.display_panel: + setattr(context, "_undo_command", context.display_panel.create_change_display_command()) + + def is_enabled(self, context: Window.ActionContext) -> bool: + context = typing.cast(DocumentController.ActionContext, context) + return context.display_item is not None and context.display_item.used_display_type == "image" + + class RasterDisplayZoomOutAction(Window.Action): action_id = "raster_display.zoom_out" action_name = _("Zoom Out") @@ -4025,7 +4058,7 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult: def is_enabled(self, context: Window.ActionContext) -> bool: context = typing.cast(DocumentController.ActionContext, context) - return context.display_panel is not None + return context.display_item is not None and context.display_item.used_display_type == "image" Window.register_action(LineProfileGraphicAction("line_profile.expand", _("Expand Line Profile Width"), 1.0)) @@ -4034,6 +4067,7 @@ def is_enabled(self, context: Window.ActionContext) -> bool: Window.register_action(RasterDisplayFillViewAction()) Window.register_action(RasterDisplayOneViewAction()) Window.register_action(RasterDisplayTwoViewAction()) +Window.register_action(RasterDisplaySetImagePositionAction()) Window.register_action(RasterDisplayZoomOutAction()) Window.register_action(RasterDisplayZoomInAction()) Window.register_action(RasterDisplayAutoDisplayAction()) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index c3277bb16..9befdebe9 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -30,6 +30,7 @@ from nion.swift.model import Persistence from nion.ui import DrawingContext from nion.ui import UserInterface + from nion.ui import Window @@ -445,8 +446,6 @@ def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.Abstr self.__mouse_value_stream = Stream.ValueStream[ typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]() self.__mouse_value_change_stream = Stream.ValueChangeStream(self.__mouse_value_stream) - self.__undo_command: typing.Optional[Undo.UndoableCommand] = None - self.__last_drag_pos: typing.Optional[Geometry.IntPoint] = None self.__reactor = Stream.ValueChangeStreamReactor[typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]( self.__mouse_value_change_stream, self.hand_reactor, @@ -454,10 +453,7 @@ def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.Abstr def mouse_pressed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: self.__mouse_value_stream.value = mouse_pos, modifiers - self.__last_drag_pos = mouse_pos self.__mouse_value_change_stream.begin() - if self.__image_canvas_item.delegate: - self.__image_canvas_item.delegate.begin_mouse_tracking() def mouse_position_changed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: self.__mouse_value_stream.value = (mouse_pos, modifiers) @@ -465,22 +461,31 @@ def mouse_position_changed(self, mouse_pos: Geometry.IntPoint, modifiers: UserIn def mouse_released(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: self.__mouse_value_stream.value = mouse_pos, modifiers self.__mouse_value_change_stream.end() - if self.__image_canvas_item.delegate: - self.__image_canvas_item.delegate.end_mouse_tracking(self.__undo_command) async def hand_reactor(self, r: Stream.ValueChangeStreamReactorInterface[typing.Tuple[Geometry.IntPoint, UserInterface.KeyboardModifiers]]) -> None: + if self.__image_canvas_item.delegate: + self.__image_canvas_item.delegate.begin_mouse_tracking() + action_context: typing.Optional[Window.ActionContext] = None + image_position: typing.Optional[Geometry.FloatPoint] = None + last_drag_pos: typing.Optional[Geometry.IntPoint] = None while True: value_change = await r.next_value_change() if value_change.is_end: break - assert self.__last_drag_pos if value_change.value is not None: mouse_pos, modifiers = value_change.value - if not self.__undo_command and self.__image_canvas_item.delegate: - self.__undo_command = self.__image_canvas_item.delegate.create_change_display_command() - delta = mouse_pos - self.__last_drag_pos - self.__image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) - self.__last_drag_pos = mouse_pos + if value_change.is_begin: + last_drag_pos = mouse_pos + assert last_drag_pos + if not action_context and self.__image_canvas_item.delegate: + action_context = self.__image_canvas_item.delegate.prepare_command_action("raster_display.set_image_position") + delta = mouse_pos - last_drag_pos + image_position = self.__image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) + last_drag_pos = mouse_pos + if self.__image_canvas_item.delegate: + self.__image_canvas_item.delegate.end_mouse_tracking(None) + if action_context and image_position: + self.__image_canvas_item.delegate.perform_command_action("raster_display.set_image_position", action_context, image_position=image_position) class ImageCanvasItem(DisplayCanvasItem.DisplayCanvasItem): @@ -754,9 +759,24 @@ def handle_auto_display(self) -> bool: delegate.update_display_data_channel_properties({"display_limits": (mn, mx)}) return True + def _set_image_canvas_position(self, image_position: Geometry.FloatPoint) -> None: + # create a widget mapping to get from image norm to widget coordinates and back + delegate = self.delegate + widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds, list()) + if delegate and widget_mapping: + self.__image_position = image_position + self.__scroll_area_layout._image_position = self.__image_position + delegate.update_display_properties({"image_position": list(self.__image_position), "image_canvas_mode": "custom"}) + # and update the image canvas accordingly + self.__image_canvas_mode = "custom" + self.__scroll_area_layout._image_canvas_mode = self.__image_canvas_mode + self.scroll_area_canvas_item._needs_layout(self.scroll_area_canvas_item) + self.__composite_canvas_item.update() + # update the image canvas position by the widget delta amount. called on main thread. - def _update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> None: + def _update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> Geometry.FloatPoint: # create a widget mapping to get from image norm to widget coordinates and back + new_image_canvas_position = Geometry.FloatPoint() delegate = self.delegate widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds, list()) if delegate and widget_mapping: @@ -770,14 +790,9 @@ def _update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> Non new_image_norm_center_0 = max(min(new_image_norm_center[0], 1.0), 0.0) new_image_norm_center_1 = max(min(new_image_norm_center[1], 1.0), 0.0) # save the new image norm center - self.__image_position = Geometry.FloatPoint(new_image_norm_center_0, new_image_norm_center_1) - self.__scroll_area_layout._image_position = self.__image_position - delegate.update_display_properties({"image_position": list(self.__image_position), "image_canvas_mode": "custom"}) - # and update the image canvas accordingly - self.__image_canvas_mode = "custom" - self.__scroll_area_layout._image_canvas_mode = self.__image_canvas_mode - self.scroll_area_canvas_item._needs_layout(self.scroll_area_canvas_item) - self.__composite_canvas_item.update() + new_image_canvas_position = Geometry.FloatPoint(new_image_norm_center_0, new_image_norm_center_1) + self._set_image_canvas_position(new_image_canvas_position) + return new_image_canvas_position def mouse_clicked(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool: if super().mouse_clicked(x, y, modifiers): diff --git a/nion/swift/test/DisplayPanel_test.py b/nion/swift/test/DisplayPanel_test.py index 3b84a742d..ae3f6c33a 100644 --- a/nion/swift/test/DisplayPanel_test.py +++ b/nion/swift/test/DisplayPanel_test.py @@ -3038,6 +3038,42 @@ def test_index_sequence_slider_works_after_dropping_onto_existing_display_panel( document_controller.periodic() self.assertEqual(2, display_item.display_data_channel.sequence_index) + def test_hand_tool_undo(self): + # testing during the development of the mouse handler and the undo/redo system with actions + with TestContext.create_memory_context() as test_context: + document_controller = test_context.create_document_controller() + document_model = document_controller.document_model + display_panel = document_controller.selected_display_panel + data_item = DataItem.DataItem(numpy.zeros((10, 10))) + document_model.append_data_item(data_item) + display_item = document_model.get_display_item_for_data_item(data_item) + display_panel.set_display_panel_display_item(display_item) + header_height = display_panel.header_canvas_item.header_height + display_panel.root_container.layout_immediate((1000 + header_height, 1000)) + # run test + display_panel.perform_action("set_fit_mode") + self.assertEqual((0.5, 0.5), tuple(display_item.display_properties["image_position"])) + document_controller.tool_mode = "hand" + display_panel.display_canvas_item.simulate_drag((100,100), (200,200)) + document_controller.periodic() + self.assertEqual((0.4, 0.4), tuple(display_item.display_properties["image_position"])) + # undo check assumptions + document_controller.handle_undo() + self.assertEqual((0.5, 0.5), tuple(display_item.display_properties["image_position"])) + # redo check assumptions + document_controller.handle_redo() + self.assertEqual((0.4, 0.4), tuple(display_item.display_properties["image_position"])) + # move again + display_panel.display_canvas_item.simulate_drag((100,100), (200,200)) + document_controller.periodic() + self.assertEqual((0.3, 0.3), tuple(display_item.display_properties["image_position"])) + # undo check assumptions + document_controller.handle_undo() + self.assertEqual((0.4, 0.4), tuple(display_item.display_properties["image_position"])) + # undo again check assumptions + document_controller.handle_undo() + self.assertEqual((0.5, 0.5), tuple(display_item.display_properties["image_position"])) + if __name__ == '__main__': logging.getLogger().setLevel(logging.DEBUG) From 8f3a9dd741c42318b176694bead00291f7d3041b Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Mon, 13 Nov 2023 14:30:15 -0800 Subject: [PATCH 06/11] Add create line mouse handler. Preliminary. --- nion/swift/DisplayCanvasItem.py | 3 +- nion/swift/DisplayPanel.py | 3 + nion/swift/DocumentController.py | 63 ++++++++- nion/swift/ImageCanvasItem.py | 183 +++++++++++++++++++++++---- nion/swift/test/DisplayPanel_test.py | 39 ++++-- nion/swift/test/Graphics_test.py | 1 + 6 files changed, 257 insertions(+), 35 deletions(-) diff --git a/nion/swift/DisplayCanvasItem.py b/nion/swift/DisplayCanvasItem.py index 914b24016..dbb621faa 100644 --- a/nion/swift/DisplayCanvasItem.py +++ b/nion/swift/DisplayCanvasItem.py @@ -32,8 +32,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 perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: ... def prepare_command_action(self, action_or_action_id: typing.Union[str, Window.Action], **kwargs: typing.Any) -> typing.Optional[Window.ActionContext]: ... + def perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: ... + def cancel_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext) -> None: ... 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: ... diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index ed5698141..bb7b8ec9f 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -2847,6 +2847,9 @@ def prepare_command_action(self, action_or_action_id: typing.Union[str, Window.A self.__document_controller.prepare_action_in_context(action_or_action_id, action_context) return action_context + def cancel_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext) -> None: + self.__document_controller.cancel_action_in_context(action_or_action_id, action_context) + def create_rectangle(self, pos: Geometry.FloatPoint) -> Graphics.RectangleGraphic: assert self.__display_item self.__display_item.graphic_selection.clear() diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index 4c70b7162..39aa39434 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -3830,6 +3830,61 @@ def is_enabled(self, context: Window.ActionContext) -> bool: return len(graphic_type_set) == 1 and list(graphic_type_set)[0] == "line-profile" +class RasterDisplayCreateGraphicAction(Window.Action): + action_id = "raster_display.add_graphic" + action_name = _("Create Graphic") + + graphics_table = { + "line": (_("Line Graphic"), Graphics.LineGraphic), + "rectangle": (_("Rectangle Graphic"), Graphics.RectangleGraphic), + "ellipse": (_("Ellipse Graphic"), Graphics.EllipseGraphic), + "point": (_("Point Graphic"), Graphics.PointGraphic), + "line-profile": (_("Line Profile Graphic"), Graphics.LineProfileGraphic), + "spot": (_("Spot Graphic"), Graphics.SpotGraphic), + "wedge": (_("Wedge Graphic"), Graphics.WedgeGraphic), + "ring": (_("Ring Graphic"), Graphics.RingGraphic), + "lattice": (_("Lattice Graphic"), Graphics.LatticeGraphic), + } + + def __init__(self) -> None: + super().__init__() + + def execute(self, context: Window.ActionContext) -> Window.ActionResult: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + if context.display_panel: + graphic = getattr(context, "_graphic") + setattr(context, "_graphic", None) + assert graphic # TODO: make this work using parameters + command = getattr(context, "_undo_command") + setattr(context, "_undo_command", None) + if not command: + command = context.display_panel.create_insert_graphics_command([graphic]) + for key, value in context.parameters.get("graphic_properties", dict[str, typing.Any]()).items(): + setattr(graphic, key, value) + command.perform() + window.push_undo_command(command) + return Window.ActionResult(Window.ActionStatus.FINISHED) + + # def invoke_prepare(self, context: ActionContext) -> None: + # context = typing.cast(DocumentController.ActionContext, context) + # if context.display_panel: + # setattr(context, "_undo_command", context.display_panel.create_insert_graphics_command()) + + # def cancel_prepare(self, context: ActionContext) -> None: + # command = getattr(context, "_undo_command") + # setattr(context, "_undo_command", None) + # if command: + # command.close() + + def get_action_name(self, context: ActionContext) -> str: + return _("Create") + " " + self.graphics_table[context.parameters["graphic_type"]][0] + + def is_enabled(self, context: Window.ActionContext) -> bool: + context = typing.cast(DocumentController.ActionContext, context) + return context.display_item is not None and context.display_item.used_display_type == "image" + + class RasterDisplayFillViewAction(Window.Action): action_id = "raster_display.fill_view" action_name = _("Fill View") @@ -3912,7 +3967,6 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult: image_position = context.parameters["image_position"] command = getattr(context, "_undo_command") setattr(context, "_undo_command", None) - self.__undo_command = None if not command: command = context.display_panel.create_change_display_command() context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) @@ -3925,6 +3979,12 @@ def invoke_prepare(self, context: ActionContext) -> None: if context.display_panel: setattr(context, "_undo_command", context.display_panel.create_change_display_command()) + def cancel_prepare(self, context: ActionContext) -> None: + command = getattr(context, "_undo_command") + setattr(context, "_undo_command", None) + if command: + command.close() + def is_enabled(self, context: Window.ActionContext) -> bool: context = typing.cast(DocumentController.ActionContext, context) return context.display_item is not None and context.display_item.used_display_type == "image" @@ -4063,6 +4123,7 @@ def is_enabled(self, context: Window.ActionContext) -> bool: Window.register_action(LineProfileGraphicAction("line_profile.expand", _("Expand Line Profile Width"), 1.0)) Window.register_action(LineProfileGraphicAction("line_profile.contract", _("Contract Line Profile Width"), -1.0)) +Window.register_action(RasterDisplayCreateGraphicAction()) Window.register_action(RasterDisplayFitToViewAction()) Window.register_action(RasterDisplayFillViewAction()) Window.register_action(RasterDisplayOneViewAction()) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 9befdebe9..0f58cde4e 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -440,16 +440,16 @@ def wheel_changed(self, x: int, y: int, dx: int, dy: int, is_horizontal: bool) - return False -class HandMouseHandler: +MousePositionAndModifiers = typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"] +MouseHandlerReactorFn = typing.Callable[[Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers]], typing.Coroutine[typing.Any, typing.Any, typing.Any]] + + +class MouseHandler: def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop) -> None: self.__image_canvas_item = image_canvas_item - self.__mouse_value_stream = Stream.ValueStream[ - typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]() + self.__mouse_value_stream = Stream.ValueStream[MousePositionAndModifiers]() self.__mouse_value_change_stream = Stream.ValueChangeStream(self.__mouse_value_stream) - self.__reactor = Stream.ValueChangeStreamReactor[typing.Tuple[Geometry.IntPoint, "UserInterface.KeyboardModifiers"]]( - self.__mouse_value_change_stream, - self.hand_reactor, - event_loop) + self.__reactor = Stream.ValueChangeStreamReactor[MousePositionAndModifiers](self.__mouse_value_change_stream, self.reactor_loop, event_loop) def mouse_pressed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: self.__mouse_value_stream.value = mouse_pos, modifiers @@ -462,30 +462,151 @@ def mouse_released(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface. self.__mouse_value_stream.value = mouse_pos, modifiers self.__mouse_value_change_stream.end() - async def hand_reactor(self, r: Stream.ValueChangeStreamReactorInterface[typing.Tuple[Geometry.IntPoint, UserInterface.KeyboardModifiers]]) -> None: - if self.__image_canvas_item.delegate: - self.__image_canvas_item.delegate.begin_mouse_tracking() - action_context: typing.Optional[Window.ActionContext] = None + async def reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers]) -> None: + await self._reactor_loop(r, self.__image_canvas_item) + + async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: + return + + +class HandMouseHandler(MouseHandler): + + async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: + delegate = image_canvas_item.delegate + assert delegate + + delegate.begin_mouse_tracking() + + # get the beginning mouse position + value_change = await r.next_value_change() + value_change_value = value_change.value + assert value_change.is_begin + assert value_change_value is not None + image_position: typing.Optional[Geometry.FloatPoint] = None - last_drag_pos: typing.Optional[Geometry.IntPoint] = None + + # preliminary setup for the tracking loop. + mouse_pos, modifiers = value_change_value + last_drag_pos = mouse_pos + + action_context = delegate.prepare_command_action("raster_display.set_image_position") + assert action_context + + # mouse tracking loop. wait for values and update the image position. while True: value_change = await r.next_value_change() if value_change.is_end: break if value_change.value is not None: mouse_pos, modifiers = value_change.value - if value_change.is_begin: - last_drag_pos = mouse_pos assert last_drag_pos - if not action_context and self.__image_canvas_item.delegate: - action_context = self.__image_canvas_item.delegate.prepare_command_action("raster_display.set_image_position") delta = mouse_pos - last_drag_pos - image_position = self.__image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) + image_position = image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) last_drag_pos = mouse_pos - if self.__image_canvas_item.delegate: - self.__image_canvas_item.delegate.end_mouse_tracking(None) - if action_context and image_position: - self.__image_canvas_item.delegate.perform_command_action("raster_display.set_image_position", action_context, image_position=image_position) + + delegate.end_mouse_tracking(None) + + # if the image position was set, it means the user moved the image. perform the action. otherwise + # cancel it so the action can release resources (e.g., undo command). + if image_position: + delegate.perform_command_action("raster_display.set_image_position", action_context, image_position=image_position) + else: + delegate.cancel_command_action("raster_display.set_image_position", action_context) + + +class CreateLineGraphicMouseHandler(MouseHandler): + + async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: + delegate = image_canvas_item.delegate + assert delegate + + delegate.begin_mouse_tracking() + + # get the beginning mouse position + value_change = await r.next_value_change() + value_change_value = value_change.value + assert value_change.is_begin + assert value_change_value is not None + + # preliminary setup for the tracking loop. + mouse_pos_, modifiers = value_change_value + mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) + widget_mapping = image_canvas_item.mouse_mapping + assert widget_mapping + pos = widget_mapping.map_point_widget_to_image_norm(mouse_pos) + start_drag_pos = mouse_pos + + # create the graphic and assign a drag part + graphic = delegate.create_line(pos) + graphic_drag_part = "end" + + # prepare for undo. move this to the command architecture somehow? + command = delegate.create_insert_graphics_command([graphic]) + + action_context = delegate.prepare_command_action("raster_display.add_graphic") + assert action_context + + delegate.add_index_to_selection(image_canvas_item.graphic_index(graphic)) + # setup drag + selection_indexes = image_canvas_item.graphic_selection.indexes + assert len(selection_indexes) == 1 + graphic_drag_item_was_selected = True + # keep track of general drag information + graphic_drag_start_pos = start_drag_pos + graphic_drag_changed = False + # keep track of info for the specific item that was clicked + # keep track of drag information for each item in the set + graphic_drag_indexes = selection_indexes + graphic_drag_items: typing.List[Graphics.Graphic] = list() + graphic_drag_items.append(graphic) + graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() + graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() + + # mouse tracking loop. wait for values and update the graphics. + while True: + value_change = await r.next_value_change() + if value_change.is_end: + break + if value_change.value is not None: + mouse_pos_, modifiers = value_change.value + mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) + force_drag = modifiers.only_option + if force_drag and graphic_drag_part == "all": + if Geometry.distance(mouse_pos, graphic_drag_start_pos) <= 2: + delegate.drag_graphics(graphic_drag_items) + continue + delegate.adjust_graphics(widget_mapping, graphic_drag_items, graphic_drag_part, graphic_part_data, graphic_drag_start_pos, mouse_pos, modifiers) + graphic_drag_changed = True + + graphics = list(image_canvas_item.graphics) + for index in graphic_drag_indexes: + graphic_ = graphics[index] + graphic_.end_drag(graphic_part_data[index]) + if graphic_drag_items and not graphic_drag_changed: + graphic_index = graphics.index(graphic) + # user didn't move graphic + if not modifiers.control: + # user clicked on a single graphic + delegate.set_selection(graphic_index) + else: + # user control clicked. toggle selection + # if control is down and item is already selected, toggle selection of item + if graphic_drag_item_was_selected: + delegate.remove_index_from_selection(graphic_index) + else: + delegate.add_index_to_selection(graphic_index) + + delegate.end_mouse_tracking(None) + + # if the user did something, perform the action. otherwise cancel it so the action can release resources ( + # e.g., undo command). + if graphic_drag_changed: + setattr(action_context, "_graphic", graphic) + setattr(action_context, "_undo_command", command) + delegate.perform_command_action("raster_display.add_graphic", action_context) + else: + command.close() + delegate.cancel_command_action("raster_display.add_graphic", action_context) class ImageCanvasItem(DisplayCanvasItem.DisplayCanvasItem): @@ -588,7 +709,7 @@ def __init__(self, ui_settings: UISettings.UISettings, self.__graphic_drag_indexes: typing.Set[int] = set() self.__last_mouse: typing.Optional[Geometry.IntPoint] = None self.__mouse_in = False - self.__mouse_handler: typing.Optional[HandMouseHandler] = None + self.__mouse_handler: typing.Optional[MouseHandler] = None # frame rate and latency self.__display_frame_rate_id: typing.Optional[str] = None @@ -759,6 +880,17 @@ def handle_auto_display(self) -> bool: delegate.update_display_data_channel_properties({"display_limits": (mn, mx)}) return True + @property + def graphics(self) -> typing.Sequence[Graphics.Graphic]: + return self.__graphics + + def graphic_index(self, graphic: Graphics.Graphic) -> int: + return self.__graphics.index(graphic) + + @property + def graphic_selection(self) -> DisplayItem.GraphicSelection: + return self.__graphic_selection + def _set_image_canvas_position(self, image_position: Geometry.FloatPoint) -> None: # create a widget mapping to get from image norm to widget coordinates and back delegate = self.delegate @@ -824,6 +956,11 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie assert self.__event_loop self.__mouse_handler = HandMouseHandler(self, self.__event_loop) self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) + if delegate.tool_mode == "line": + assert not self.__mouse_handler + assert self.__event_loop + self.__mouse_handler = CreateLineGraphicMouseHandler(self, self.__event_loop) + self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) else: delegate.begin_mouse_tracking() # figure out clicked graphic @@ -890,7 +1027,7 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie self.__graphic_part_data[index] = graphic.begin_drag() if not self.__graphic_drag_items and not modifiers.control: delegate.clear_selection() - elif delegate.tool_mode == "line": + elif delegate.tool_mode == "line__": graphic = delegate.create_line(pos) delegate.add_index_to_selection(self.__graphics.index(graphic)) if graphic: diff --git a/nion/swift/test/DisplayPanel_test.py b/nion/swift/test/DisplayPanel_test.py index ae3f6c33a..6ca826c8c 100644 --- a/nion/swift/test/DisplayPanel_test.py +++ b/nion/swift/test/DisplayPanel_test.py @@ -1184,16 +1184,35 @@ def test_dragging_to_add_ellipse_makes_desired_ellipse(self): self.assertAlmostEqual(region.bounds[1][0], 0.30) self.assertAlmostEqual(region.bounds[1][1], 0.15) - def test_dragging_to_add_line_makes_desired_line(self): - self.document_controller.tool_mode = "line" - self.display_panel.display_canvas_item.simulate_drag((100,125), (200,250)) - self.assertEqual(len(self.display_item.graphics), 1) - region = self.display_item.graphics[0] - self.assertEqual(region.type, "line-graphic") - self.assertAlmostEqual(region.start[0], 0.1) - self.assertAlmostEqual(region.start[1], 0.125) - self.assertAlmostEqual(region.end[0], 0.2) - self.assertAlmostEqual(region.end[1], 0.25) + def test_dragging_to_add_line_makes_desired_line_and_is_undoable(self): + with TestContext.create_memory_context() as test_context: + # set up the layout + document_controller = test_context.create_document_controller() + document_model = document_controller.document_model + display_panel = document_controller.workspace_controller.display_panels[0] + document_controller.tool_mode = "line" + data_item = DataItem.DataItem(numpy.zeros((10, 10))) + document_model.append_data_item(data_item) + display_item = document_model.get_display_item_for_data_item(data_item) + display_panel.set_display_panel_display_item(display_item) + root_canvas_item = document_controller.workspace_controller.image_row.children[0]._root_canvas_item() + header_height = self.display_panel.header_canvas_item.header_height + root_canvas_item.update_layout(Geometry.IntPoint(), Geometry.IntSize(width=1000, height=1000 + header_height), immediate=True) + # drag to make line + display_panel.display_canvas_item.simulate_drag((100,125), (200,250)) + document_controller.periodic() + self.assertEqual(1, len(display_item.graphics)) + region = display_item.graphics[0] + self.assertEqual("line-graphic", region.type) + self.assertAlmostEqual(0.1, region.start[0]) + self.assertAlmostEqual(0.125, region.start[1]) + self.assertAlmostEqual(0.2, region.end[0]) + self.assertAlmostEqual(0.25, region.end[1]) + # check undo/redo + document_controller.handle_undo() + self.assertEqual(0, len(display_item.graphics)) + document_controller.handle_redo() + self.assertEqual(1, len(display_item.graphics)) def test_dragging_to_add_line_profile_makes_desired_line_profile(self): with TestContext.create_memory_context() as test_context: diff --git a/nion/swift/test/Graphics_test.py b/nion/swift/test/Graphics_test.py index c650e5019..3fccc7ede 100644 --- a/nion/swift/test/Graphics_test.py +++ b/nion/swift/test/Graphics_test.py @@ -130,6 +130,7 @@ def test_create_all_graphic_by_dragging(self): document_controller.tool_mode = tool_mode self.assertEqual(0, len(display_item.graphics)) display_panel.display_canvas_item.simulate_drag((500, 500), (600, 600)) + document_controller.periodic() self.assertEqual(1, len(display_item.graphics)) graphic = display_item.graphics[0] self.assertIsInstance(graphic, graphic_type) From 58aa9e157e36a7db68da49c45eb26556d661a1f8 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Mon, 13 Nov 2023 20:56:05 -0800 Subject: [PATCH 07/11] Continue with line mouse handler, partially generalized. --- nion/swift/DocumentController.py | 253 +++++++++++++++++++++++++++---- nion/swift/ImageCanvasItem.py | 19 ++- 2 files changed, 231 insertions(+), 41 deletions(-) diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index 39aa39434..4df4db1f1 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -2761,6 +2761,184 @@ def _get_action_context_for_display_items(self, display_items: typing.Sequence[D used_data_items) +class GraphicFactoryBase: + def create_graphic_in_display_item(self, window: DocumentController, display_item: DisplayItem.DisplayItem, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.Graphic: + graphic = self.create_graphic(graphic_properties) + display_item.graphic_selection.clear() + display_item.add_graphic(graphic) + display_item.graphic_selection.set(display_item.graphics.index(graphic)) + return graphic + + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + raise NotImplementedError() + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.Graphic: + raise NotImplementedError() + + def _get_calibrated_origin_image_norm(self, display_item :DisplayItem.DisplayItem) -> Geometry.FloatPoint: + display_data_channel = display_item.display_data_channel + assert display_data_channel + display_values = display_data_channel.get_latest_computed_display_values() + assert display_values + element_data_and_metadata = display_values.element_data_and_metadata + assert element_data_and_metadata + data_shape = element_data_and_metadata.datum_dimension_shape + mapping = ImageCanvasItem.ImageCanvasItemMapping.make(data_shape, Geometry.IntRect.unit_rect(), element_data_and_metadata.datum_dimensional_calibrations) + assert mapping + calibrated_origin_image_norm = mapping.calibrated_origin_image_norm + assert calibrated_origin_image_norm + return calibrated_origin_image_norm + + +class LineGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "start": pos.as_tuple(), + "end": pos.as_tuple() + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.LineGraphic: + graphic = Graphics.LineGraphic() + graphic.start = graphic_properties.get("start", (0.2, 0.2)) + graphic.end = graphic_properties.get("end", (0.8, 0.8)) + return graphic + + +class RectangleGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "bounds": (pos.as_tuple(), (0.0, 0.0)), + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.RectangleGraphic: + graphic = Graphics.RectangleGraphic() + graphic.bounds = graphic_properties.get("bounds", ((0.25, 0.25), (0.5, 0.5))) + return graphic + + +class EllipseGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "bounds": (pos.as_tuple(), (0.0, 0.0)), + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.EllipseGraphic: + graphic = Graphics.EllipseGraphic() + graphic.bounds = graphic_properties.get("bounds", ((0.25, 0.25), (0.5, 0.5))) + return graphic + + +class PointGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "position": pos.as_tuple(), + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.PointGraphic: + graphic = Graphics.PointGraphic() + graphic.position = graphic_properties.get("position", (0.5, 0.5)) + return graphic + + +class LineProfileGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "start": pos.as_tuple(), + "end": pos.as_tuple() + } + + def create_graphic_in_display_item(self, window: DocumentController, display_item: DisplayItem.DisplayItem, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.Graphic: + graphic = super().create_graphic_in_display_item(window, display_item, graphic_properties) + data_item = display_item.data_item + assert data_item + document_model = window.document_model + line_profile_data_item = document_model.get_line_profile_new(display_item, data_item, None, graphic) + assert line_profile_data_item + line_profile_display_item = document_model.get_display_item_for_data_item(line_profile_data_item) + assert line_profile_display_item + window.show_display_item(line_profile_display_item) + return graphic + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.LineProfileGraphic: + graphic = Graphics.LineProfileGraphic() + graphic.start = graphic_properties.get("start", (0.2, 0.2)) + graphic.end = graphic_properties.get("end", (0.8, 0.8)) + graphic.width = graphic_properties.get("width", 1.0) + return graphic + + +class SpotGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + return { + "bounds": Geometry.FloatRect.from_center_and_size(pos - self._get_calibrated_origin_image_norm(display_item), Geometry.FloatSize()).as_tuple() + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.SpotGraphic: + graphic = Graphics.SpotGraphic() + graphic.bounds = graphic_properties.get("bounds", ((0.25, 0.25), (0.25, 0.25))) + return graphic + + +class WedgeGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + angle = math.pi - math.atan2(0.5 - pos.y, 0.5 - pos.x) + return { + "end_angle": angle, + "start_angle": angle + math.pi + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.WedgeGraphic: + graphic = Graphics.WedgeGraphic() + graphic.start_angle = graphic_properties.get("start_angle", 0) + graphic.end_angle = graphic_properties.get("end_angle", (3 / 4) * math.pi) + return graphic + + +class RingGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + radius = math.sqrt((pos.y - 0.5) ** 2 + (pos.x - 0.5) ** 2) + return { + "radius_1": radius, + } + + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.RingGraphic: + graphic = Graphics.RingGraphic() + graphic.radius_1 = graphic_properties.get("radius_1", 0.15) + graphic.radius_2 = graphic_properties.get("radius_2", 0.25) + return graphic + + +class LatticeGraphicFactory(GraphicFactoryBase): + def get_graphic_properties_from_position(self, display_item: DisplayItem.DisplayItem, pos: Geometry.FloatPoint) -> typing.Mapping[str, typing.Any]: + u_pos = pos - self._get_calibrated_origin_image_norm(display_item) + v_pos = Geometry.FloatPoint(-u_pos.x, -u_pos.y) # rotated 90 + return { + "u_pos": u_pos.as_tuple(), + "v_pos": v_pos.as_tuple(), + "radius": 0.1 + } + + def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.LatticeGraphic: + graphic = Graphics.LatticeGraphic() + graphic.u_pos = graphic_properties.get("u_pos", (0.0, 0.25)) + graphic.v_pos = graphic_properties.get("v_pos", (-0.25, 0.0)) + return graphic + + +graphic_factory_table: typing.Mapping[str, GraphicFactoryBase] = { + "line-graphic": LineGraphicFactory(), + "rectangle-graphic": RectangleGraphicFactory(), + "ellipse-graphic": EllipseGraphicFactory(), + "point-graphic": PointGraphicFactory(), + "line-profile-graphic": LineProfileGraphicFactory(), + "spot-graphic": SpotGraphicFactory(), + "wedge-graphic": WedgeGraphicFactory(), + "ring-graphic": RingGraphicFactory(), + "lattice-graphic": LatticeGraphicFactory(), +} + + class DeleteItemAction(Window.Action): action_id = "item.delete" action_name = _("Delete Item") @@ -3834,51 +4012,54 @@ class RasterDisplayCreateGraphicAction(Window.Action): action_id = "raster_display.add_graphic" action_name = _("Create Graphic") - graphics_table = { - "line": (_("Line Graphic"), Graphics.LineGraphic), - "rectangle": (_("Rectangle Graphic"), Graphics.RectangleGraphic), - "ellipse": (_("Ellipse Graphic"), Graphics.EllipseGraphic), - "point": (_("Point Graphic"), Graphics.PointGraphic), - "line-profile": (_("Line Profile Graphic"), Graphics.LineProfileGraphic), - "spot": (_("Spot Graphic"), Graphics.SpotGraphic), - "wedge": (_("Wedge Graphic"), Graphics.WedgeGraphic), - "ring": (_("Ring Graphic"), Graphics.RingGraphic), - "lattice": (_("Lattice Graphic"), Graphics.LatticeGraphic), - } - def __init__(self) -> None: super().__init__() def execute(self, context: Window.ActionContext) -> Window.ActionResult: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + display_item = context.display_item + if context.display_panel and display_item: + graphic_type = context.parameters["graphic_type"] + graphic_properties = context.parameters.get("graphic_properties", dict[str, typing.Any]()) + graphic = graphic_factory_table[graphic_type].create_graphic_in_display_item(window, display_item, graphic_properties) + command = context.display_panel.create_insert_graphics_command([graphic]) + command.perform() + window.push_undo_command(command) + return Window.ActionResult(Window.ActionStatus.FINISHED) + + def invoke(self, context: Window.ActionContext) -> Window.ActionResult: context = typing.cast(DocumentController.ActionContext, context) window = typing.cast(DocumentController, context.window) if context.display_panel: graphic = getattr(context, "_graphic") setattr(context, "_graphic", None) - assert graphic # TODO: make this work using parameters + assert graphic command = getattr(context, "_undo_command") setattr(context, "_undo_command", None) - if not command: - command = context.display_panel.create_insert_graphics_command([graphic]) - for key, value in context.parameters.get("graphic_properties", dict[str, typing.Any]()).items(): - setattr(graphic, key, value) + assert command + # for key, value in context.parameters.get("graphic_properties", dict[str, typing.Any]()).items(): + # setattr(graphic, key, value) command.perform() window.push_undo_command(command) return Window.ActionResult(Window.ActionStatus.FINISHED) - # def invoke_prepare(self, context: ActionContext) -> None: - # context = typing.cast(DocumentController.ActionContext, context) - # if context.display_panel: - # setattr(context, "_undo_command", context.display_panel.create_insert_graphics_command()) - - # def cancel_prepare(self, context: ActionContext) -> None: - # command = getattr(context, "_undo_command") - # setattr(context, "_undo_command", None) - # if command: - # command.close() + def invoke_prepare(self, context: ActionContext) -> None: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + display_item = context.display_item + if context.display_panel and display_item: + graphic_type = context.parameters["graphic_type"] + graphic_properties = context.parameters.get("graphic_properties", dict[str, typing.Any]()) + graphic = graphic_factory_table[graphic_type].create_graphic_in_display_item(window, display_item, graphic_properties) + setattr(context, "_graphic", graphic) + setattr(context, "_undo_command", context.display_panel.create_insert_graphics_command([graphic])) - def get_action_name(self, context: ActionContext) -> str: - return _("Create") + " " + self.graphics_table[context.parameters["graphic_type"]][0] + def cancel_prepare(self, context: ActionContext) -> None: + command = getattr(context, "_undo_command") + setattr(context, "_undo_command", None) + if command: + command.close() def is_enabled(self, context: Window.ActionContext) -> bool: context = typing.cast(DocumentController.ActionContext, context) @@ -3961,14 +4142,24 @@ def __init__(self) -> None: super().__init__() def execute(self, context: Window.ActionContext) -> Window.ActionResult: + context = typing.cast(DocumentController.ActionContext, context) + window = typing.cast(DocumentController, context.window) + if context.display_panel: + image_position = context.parameters["image_position"] + command = context.display_panel.create_change_display_command() + context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) + command.perform() + window.push_undo_command(command) + return Window.ActionResult(Window.ActionStatus.FINISHED) + + def invoke(self, context: Window.ActionContext) -> Window.ActionResult: context = typing.cast(DocumentController.ActionContext, context) window = typing.cast(DocumentController, context.window) if context.display_panel: image_position = context.parameters["image_position"] command = getattr(context, "_undo_command") setattr(context, "_undo_command", None) - if not command: - command = context.display_panel.create_change_display_command() + assert command context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) command.perform() window.push_undo_command(command) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 0f58cde4e..10e792d25 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -536,16 +536,18 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP pos = widget_mapping.map_point_widget_to_image_norm(mouse_pos) start_drag_pos = mouse_pos - # create the graphic and assign a drag part - graphic = delegate.create_line(pos) - graphic_drag_part = "end" - - # prepare for undo. move this to the command architecture somehow? - command = delegate.create_insert_graphics_command([graphic]) + graphic_properties = { + "start": pos.as_tuple(), + "end": pos.as_tuple() + } - action_context = delegate.prepare_command_action("raster_display.add_graphic") + action_context = delegate.prepare_command_action("raster_display.add_graphic", graphic_type="line-graphic", graphic_properties=graphic_properties) assert action_context + # create the graphic and assign a drag part + graphic = getattr(action_context, "_graphic") + graphic_drag_part = "end" + delegate.add_index_to_selection(image_canvas_item.graphic_index(graphic)) # setup drag selection_indexes = image_canvas_item.graphic_selection.indexes @@ -601,11 +603,8 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP # if the user did something, perform the action. otherwise cancel it so the action can release resources ( # e.g., undo command). if graphic_drag_changed: - setattr(action_context, "_graphic", graphic) - setattr(action_context, "_undo_command", command) delegate.perform_command_action("raster_display.add_graphic", action_context) else: - command.close() delegate.cancel_command_action("raster_display.add_graphic", action_context) From 706cc7c55bb5d9656aa1f8d876332c89f99c1c20 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Tue, 14 Nov 2023 13:41:41 -0800 Subject: [PATCH 08/11] Continue more with line mouse handler. --- nion/swift/DisplayCanvasItem.py | 29 +++++- nion/swift/DisplayPanel.py | 82 ++++++++++++--- nion/swift/DocumentController.py | 71 ++----------- nion/swift/ImageCanvasItem.py | 167 +++++++++++++------------------ nion/swift/model/Graphics.py | 9 ++ 5 files changed, 181 insertions(+), 177 deletions(-) diff --git a/nion/swift/DisplayCanvasItem.py b/nion/swift/DisplayCanvasItem.py index dbb621faa..4569a4d39 100644 --- a/nion/swift/DisplayCanvasItem.py +++ b/nion/swift/DisplayCanvasItem.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import abc +import types import typing from nion.swift import Undo @@ -12,6 +15,27 @@ 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 @@ -32,9 +56,8 @@ 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 prepare_command_action(self, action_or_action_id: typing.Union[str, Window.Action], **kwargs: typing.Any) -> typing.Optional[Window.ActionContext]: ... - def perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: ... - def cancel_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext) -> None: ... + def create_change_display_properties_task(self) -> InteractiveTask: ... + def create_create_graphic_task(self, graphic_type: str, start_position: Geometry.FloatPoint) -> 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: ... diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index bb7b8ec9f..55aa8111b 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -1741,6 +1741,68 @@ def display_data_channel(self, value: typing.Optional[DisplayItem.DisplayDataCha self.__display_data_channel = value +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.""" @@ -2834,21 +2896,11 @@ def create_change_graphics_command(self) -> ChangeGraphicsCommand: def push_undo_command(self, command: Undo.UndoableCommand) -> None: self.__document_controller.push_undo_command(command) - # naming to avoid conflict without older perform_action method above - def perform_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext, **kwargs: typing.Any) -> None: - for key, value in iter(kwargs.items()): - action_context.parameters[key] = value - self.__document_controller.perform_action_in_context(action_or_action_id, action_context) - - def prepare_command_action(self, action_or_action_id: typing.Union[str, Window.Action], **kwargs: typing.Any) -> typing.Optional[DocumentController.DocumentController.ActionContext]: - action_context = self.__document_controller._get_action_context() - for key, value in iter(kwargs.items()): - action_context.parameters[key] = value - self.__document_controller.prepare_action_in_context(action_or_action_id, action_context) - return action_context - - def cancel_command_action(self, action_or_action_id: typing.Union[str, Window.Action], action_context: Window.ActionContext) -> None: - self.__document_controller.cancel_action_in_context(action_or_action_id, action_context) + def create_change_display_properties_task(self) -> DisplayCanvasItem.InteractiveTask: + return ChangeDisplayPropertiesInteractiveTask(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 diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index 4df4db1f1..7d7928cee 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -2775,7 +2775,7 @@ def get_graphic_properties_from_position(self, display_item: DisplayItem.Display def create_graphic(self, graphic_properties: typing.Mapping[str, typing.Any]) -> Graphics.Graphic: raise NotImplementedError() - def _get_calibrated_origin_image_norm(self, display_item :DisplayItem.DisplayItem) -> Geometry.FloatPoint: + def _get_calibrated_origin_image_norm(self, display_item: DisplayItem.DisplayItem) -> Geometry.FloatPoint: display_data_channel = display_item.display_data_channel assert display_data_channel display_values = display_data_channel.get_latest_computed_display_values() @@ -4028,39 +4028,6 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult: window.push_undo_command(command) return Window.ActionResult(Window.ActionStatus.FINISHED) - def invoke(self, context: Window.ActionContext) -> Window.ActionResult: - context = typing.cast(DocumentController.ActionContext, context) - window = typing.cast(DocumentController, context.window) - if context.display_panel: - graphic = getattr(context, "_graphic") - setattr(context, "_graphic", None) - assert graphic - command = getattr(context, "_undo_command") - setattr(context, "_undo_command", None) - assert command - # for key, value in context.parameters.get("graphic_properties", dict[str, typing.Any]()).items(): - # setattr(graphic, key, value) - command.perform() - window.push_undo_command(command) - return Window.ActionResult(Window.ActionStatus.FINISHED) - - def invoke_prepare(self, context: ActionContext) -> None: - context = typing.cast(DocumentController.ActionContext, context) - window = typing.cast(DocumentController, context.window) - display_item = context.display_item - if context.display_panel and display_item: - graphic_type = context.parameters["graphic_type"] - graphic_properties = context.parameters.get("graphic_properties", dict[str, typing.Any]()) - graphic = graphic_factory_table[graphic_type].create_graphic_in_display_item(window, display_item, graphic_properties) - setattr(context, "_graphic", graphic) - setattr(context, "_undo_command", context.display_panel.create_insert_graphics_command([graphic])) - - def cancel_prepare(self, context: ActionContext) -> None: - command = getattr(context, "_undo_command") - setattr(context, "_undo_command", None) - if command: - command.close() - def is_enabled(self, context: Window.ActionContext) -> bool: context = typing.cast(DocumentController.ActionContext, context) return context.display_item is not None and context.display_item.used_display_type == "image" @@ -4134,9 +4101,9 @@ def is_enabled(self, context: Window.ActionContext) -> bool: return context.display_item is not None and context.display_item.used_display_type == "image" -class RasterDisplaySetImagePositionAction(Window.Action): - action_id = "raster_display.set_image_position" - action_name = _("Set Image Position") +class RasterDisplaySetDisplayPropertiesAction(Window.Action): + action_id = "raster_display.set_display_properties" + action_name = _("Set Display Properties") def __init__(self) -> None: super().__init__() @@ -4145,37 +4112,13 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult: context = typing.cast(DocumentController.ActionContext, context) window = typing.cast(DocumentController, context.window) if context.display_panel: - image_position = context.parameters["image_position"] + display_properties = context.parameters["display_properties"] command = context.display_panel.create_change_display_command() - context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) + context.display_panel.update_display_properties(display_properties) command.perform() window.push_undo_command(command) return Window.ActionResult(Window.ActionStatus.FINISHED) - def invoke(self, context: Window.ActionContext) -> Window.ActionResult: - context = typing.cast(DocumentController.ActionContext, context) - window = typing.cast(DocumentController, context.window) - if context.display_panel: - image_position = context.parameters["image_position"] - command = getattr(context, "_undo_command") - setattr(context, "_undo_command", None) - assert command - context.display_panel.update_display_properties({"image_position": image_position, "image_canvas_mode": "custom"}) - command.perform() - window.push_undo_command(command) - return Window.ActionResult(Window.ActionStatus.FINISHED) - - def invoke_prepare(self, context: ActionContext) -> None: - context = typing.cast(DocumentController.ActionContext, context) - if context.display_panel: - setattr(context, "_undo_command", context.display_panel.create_change_display_command()) - - def cancel_prepare(self, context: ActionContext) -> None: - command = getattr(context, "_undo_command") - setattr(context, "_undo_command", None) - if command: - command.close() - def is_enabled(self, context: Window.ActionContext) -> bool: context = typing.cast(DocumentController.ActionContext, context) return context.display_item is not None and context.display_item.used_display_type == "image" @@ -4319,7 +4262,7 @@ def is_enabled(self, context: Window.ActionContext) -> bool: Window.register_action(RasterDisplayFillViewAction()) Window.register_action(RasterDisplayOneViewAction()) Window.register_action(RasterDisplayTwoViewAction()) -Window.register_action(RasterDisplaySetImagePositionAction()) +Window.register_action(RasterDisplaySetDisplayPropertiesAction()) Window.register_action(RasterDisplayZoomOutAction()) Window.register_action(RasterDisplayZoomInAction()) Window.register_action(RasterDisplayAutoDisplayAction()) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 10e792d25..4cdb5c97e 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -475,8 +475,6 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP delegate = image_canvas_item.delegate assert delegate - delegate.begin_mouse_tracking() - # get the beginning mouse position value_change = await r.next_value_change() value_change_value = value_change.value @@ -489,29 +487,22 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP mouse_pos, modifiers = value_change_value last_drag_pos = mouse_pos - action_context = delegate.prepare_command_action("raster_display.set_image_position") - assert action_context - - # mouse tracking loop. wait for values and update the image position. - while True: - value_change = await r.next_value_change() - if value_change.is_end: - break - if value_change.value is not None: - mouse_pos, modifiers = value_change.value - assert last_drag_pos - delta = mouse_pos - last_drag_pos - image_position = image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) - last_drag_pos = mouse_pos - - delegate.end_mouse_tracking(None) - - # if the image position was set, it means the user moved the image. perform the action. otherwise - # cancel it so the action can release resources (e.g., undo command). - if image_position: - delegate.perform_command_action("raster_display.set_image_position", action_context, image_position=image_position) - else: - delegate.cancel_command_action("raster_display.set_image_position", action_context) + with delegate.create_change_display_properties_task() as change_display_properties_task: + # mouse tracking loop. wait for values and update the image position. + while True: + value_change = await r.next_value_change() + if value_change.is_end: + break + if value_change.value is not None: + mouse_pos, modifiers = value_change.value + assert last_drag_pos + delta = mouse_pos - last_drag_pos + image_position = image_canvas_item._update_image_canvas_position(-delta.as_size().to_float_size()) + last_drag_pos = mouse_pos + + # if the image position was set, it means the user moved the image. perform the task. + if image_position: + change_display_properties_task.commit() class CreateLineGraphicMouseHandler(MouseHandler): @@ -520,8 +511,6 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP delegate = image_canvas_item.delegate assert delegate - delegate.begin_mouse_tracking() - # get the beginning mouse position value_change = await r.next_value_change() value_change_value = value_change.value @@ -536,76 +525,64 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP pos = widget_mapping.map_point_widget_to_image_norm(mouse_pos) start_drag_pos = mouse_pos - graphic_properties = { - "start": pos.as_tuple(), - "end": pos.as_tuple() - } - - action_context = delegate.prepare_command_action("raster_display.add_graphic", graphic_type="line-graphic", graphic_properties=graphic_properties) - assert action_context - - # create the graphic and assign a drag part - graphic = getattr(action_context, "_graphic") - graphic_drag_part = "end" - - delegate.add_index_to_selection(image_canvas_item.graphic_index(graphic)) - # setup drag - selection_indexes = image_canvas_item.graphic_selection.indexes - assert len(selection_indexes) == 1 - graphic_drag_item_was_selected = True - # keep track of general drag information - graphic_drag_start_pos = start_drag_pos - graphic_drag_changed = False - # keep track of info for the specific item that was clicked - # keep track of drag information for each item in the set - graphic_drag_indexes = selection_indexes - graphic_drag_items: typing.List[Graphics.Graphic] = list() - graphic_drag_items.append(graphic) - graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() - graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - - # mouse tracking loop. wait for values and update the graphics. - while True: - value_change = await r.next_value_change() - if value_change.is_end: - break - if value_change.value is not None: - mouse_pos_, modifiers = value_change.value - mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) - force_drag = modifiers.only_option - if force_drag and graphic_drag_part == "all": - if Geometry.distance(mouse_pos, graphic_drag_start_pos) <= 2: - delegate.drag_graphics(graphic_drag_items) - continue - delegate.adjust_graphics(widget_mapping, graphic_drag_items, graphic_drag_part, graphic_part_data, graphic_drag_start_pos, mouse_pos, modifiers) - graphic_drag_changed = True - - graphics = list(image_canvas_item.graphics) - for index in graphic_drag_indexes: - graphic_ = graphics[index] - graphic_.end_drag(graphic_part_data[index]) - if graphic_drag_items and not graphic_drag_changed: - graphic_index = graphics.index(graphic) - # user didn't move graphic - if not modifiers.control: - # user clicked on a single graphic - delegate.set_selection(graphic_index) - else: - # user control clicked. toggle selection - # if control is down and item is already selected, toggle selection of item - if graphic_drag_item_was_selected: - delegate.remove_index_from_selection(graphic_index) + with delegate.create_create_graphic_task("line-graphic", pos) as create_create_graphic_task: + # create the graphic and assign a drag part + graphic = getattr(create_create_graphic_task, "_graphic") + graphic_drag_part = graphic._default_drag_part + + delegate.add_index_to_selection(image_canvas_item.graphic_index(graphic)) + # setup drag + selection_indexes = image_canvas_item.graphic_selection.indexes + assert len(selection_indexes) == 1 + graphic_drag_item_was_selected = True + # keep track of general drag information + graphic_drag_start_pos = start_drag_pos + graphic_drag_changed = False + # keep track of info for the specific item that was clicked + # keep track of drag information for each item in the set + graphic_drag_indexes = selection_indexes + graphic_drag_items: typing.List[Graphics.Graphic] = list() + graphic_drag_items.append(graphic) + graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() + graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() + + # mouse tracking loop. wait for values and update the graphics. + while True: + value_change = await r.next_value_change() + if value_change.is_end: + break + if value_change.value is not None: + mouse_pos_, modifiers = value_change.value + mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) + force_drag = modifiers.only_option + if force_drag and graphic_drag_part == "all": + if Geometry.distance(mouse_pos, graphic_drag_start_pos) <= 2: + delegate.drag_graphics(graphic_drag_items) + continue + delegate.adjust_graphics(widget_mapping, graphic_drag_items, graphic_drag_part, graphic_part_data, graphic_drag_start_pos, mouse_pos, modifiers) + graphic_drag_changed = True + + graphics = list(image_canvas_item.graphics) + for index in graphic_drag_indexes: + graphic_ = graphics[index] + graphic_.end_drag(graphic_part_data[index]) + if graphic_drag_items and not graphic_drag_changed: + graphic_index = graphics.index(graphic) + # user didn't move graphic + if not modifiers.control: + # user clicked on a single graphic + delegate.set_selection(graphic_index) else: - delegate.add_index_to_selection(graphic_index) - - delegate.end_mouse_tracking(None) + # user control clicked. toggle selection + # if control is down and item is already selected, toggle selection of item + if graphic_drag_item_was_selected: + delegate.remove_index_from_selection(graphic_index) + else: + delegate.add_index_to_selection(graphic_index) - # if the user did something, perform the action. otherwise cancel it so the action can release resources ( - # e.g., undo command). - if graphic_drag_changed: - delegate.perform_command_action("raster_display.add_graphic", action_context) - else: - delegate.cancel_command_action("raster_display.add_graphic", action_context) + # if graphic_drag_changed, it means the user moved the image. perform the task. + if graphic_drag_changed: + create_create_graphic_task.commit() class ImageCanvasItem(DisplayCanvasItem.DisplayCanvasItem): diff --git a/nion/swift/model/Graphics.py b/nion/swift/model/Graphics.py index a103528c0..b006a2fd4 100755 --- a/nion/swift/model/Graphics.py +++ b/nion/swift/model/Graphics.py @@ -647,6 +647,7 @@ def __init__(self, type: str) -> None: self.label_font = "normal 11px serif" self.__source_reference = self.create_item_reference() self._default_stroke_color = "#F80" + self._default_drag_part = "all" @property def source_specifier(self) -> typing.Optional[Persistence._SpecifierType]: @@ -907,6 +908,7 @@ def __init__(self, type: str, title: typing.Optional[str]) -> None: self.title = title self.define_property("bounds", ((0.0, 0.0), (1.0, 1.0)), validate=self.__validate_bounds, changed=self.__bounds_changed, hidden=True) self.define_property("rotation", 0.0, changed=self._property_changed, hidden=True) + self._default_drag_part = "bottom-right" @property def bounds(self) -> Geometry.FloatRect: @@ -1244,6 +1246,7 @@ def write_vector(persistent_property: Persistence.PersistentProperty, properties self.define_property("vector", ((0.0, 0.0), (1.0, 1.0)), changed=self.__vector_changed, reader=read_vector, writer=write_vector, validate=lambda value: (tuple(value[0]), tuple(value[1])), hidden=True) self.define_property("start_arrow_enabled", False, changed=self._property_changed, validate=lambda value: bool(value), hidden=True) self.define_property("end_arrow_enabled", False, changed=self._property_changed, validate=lambda value: bool(value), hidden=True) + self._default_drag_part = "end" @property def vector(self) -> typing.Tuple[Geometry.FloatPoint, Geometry.FloatPoint]: @@ -1775,6 +1778,7 @@ def validate_interval(interval: typing.Any) -> typing.Tuple[float, float]: # interval is stored in image normalized coordinates self.define_property("interval", (0.0, 1.0), changed=self.__interval_changed, reader=read_interval, writer=write_interval, validate=validate_interval, hidden=True) + self._default_drag_part = "end" @property def interval(self) -> typing.Tuple[float, float]: @@ -1884,6 +1888,7 @@ def __init__(self) -> None: self.title = _("Channel") # channel is stored in image normalized coordinates self.define_property("position", 0.5, changed=self._property_changed, validate=lambda value: float(value), hidden=True) + self._default_drag_part = "position" @property def position(self) -> float: @@ -1944,6 +1949,7 @@ def __init__(self) -> None: self.title = _("Spot") self.define_property("bounds", ((0.0, 0.0), (1.0, 1.0)), validate=self.__validate_bounds, changed=self.__bounds_changed, hidden=True) self.define_property("rotation", 0.0, changed=self._property_changed, hidden=True) + self._default_drag_part = "bottom-right" @property def bounds(self) -> Geometry.FloatRect: @@ -2159,6 +2165,7 @@ def validate_angles(value: typing.Tuple[float, float]) -> typing.Tuple[float, fl self.__first_drag = True self.__inverted_drag = False self.define_property("angle_interval", (0.0, math.pi), validate=validate_angles, changed=self._property_changed, hidden=True) + self._default_drag_part = "start-angle" @property def angle_interval(self) -> typing.Tuple[float, float]: @@ -2392,6 +2399,7 @@ def validate_angles(value: float) -> float: self.define_property("radius_1", 0.2, validate=validate_angles, changed=self._property_changed, hidden=True) self.define_property("radius_2", 0.2, validate=validate_angles, changed=self._property_changed, hidden=True) self.define_property("mode", "band-pass", changed=self._property_changed, hidden=True) + self._default_drag_part = "radius_1" @property def radius_1(self) -> float: @@ -2609,6 +2617,7 @@ def __init__(self) -> None: self.define_property("u_pos", (0.0, 0.25), validate=lambda value: tuple(value), changed=self._property_changed, hidden=True) self.define_property("v_pos", (-0.25, 0.0), validate=lambda value: tuple(value), changed=self._property_changed, hidden=True) self.define_property("radius", 0.1, changed=self._property_changed, hidden=True) + self._default_drag_part = "u-all" @property def u_pos(self) -> Geometry.FloatSize: From ebccee29a09020c911cf8683e667af9fc916c86b Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Tue, 14 Nov 2023 15:07:25 -0800 Subject: [PATCH 09/11] Use mouse handlers for all graphics in raster image display. --- nion/swift/ImageCanvasItem.py | 195 +++--------------------- nion/swift/test/DisplayPanel_test.py | 37 +++++ nion/swift/test/ImageCanvasItem_test.py | 2 + 3 files changed, 57 insertions(+), 177 deletions(-) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 4cdb5c97e..ffeea6d7f 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -505,7 +505,10 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP change_display_properties_task.commit() -class CreateLineGraphicMouseHandler(MouseHandler): +class CreateGraphicMouseHandler(MouseHandler): + def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop, graphic_type: str) -> None: + super().__init__(image_canvas_item, event_loop) + self.graphic_type = graphic_type async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: delegate = image_canvas_item.delegate @@ -525,7 +528,7 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP pos = widget_mapping.map_point_widget_to_image_norm(mouse_pos) start_drag_pos = mouse_pos - with delegate.create_create_graphic_task("line-graphic", pos) as create_create_graphic_task: + with delegate.create_create_graphic_task(self.graphic_type, pos) as create_create_graphic_task: # create the graphic and assign a drag part graphic = getattr(create_create_graphic_task, "_graphic") graphic_drag_part = graphic._default_drag_part @@ -932,10 +935,21 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie assert self.__event_loop self.__mouse_handler = HandMouseHandler(self, self.__event_loop) self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) - if delegate.tool_mode == "line": + graphic_type_map = { + "line": "line-graphic", + "rectangle": "rectangle-graphic", + "ellipse": "ellipse-graphic", + "point": "point-graphic", + "line-profile": "line-profile-graphic", + "spot": "spot-graphic", + "wedge": "wedge-graphic", + "ring": "ring-graphic", + "lattice": "lattice-graphic", + } + if delegate.tool_mode in graphic_type_map.keys(): assert not self.__mouse_handler assert self.__event_loop - self.__mouse_handler = CreateLineGraphicMouseHandler(self, self.__event_loop) + self.__mouse_handler = CreateGraphicMouseHandler(self, self.__event_loop, graphic_type_map[delegate.tool_mode]) self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) else: delegate.begin_mouse_tracking() @@ -1003,179 +1017,6 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie self.__graphic_part_data[index] = graphic.begin_drag() if not self.__graphic_drag_items and not modifiers.control: delegate.clear_selection() - elif delegate.tool_mode == "line__": - graphic = delegate.create_line(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "end" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "rectangle": - graphic = delegate.create_rectangle(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "bottom-right" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "ellipse": - graphic = delegate.create_ellipse(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "bottom-right" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "point": - graphic = delegate.create_point(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "all" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "line-profile": - graphic = delegate.create_line_profile(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "end" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "spot": - graphic = delegate.create_spot(pos) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "bottom-right" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "wedge": - mouse_angle = math.pi - math.atan2(0.5 - pos[0], 0.5 - pos[1]) - graphic = delegate.create_wedge(mouse_angle) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "start-angle" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "ring": - radius = math.sqrt((pos[0] - 0.5) ** 2 + (pos[1] - 0.5) ** 2) - graphic = delegate.create_ring(radius) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "radius_1" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) - elif delegate.tool_mode == "lattice": - graphic = delegate.create_lattice(pos.as_size()) - delegate.add_index_to_selection(self.__graphics.index(graphic)) - if graphic: - # setup drag - selection_indexes = self.__graphic_selection.indexes - assert len(selection_indexes) == 1 - self.graphic_drag_item_was_selected = True - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphic - self.__graphic_drag_part = "u-all" - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[list(selection_indexes)[0]] = graphic.begin_drag() - self.__undo_command = delegate.create_insert_graphics_command([graphic]) return True diff --git a/nion/swift/test/DisplayPanel_test.py b/nion/swift/test/DisplayPanel_test.py index 6ca826c8c..f034daca9 100644 --- a/nion/swift/test/DisplayPanel_test.py +++ b/nion/swift/test/DisplayPanel_test.py @@ -225,17 +225,20 @@ def test_drag_multiple(self): self.assertEqual(len(self.display_item.graphic_selection.indexes), 2) # drag by (0.1, 0.2) self.display_panel.display_canvas_item.simulate_drag((500,500), (600,700)) + self.document_controller.periodic() self.assertCloseRectangle(self.display_item.graphics[1].bounds, ((0.35, 0.45), (0.5, 0.5))) self.assertClosePoint(self.display_item.graphics[0].start, (0.3, 0.4)) self.assertClosePoint(self.display_item.graphics[0].end, (0.9, 1.0)) # drag on endpoint (0.3, 0.4) make sure it drags all self.display_panel.display_canvas_item.simulate_drag((300,400), (200,200)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.2, 0.2)) self.assertClosePoint(self.display_item.graphics[0].end, (0.8, 0.8)) self.assertCloseRectangle(self.display_item.graphics[1].bounds, ((0.25, 0.25), (0.5, 0.5))) # now select just the line, drag middle of circle. should only drag circle. self.display_item.graphic_selection.set(0) self.display_panel.display_canvas_item.simulate_drag((700,500), (800,500)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.2, 0.2)) self.assertClosePoint(self.display_item.graphics[0].end, (0.8, 0.8)) self.assertCloseRectangle(self.display_item.graphics[1].bounds, ((0.35, 0.25), (0.5, 0.5))) @@ -249,54 +252,74 @@ def test_drag_line_part(self): # select it self.display_item.graphic_selection.set(0) self.display_panel.display_canvas_item.simulate_drag((200,200), (300,400)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.3, 0.4)) self.assertClosePoint(self.display_item.graphics[0].end, (0.8, 0.8)) # shift drag a part, should not deselect and should align horizontally self.display_panel.display_canvas_item.simulate_drag((300,400), (350,700), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertClosePoint(self.display_item.graphics[0].start, (0.35, 0.8)) # shift drag start to top left quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((350,800), (370,340), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.34, 0.34)) self.display_panel.display_canvas_item.simulate_drag((340,340), (240,270), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.24, 0.24)) # shift drag start to bottom left quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((240,240), (370,1140), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.37, 1.23)) self.display_panel.display_canvas_item.simulate_drag((370,1230), (370,1350), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (0.25, 1.35)) # shift drag start to bottom right quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((250,1350), (1230,1175), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (1.23, 1.23)) self.display_panel.display_canvas_item.simulate_drag((1230,1230), (1150,1210), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (1.21, 1.21)) # shift drag start to top right quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((1210,1210), (1230,310), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (1.29, 0.31)) self.display_panel.display_canvas_item.simulate_drag((1290,310), (1110,420), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].start, (1.18, 0.42)) # now reverse start/end and run the same test self.display_panel.display_canvas_item.simulate_drag((800,800), (200,200)) + self.document_controller.periodic() self.display_panel.display_canvas_item.simulate_drag((1180,420), (800,800)) + self.document_controller.periodic() # shift drag start to top left quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((200,200), (370,340), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (0.34, 0.34)) self.display_panel.display_canvas_item.simulate_drag((340,340), (240,270), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (0.24, 0.24)) # shift drag start to bottom left quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((240,240), (370,1140), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (0.37, 1.23)) self.display_panel.display_canvas_item.simulate_drag((370,1230), (370,1350), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (0.25, 1.35)) # shift drag start to bottom right quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((250,1350), (1230,1175), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (1.23, 1.23)) self.display_panel.display_canvas_item.simulate_drag((1230,1230), (1150,1210), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (1.21, 1.21)) # shift drag start to top right quadrant. check both y-maj and x-maj. self.display_panel.display_canvas_item.simulate_drag((1210,1210), (1230,310), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (1.29, 0.31)) self.display_panel.display_canvas_item.simulate_drag((1290,310), (1110,420), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].end, (1.18, 0.42)) def test_nudge_line(self): @@ -385,6 +408,7 @@ def test_drag_point_moves_the_point_graphic(self): # select it self.display_item.graphic_selection.set(0) self.display_panel.display_canvas_item.simulate_drag((500,500), (300,400)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].position, (0.3, 0.4)) def test_click_on_point_selects_it(self): @@ -448,10 +472,12 @@ def test_resize_rectangle(self): self.display_item.graphic_selection.set(0) # drag top left corner self.display_panel.display_canvas_item.simulate_drag((250,250), (300,250)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].bounds[0], (0.30, 0.25)) self.assertClosePoint(self.display_item.graphics[0].bounds[1], (0.45, 0.5)) # drag with shift key self.display_panel.display_canvas_item.simulate_drag((300,250), (350,250), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_item.graphics[0].bounds[0], (0.25, 0.25)) self.assertClosePoint(self.display_item.graphics[0].bounds[1], (0.5, 0.5)) @@ -472,10 +498,12 @@ def test_resize_nonsquare_rectangle(self): self.display_item.graphic_selection.set(0) # drag top left corner self.display_panel.display_canvas_item.simulate_drag((500,250), (800,250)) + self.document_controller.periodic() self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.origin), (8, 2.5)) self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.size.as_point()), (7, 5)) # drag with shift key self.display_panel.display_canvas_item.simulate_drag((800,250), (900,250), CanvasItem.KeyboardModifiers(shift=True)) + self.document_controller.periodic() self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.origin), (9, 1.5)) self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.size.as_point()), (6, 6)) @@ -496,10 +524,12 @@ def test_resize_nonsquare_ellipse(self): self.display_item.graphic_selection.set(0) # drag top left corner self.display_panel.display_canvas_item.simulate_drag((500,250), (800,250), CanvasItem.KeyboardModifiers(alt=False)) + self.document_controller.periodic() self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.origin), (8, 2.5)) self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.size.as_point()), (4, 5)) # drag with shift key self.display_panel.display_canvas_item.simulate_drag((800,250), (900,250), CanvasItem.KeyboardModifiers(shift=True, alt=False)) + self.document_controller.periodic() self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.origin), (9, 4)) self.assertClosePoint(self.display_panel.display_canvas_item.map_image_norm_to_image(self.display_item.graphics[0].bounds.size.as_point()), (2, 2)) @@ -1156,6 +1186,7 @@ def test_perform_action_gets_dispatched_to_image_canvas_item(self): def test_dragging_to_add_point_makes_desired_point(self): self.document_controller.tool_mode = "point" self.display_panel.display_canvas_item.simulate_drag((100,125), (200,250)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphics), 1) region = self.display_item.graphics[0] self.assertEqual(region.type, "point-graphic") @@ -1165,6 +1196,7 @@ def test_dragging_to_add_point_makes_desired_point(self): def test_dragging_to_add_rectangle_makes_desired_rectangle(self): self.document_controller.tool_mode = "rectangle" self.display_panel.display_canvas_item.simulate_drag((100,125), (250,200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphics), 1) region = self.display_item.graphics[0] self.assertEqual(region.type, "rect-graphic") @@ -1176,6 +1208,7 @@ def test_dragging_to_add_rectangle_makes_desired_rectangle(self): def test_dragging_to_add_ellipse_makes_desired_ellipse(self): self.document_controller.tool_mode = "ellipse" self.display_panel.display_canvas_item.simulate_drag((100,125), (250,200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphics), 1) region = self.display_item.graphics[0] self.assertEqual(region.type, "ellipse-graphic") @@ -1230,6 +1263,7 @@ def test_dragging_to_add_line_profile_makes_desired_line_profile(self): root_canvas_item.update_layout(Geometry.IntPoint(), Geometry.IntSize(width=1000, height=1000 + header_height), immediate=True) # drag for the line profile display_panel.display_canvas_item.simulate_drag((100,125), (200,250)) + self.document_controller.periodic() # check results self.assertEqual(len(display_item.graphics), 1) region = display_item.graphics[0] @@ -1259,6 +1293,7 @@ def test_dragging_to_add_line_profile_puts_source_and_destination_under_transact modifiers = CanvasItem.KeyboardModifiers() display_panel.display_canvas_item.mouse_pressed(100, 100, modifiers) display_panel.display_canvas_item.mouse_position_changed(125, 125, modifiers) + document_controller.periodic() self.assertTrue(data_item.in_transaction_state) self.assertTrue(display_item.in_transaction_state) line_plot_data_item = document_model.data_items[-1] @@ -1266,6 +1301,7 @@ def test_dragging_to_add_line_profile_puts_source_and_destination_under_transact self.assertTrue(line_plot_data_item.in_transaction_state) self.assertTrue(line_plot_display_item.in_transaction_state) display_panel.display_canvas_item.mouse_released(200, 200, modifiers) + document_controller.periodic() self.assertFalse(data_item.in_transaction_state) self.assertFalse(display_item.in_transaction_state) self.assertFalse(line_plot_data_item.in_transaction_state) @@ -1292,6 +1328,7 @@ def test_dragging_to_add_line_profile_works_when_line_profile_is_filtered_from_d document_controller.set_filter("none") document_controller.tool_mode = "line-profile" display_panel.display_canvas_item.simulate_drag((100,125), (200,250)) + self.document_controller.periodic() self.assertEqual(len(display_item.graphics), 1) region = display_item.graphics[0] self.assertEqual(region.type, "line-profile-graphic") diff --git a/nion/swift/test/ImageCanvasItem_test.py b/nion/swift/test/ImageCanvasItem_test.py index a24958ca3..62f462130 100644 --- a/nion/swift/test/ImageCanvasItem_test.py +++ b/nion/swift/test/ImageCanvasItem_test.py @@ -42,6 +42,7 @@ def test_mapping_widget_to_image_on_2d_data_stack_uses_signal_dimensions(self): # run test document_controller.tool_mode = "line-profile" display_panel.display_canvas_item.simulate_drag((20,25), (65,85)) + document_controller.periodic() self.assertEqual(display_item.graphics[0].vector, ((0.2, 0.25), (0.65, 0.85))) def test_mapping_widget_to_image_on_3d_spectrum_image_uses_collection_dimensions(self): @@ -60,6 +61,7 @@ def test_mapping_widget_to_image_on_3d_spectrum_image_uses_collection_dimensions # run test document_controller.tool_mode = "line-profile" display_panel.display_canvas_item.simulate_drag((20,25), (65,85)) + document_controller.periodic() self.assertEqual(display_item.graphics[0].vector, ((0.2, 0.25), (0.65, 0.85))) def test_dimension_used_for_scale_marker_on_2d_data_stack_is_correct(self): From 9d7533a08c428f46ba3f0f8fd054417ad932b7e0 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Tue, 14 Nov 2023 17:14:11 -0800 Subject: [PATCH 10/11] Use mouse handler for raster pointer tool. --- nion/swift/DisplayCanvasItem.py | 1 + nion/swift/DisplayPanel.py | 27 ++++ nion/swift/ImageCanvasItem.py | 188 +++++++++++++++++++++--- nion/swift/model/Graphics.py | 1 + nion/swift/test/DisplayPanel_test.py | 25 ++++ nion/swift/test/Graphics_test.py | 45 ++++-- nion/swift/test/ImageCanvasItem_test.py | 6 + 7 files changed, 258 insertions(+), 35 deletions(-) diff --git a/nion/swift/DisplayCanvasItem.py b/nion/swift/DisplayCanvasItem.py index 4569a4d39..ff7a7637c 100644 --- a/nion/swift/DisplayCanvasItem.py +++ b/nion/swift/DisplayCanvasItem.py @@ -58,6 +58,7 @@ def create_move_display_layer_command(self, display_item: DisplayItem.DisplayIte 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: ... diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index 55aa8111b..1a36c3226 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -1741,6 +1741,30 @@ 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__() @@ -2899,6 +2923,9 @@ def push_undo_command(self, command: Undo.UndoableCommand) -> None: 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) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index ffeea6d7f..2b4dac5cb 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -450,6 +450,7 @@ def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.Abstr self.__mouse_value_stream = Stream.ValueStream[MousePositionAndModifiers]() self.__mouse_value_change_stream = Stream.ValueChangeStream(self.__mouse_value_stream) self.__reactor = Stream.ValueChangeStreamReactor[MousePositionAndModifiers](self.__mouse_value_change_stream, self.reactor_loop, event_loop) + self.cursor_shape = "arrow" def mouse_pressed(self, mouse_pos: Geometry.IntPoint, modifiers: UserInterface.KeyboardModifiers) -> None: self.__mouse_value_stream.value = mouse_pos, modifiers @@ -469,7 +470,144 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP return +class PointerMouseHandler(MouseHandler): + def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop) -> None: + super().__init__(image_canvas_item, event_loop) + self.cursor_shape = "arrow" + + async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: + delegate = image_canvas_item.delegate + assert delegate + + # get the beginning mouse position + value_change = await r.next_value_change() + value_change_value = value_change.value + assert value_change.is_begin + assert value_change_value is not None + + # preliminary setup for the tracking loop. + mouse_pos_, modifiers = value_change_value + mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) + widget_mapping = image_canvas_item.mouse_mapping + assert widget_mapping + start_drag_pos = mouse_pos + + graphic_drag_items: typing.List[Graphics.Graphic] = list() + graphic_drag_item: typing.Optional[Graphics.Graphic] = None + graphic_drag_item_was_selected = False + graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() + graphic_drag_indexes = set() + + graphics = image_canvas_item.graphics + selection_indexes = image_canvas_item.graphic_selection.indexes + multiple_items_selected = len(selection_indexes) > 1 + part_specs: typing.List[typing.Tuple[int, Graphics.Graphic, bool, str]] = list() + part_spec: typing.Optional[typing.Tuple[int, Graphics.Graphic, bool, str]] + specific_part_spec: typing.Optional[typing.Tuple[int, Graphics.Graphic, bool, str]] = None + # the graphics are drawn in order, which means the graphics with the higher index are "on top" of the + # graphics with the lower index. but priority should also be given to selected graphics. so sort the + # graphics according to whether they are selected or not (selected ones go later), then by their index. + for graphic_index, graphic in sorted(enumerate(graphics), key=lambda ig: (ig[0] in selection_indexes, ig[0])): + if isinstance(graphic, (Graphics.PointTypeGraphic, Graphics.LineTypeGraphic, Graphics.RectangleTypeGraphic, Graphics.SpotGraphic, Graphics.WedgeGraphic, Graphics.RingGraphic, Graphics.LatticeGraphic)): + already_selected = graphic_index in selection_indexes + move_only = not already_selected or multiple_items_selected + try: + part, specific = graphic.test(widget_mapping, image_canvas_item.ui_settings, start_drag_pos, move_only) + except Exception as e: + import traceback + logging.debug("Graphic Test Error: %s", e) + traceback.print_exc() + traceback.print_stack() + continue + if part: + part_spec = graphic_index, graphic, already_selected, "all" if move_only and not part.startswith("inverted") else part + part_specs.append(part_spec) + if specific: + specific_part_spec = part_spec + part_spec = specific_part_spec if specific_part_spec is not None else part_specs[-1] if len(part_specs) > 0 else None + if part_spec is not None: + graphic_index, graphic, already_selected, part = part_spec + part = part if specific_part_spec is not None else part_spec[-1] + # select item and prepare for drag + graphic_drag_item_was_selected = already_selected + if not graphic_drag_item_was_selected: + if modifiers.control: + delegate.add_index_to_selection(graphic_index) + selection_indexes.add(graphic_index) + elif not already_selected: + delegate.set_selection(graphic_index) + selection_indexes.clear() + selection_indexes.add(graphic_index) + # keep track of general drag information + graphic_drag_start_pos = start_drag_pos + graphic_drag_changed = False + # keep track of info for the specific item that was clicked + graphic_drag_item = graphics[graphic_index] + graphic_drag_part = part + # keep track of drag information for each item in the set + graphic_drag_indexes = selection_indexes + for index in graphic_drag_indexes: + graphic = graphics[index] + graphic_drag_items.append(graphic) + graphic_part_data[index] = graphic.begin_drag() + if not graphic_drag_items and not modifiers.control: + delegate.clear_selection() + + def get_pointer_tool_shape(mouse_pos: Geometry.FloatPoint) -> str: + for graphic in graphics: + if isinstance(graphic, (Graphics.RectangleTypeGraphic, Graphics.SpotGraphic)): + part, specific = graphic.test(image_canvas_item.mouse_mapping, image_canvas_item.ui_settings, mouse_pos, False) + if part and part.endswith("rotate"): + return "cross" + return "arrow" + + with delegate.create_change_graphics_task() as change_graphics_task: + while True: + value_change = await r.next_value_change() + if value_change.is_end: + break + if value_change.value is not None: + mouse_pos_, modifiers = value_change.value + mouse_pos = Geometry.FloatPoint(x=mouse_pos_.x, y=mouse_pos_.y) + + if graphic_drag_items: + graphic_drag_changed = True + force_drag = modifiers.only_option + if force_drag and graphic_drag_part == "all": + if Geometry.distance(mouse_pos, graphic_drag_start_pos) <= 2: + delegate.drag_graphics(graphic_drag_items) + continue + delegate.adjust_graphics(widget_mapping, graphic_drag_items, graphic_drag_part, graphic_part_data, graphic_drag_start_pos, mouse_pos, modifiers) + + self.cursor_shape = get_pointer_tool_shape(mouse_pos) + + graphics = list(image_canvas_item.graphics) + for index in graphic_drag_indexes: + graphic_ = graphics[index] + graphic_.end_drag(graphic_part_data[index]) + if graphic_drag_items and not graphic_drag_changed: + graphic_index = graphics.index(graphic_drag_item) if graphic_drag_item else 0 + # user didn't move graphic + if not modifiers.control: + # user clicked on a single graphic + delegate.set_selection(graphic_index) + else: + # user control clicked. toggle selection + # if control is down and item is already selected, toggle selection of item + if graphic_drag_item_was_selected: + delegate.remove_index_from_selection(graphic_index) + else: + delegate.add_index_to_selection(graphic_index) + + # if graphic_drag_changed, it means the user moved the image. perform the task. + if graphic_drag_changed: + change_graphics_task.commit() + + class HandMouseHandler(MouseHandler): + def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop) -> None: + super().__init__(image_canvas_item, event_loop) + self.cursor_shape = "hand" async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: delegate = image_canvas_item.delegate @@ -569,6 +707,7 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP for index in graphic_drag_indexes: graphic_ = graphics[index] graphic_.end_drag(graphic_part_data[index]) + if graphic_drag_items and not graphic_drag_changed: graphic_index = graphics.index(graphic) # user didn't move graphic @@ -588,6 +727,20 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP create_create_graphic_task.commit() +# map the tool mode to the graphic type +graphic_type_map = { + "line": "line-graphic", + "rectangle": "rectangle-graphic", + "ellipse": "ellipse-graphic", + "point": "point-graphic", + "line-profile": "line-profile-graphic", + "spot": "spot-graphic", + "wedge": "wedge-graphic", + "ring": "ring-graphic", + "lattice": "lattice-graphic", +} + + class ImageCanvasItem(DisplayCanvasItem.DisplayCanvasItem): """A canvas item to paint an image. @@ -870,6 +1023,10 @@ def graphic_index(self, graphic: Graphics.Graphic) -> int: def graphic_selection(self) -> DisplayItem.GraphicSelection: return self.__graphic_selection + @property + def ui_settings(self) -> UISettings.UISettings: + return self.__ui_settings + def _set_image_canvas_position(self, image_position: Geometry.FloatPoint) -> None: # create a widget mapping to get from image norm to widget coordinates and back delegate = self.delegate @@ -930,23 +1087,17 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie if delegate.image_mouse_pressed(image_position, modifiers): return True self.__undo_command = None - if delegate.tool_mode == "hand": + if delegate.tool_mode == "pointer": + assert not self.__mouse_handler + assert self.__event_loop + self.__mouse_handler = PointerMouseHandler(self, self.__event_loop) + self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) + elif delegate.tool_mode == "hand": assert not self.__mouse_handler assert self.__event_loop self.__mouse_handler = HandMouseHandler(self, self.__event_loop) self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) - graphic_type_map = { - "line": "line-graphic", - "rectangle": "rectangle-graphic", - "ellipse": "ellipse-graphic", - "point": "point-graphic", - "line-profile": "line-profile-graphic", - "spot": "spot-graphic", - "wedge": "wedge-graphic", - "ring": "ring-graphic", - "lattice": "lattice-graphic", - } - if delegate.tool_mode in graphic_type_map.keys(): + elif delegate.tool_mode in graphic_type_map.keys(): assert not self.__mouse_handler assert self.__event_loop self.__mouse_handler = CreateGraphicMouseHandler(self, self.__event_loop, graphic_type_map[delegate.tool_mode]) @@ -959,7 +1110,7 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie self.graphic_drag_item_was_selected = False self.__graphic_part_data = dict() self.__graphic_drag_indexes = set() - if delegate.tool_mode == "pointer": + if delegate.tool_mode == "pointer__": graphics = self.__graphics selection_indexes = self.__graphic_selection.indexes multiple_items_selected = len(selection_indexes) > 1 @@ -1090,14 +1241,7 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa if delegate.image_mouse_position_changed(image_position, modifiers): return True if delegate.tool_mode == "pointer": - def get_pointer_tool_shape() -> str: - for graphic in self.__graphics: - if isinstance(graphic, (Graphics.RectangleTypeGraphic, Graphics.SpotGraphic)): - part, specific = graphic.test(self.__get_mouse_mapping(), self.__ui_settings, Geometry.FloatPoint(x=x, y=y), False) - if part and part.endswith("rotate"): - return "cross" - return "arrow" - self.cursor_shape = get_pointer_tool_shape() + self.cursor_shape = self.__mouse_handler.cursor_shape if self.__mouse_handler else "arrow" elif delegate.tool_mode == "line": self.cursor_shape = "cross" elif delegate.tool_mode == "rectangle": diff --git a/nion/swift/model/Graphics.py b/nion/swift/model/Graphics.py index b006a2fd4..4bfb8abba 100755 --- a/nion/swift/model/Graphics.py +++ b/nion/swift/model/Graphics.py @@ -1641,6 +1641,7 @@ def position(self) -> Geometry.FloatPoint: @position.setter def position(self, value: Geometry.FloatPointTuple) -> None: + assert value is not None self._set_persistent_property_value("position", tuple(value)) def reset_position(self) -> None: diff --git a/nion/swift/test/DisplayPanel_test.py b/nion/swift/test/DisplayPanel_test.py index f034daca9..ed23838af 100644 --- a/nion/swift/test/DisplayPanel_test.py +++ b/nion/swift/test/DisplayPanel_test.py @@ -165,19 +165,24 @@ def test_select_line(self): self.document_controller.add_line_graphic() # click outside so nothing is selected self.display_panel.display_canvas_item.simulate_click((0, 0)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 0) # select the line self.display_panel.display_canvas_item.simulate_click((200, 200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(0 in self.display_item.graphic_selection.indexes) # now shift the view and try again self.display_panel.display_canvas_item.simulate_click((0, 0)) + self.document_controller.periodic() self.display_panel.display_canvas_item.move_left() # 10 pixels left self.display_panel.display_canvas_item.move_left() # 10 pixels left self.display_panel.display_canvas_item.refresh_layout_immediate() self.display_panel.display_canvas_item.simulate_click((200, 200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 0) self.display_panel.display_canvas_item.simulate_click((220, 200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(0 in self.display_item.graphic_selection.indexes) @@ -187,21 +192,26 @@ def test_select_multiple(self): self.document_controller.add_ellipse_graphic() # click outside so nothing is selected self.display_panel.display_canvas_item.simulate_click((0, 0)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 0) # select the ellipse self.display_panel.display_canvas_item.simulate_click((725, 500)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(1 in self.display_item.graphic_selection.indexes) # select the line self.display_panel.display_canvas_item.simulate_click((200, 200)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(0 in self.display_item.graphic_selection.indexes) # add the ellipse to the selection. click inside the right side. self.display_panel.display_canvas_item.simulate_click((725, 500), CanvasItem.KeyboardModifiers(control=True)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 2) self.assertTrue(0 in self.display_item.graphic_selection.indexes) # remove the ellipse from the selection. click inside the right side. self.display_panel.display_canvas_item.simulate_click((725, 500), CanvasItem.KeyboardModifiers(control=True)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(0 in self.display_item.graphic_selection.indexes) @@ -418,8 +428,10 @@ def test_click_on_point_selects_it(self): self.assertClosePoint(self.display_item.graphics[0].position, (0.5, 0.5)) # select it self.display_panel.display_canvas_item.simulate_click((100,100)) + self.document_controller.periodic() self.assertFalse(self.display_item.graphic_selection.indexes) self.display_panel.display_canvas_item.simulate_click((500,500)) + self.document_controller.periodic() self.assertEqual(len(self.display_item.graphic_selection.indexes), 1) self.assertTrue(0 in self.display_item.graphic_selection.indexes) @@ -951,6 +963,7 @@ def test_combined_horizontal_drag_and_expand_works_nominally(self): line_plot_canvas_item.mouse_position_changed(plot_left+96, v, CanvasItem.KeyboardModifiers()) line_plot_canvas_item.mouse_position_changed(plot_left+196, v, CanvasItem.KeyboardModifiers()) line_plot_canvas_item.mouse_released(plot_left+116, 190, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() channel_per_pixel = 1024.0/10 / plot_width self.assertEqual(self.display_item.get_display_property("left_channel"), int(0 - channel_per_pixel * 100)) self.assertEqual(self.display_item.get_display_property("right_channel"), int(int(1024/10.0) - channel_per_pixel * 100)) @@ -971,6 +984,7 @@ def test_click_on_selection_makes_it_selected(self): # do the click line_plot_canvas_item.mouse_pressed(plot_left+plot_width * 0.35, 100, CanvasItem.KeyboardModifiers()) line_plot_canvas_item.mouse_released(plot_left+plot_width * 0.35, 100, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() # make sure results are correct self.assertEqual(len(line_plot_display_item.graphic_selection.indexes), 1) @@ -990,11 +1004,13 @@ def test_click_outside_selection_makes_it_unselected(self): # do the first click to select line_plot_canvas_item.mouse_pressed(plot_left+plot_width * 0.35, 100, CanvasItem.KeyboardModifiers()) line_plot_canvas_item.mouse_released(plot_left+plot_width * 0.35, 100, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() # make sure results are correct self.assertEqual(len(line_plot_display_item.graphic_selection.indexes), 1) # do the second click to deselect line_plot_canvas_item.mouse_pressed(plot_left+plot_width * 0.1, 100, CanvasItem.KeyboardModifiers()) line_plot_canvas_item.mouse_released(plot_left+plot_width * 0.1, 100, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() # make sure results are correct self.assertEqual(len(line_plot_display_item.graphic_selection.indexes), 0) @@ -1013,9 +1029,11 @@ def test_click_drag_interval_end_channel_to_right_adjust_end_channel(self): modifiers = CanvasItem.KeyboardModifiers() line_plot_canvas_item.mouse_pressed(plot_left + plot_width * 0.35, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.35, 100, modifiers) + self.document_controller.periodic() line_plot_canvas_item.mouse_pressed(plot_left + plot_width * 0.4, 100, modifiers) line_plot_canvas_item.mouse_position_changed(plot_left + plot_width * 0.5, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.5, 100, modifiers) + self.document_controller.periodic() # make sure results are correct line_plot_canvas_item.root_container.refresh_layout_immediate() self.assertAlmostEqual(line_plot_display_item.graphics[0].start, 0.3) @@ -1036,9 +1054,11 @@ def test_click_drag_interval_end_channel_to_left_of_start_channel_results_in_lef modifiers = CanvasItem.KeyboardModifiers() line_plot_canvas_item.mouse_pressed(plot_left + plot_width * 0.35, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.35, 100, modifiers) + self.document_controller.periodic() line_plot_canvas_item.mouse_pressed(plot_left + plot_width * 0.4, 100, modifiers) line_plot_canvas_item.mouse_position_changed(plot_left + plot_width * 0.2, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.2, 100, modifiers) + self.document_controller.periodic() # make sure results are correct self.assertAlmostEqual(line_plot_display_item.graphics[0].start, 0.2, 2) # pixel accuracy, approx. 1/500 self.assertAlmostEqual(line_plot_display_item.graphics[0].end, 0.3, 2) # pixel accuracy, approx. 1/500 @@ -1059,6 +1079,7 @@ def test_click_drag_interval_tool_creates_selection(self): line_plot_canvas_item.mouse_pressed(plot_left + plot_width * 0.4, 100, modifiers) line_plot_canvas_item.mouse_position_changed(plot_left + plot_width * 0.3, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.3, 100, modifiers) + self.document_controller.periodic() # make sure results are correct self.assertAlmostEqual(line_plot_display_item.graphics[0].start, 0.1, 2) # pixel accuracy, approx. 1/500 self.assertAlmostEqual(line_plot_display_item.graphics[0].end, 0.9, 2) # pixel accuracy, approx. 1/500 @@ -1079,6 +1100,7 @@ def test_click_drag_interval_tool_creates_selection(self): line_plot_canvas_item.mouse_position_changed(plot_left + plot_width * 0.40, 100, modifiers) line_plot_canvas_item.mouse_position_changed(plot_left + plot_width * 0.50, 100, modifiers) line_plot_canvas_item.mouse_released(plot_left + plot_width * 0.50, 100, modifiers) + self.document_controller.periodic() # make sure results are correct self.assertEqual(len(line_plot_display_item.graphics), 1) self.assertTrue(isinstance(line_plot_display_item.graphics[0], Graphics.IntervalGraphic)) @@ -1488,6 +1510,7 @@ def test_all_graphic_types_hit_test_on_1d_display(self): self.document_controller.add_interval_graphic() display_canvas_item.mouse_pressed(100, 100, CanvasItem.KeyboardModifiers()) display_canvas_item.mouse_released(100, 100, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() def test_all_graphic_types_hit_test_on_2d_display(self): self.document_controller.add_point_graphic() @@ -1498,6 +1521,7 @@ def test_all_graphic_types_hit_test_on_2d_display(self): self.display_panel.display_canvas_item.prepare_display() # force layout self.display_panel.display_canvas_item.mouse_pressed(10, 10, CanvasItem.KeyboardModifiers()) self.display_panel.display_canvas_item.mouse_released(10, 10, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() def test_all_graphic_types_hit_test_on_3d_display(self): display_canvas_item = self.setup_3d_data() @@ -1508,6 +1532,7 @@ def test_all_graphic_types_hit_test_on_3d_display(self): self.document_controller.add_interval_graphic() display_canvas_item.mouse_pressed(10, 10, CanvasItem.KeyboardModifiers()) display_canvas_item.mouse_released(10, 10, CanvasItem.KeyboardModifiers()) + self.document_controller.periodic() def test_display_graphics_update_after_changing_display_type(self): with TestContext.create_memory_context() as test_context: diff --git a/nion/swift/test/Graphics_test.py b/nion/swift/test/Graphics_test.py index 3fccc7ede..ec45646a2 100644 --- a/nion/swift/test/Graphics_test.py +++ b/nion/swift/test/Graphics_test.py @@ -159,14 +159,18 @@ def test_drag_spot_mask(self): origin = Geometry.FloatPoint(500, 500) # activate display panel display_panel.display_canvas_item.simulate_click(origin) + document_controller.periodic() # move primary spot display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(100, 250), origin + Geometry.FloatPoint(50, 200)) + document_controller.periodic() self.assertAlmostEqualRect(Geometry.FloatRect.from_center_and_size((0.05, 0.20), (0.1, 0.1)), spot_graphic.bounds) # move secondary spot display_panel.display_canvas_item.simulate_drag(origin - Geometry.FloatPoint(50, 200), origin - Geometry.FloatPoint(100, 250)) + document_controller.periodic() self.assertAlmostEqualRect(Geometry.FloatRect.from_center_and_size((0.10, 0.25), (0.1, 0.1)), spot_graphic.bounds) # move top-right display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(50, 300), origin + Geometry.FloatPoint(60, 310)) + document_controller.periodic() self.assertAlmostEqualRect(Geometry.FloatRect.from_center_and_size((0.10, 0.25), (0.08, 0.12)), spot_graphic.bounds) def test_drag_wedge_mask(self): @@ -189,22 +193,25 @@ def test_drag_wedge_mask(self): display_item.add_graphic(wedge_graphic) wedge_graphic.start_angle = math.radians(30) wedge_graphic.end_angle = math.radians(60) - origin = Geometry.FloatPoint(500, 500) + origin = Geometry.IntPoint(500, 500) # activate display panel display_panel.display_canvas_item.simulate_click(origin) # drag start angle - display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(250 * -math.sin(math.radians(30)), 250 * math.cos(math.radians(30))), - origin + Geometry.FloatPoint(250 * -math.sin(math.radians(20)), 250 * math.cos(math.radians(20)))) - self.assertAlmostEqual(20, math.degrees(wedge_graphic.start_angle)) - self.assertAlmostEqual(60, math.degrees(wedge_graphic.end_angle)) - display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(250 * -math.sin(math.radians(60)), 250 * math.cos(math.radians(60))), - origin + Geometry.FloatPoint(250 * -math.sin(math.radians(50)), 250 * math.cos(math.radians(50)))) - self.assertAlmostEqual(20, math.degrees(wedge_graphic.start_angle)) - self.assertAlmostEqual(50, math.degrees(wedge_graphic.end_angle)) - display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(250 * -math.sin(math.radians(35)), 250 * math.cos(math.radians(35))), - origin + Geometry.FloatPoint(250 * -math.sin(math.radians(45)), 250 * math.cos(math.radians(45)))) - self.assertAlmostEqual(30, math.degrees(wedge_graphic.start_angle)) - self.assertAlmostEqual(60, math.degrees(wedge_graphic.end_angle)) + display_panel.display_canvas_item.simulate_drag(origin + Geometry.IntPoint(250 * -math.sin(math.radians(30)), 250 * math.cos(math.radians(30))), + origin + Geometry.IntPoint(250 * -math.sin(math.radians(20)), 250 * math.cos(math.radians(20)))) + document_controller.periodic() + self.assertAlmostEqual(20, math.degrees(wedge_graphic.start_angle), delta=1.0) + self.assertAlmostEqual(60, math.degrees(wedge_graphic.end_angle), delta=1.0) + display_panel.display_canvas_item.simulate_drag(origin + Geometry.IntPoint(250 * -math.sin(math.radians(60)), 250 * math.cos(math.radians(60))), + origin + Geometry.IntPoint(250 * -math.sin(math.radians(50)), 250 * math.cos(math.radians(50)))) + document_controller.periodic() + self.assertAlmostEqual(20, math.degrees(wedge_graphic.start_angle), delta=1.0) + self.assertAlmostEqual(50, math.degrees(wedge_graphic.end_angle), delta=1.0) + display_panel.display_canvas_item.simulate_drag(origin + Geometry.IntPoint(250 * -math.sin(math.radians(35)), 250 * math.cos(math.radians(35))), + origin + Geometry.IntPoint(250 * -math.sin(math.radians(45)), 250 * math.cos(math.radians(45)))) + document_controller.periodic() + self.assertAlmostEqual(30, math.degrees(wedge_graphic.start_angle), delta=1.0) + self.assertAlmostEqual(60, math.degrees(wedge_graphic.end_angle), delta=1.0) def test_drag_ring_mask(self): with TestContext.create_memory_context() as test_context: @@ -229,12 +236,16 @@ def test_drag_ring_mask(self): origin = Geometry.FloatPoint(500, 500) # activate display panel display_panel.display_canvas_item.simulate_click(origin) + document_controller.periodic() display_panel.display_canvas_item.simulate_click(origin + Geometry.FloatPoint(250, 0)) + document_controller.periodic() # move rings display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(200, 0), origin + Geometry.FloatPoint(100, 0)) + document_controller.periodic() self.assertAlmostEqual(0.1, ring_graphic.radius_1) self.assertAlmostEqual(0.3, ring_graphic.radius_2) display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(300, 0), origin + Geometry.FloatPoint(350, 0)) + document_controller.periodic() self.assertAlmostEqual(0.1, ring_graphic.radius_1) self.assertAlmostEqual(0.35, ring_graphic.radius_2) @@ -262,16 +273,20 @@ def test_drag_ring_lattice(self): origin = Geometry.FloatPoint(500, 500) # activate display panel display_panel.display_canvas_item.simulate_click(origin) + document_controller.periodic() # move vectors display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(100, 300), origin + Geometry.FloatPoint(150, 250)) + document_controller.periodic() self.assertAlmostEqualPoint(Geometry.FloatPoint(0.15, 0.25), lattice_graphic.u_pos) self.assertAlmostEqualPoint(Geometry.FloatPoint(-0.20, -0.20), lattice_graphic.v_pos) self.assertAlmostEqual(0.05, lattice_graphic.radius) display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(-200, -200), origin + Geometry.FloatPoint(-250, -150)) + document_controller.periodic() self.assertAlmostEqualPoint(Geometry.FloatPoint(0.15, 0.25), lattice_graphic.u_pos) self.assertAlmostEqualPoint(Geometry.FloatPoint(-0.25, -0.15), lattice_graphic.v_pos) self.assertAlmostEqual(0.05, lattice_graphic.radius) display_panel.display_canvas_item.simulate_drag(origin + Geometry.FloatPoint(-300, -100), origin + Geometry.FloatPoint(-350, -50)) + document_controller.periodic() self.assertAlmostEqualPoint(Geometry.FloatPoint(0.15, 0.25), lattice_graphic.u_pos) self.assertAlmostEqualPoint(Geometry.FloatPoint(-0.25, -0.15), lattice_graphic.v_pos) self.assertAlmostEqual(0.10, lattice_graphic.radius) @@ -292,8 +307,10 @@ def test_spot_mask_inverted(self): initial_bounds = Geometry.FloatRect.from_center_and_size((0.25, 0.25), (0.25, 0.25)) spot_graphic.bounds = initial_bounds display_panel.display_canvas_item.simulate_drag((250, 250), (250, 500)) + document_controller.periodic() self.assertNotEqual(initial_bounds, Geometry.FloatRect.make(spot_graphic.bounds)) display_panel.display_canvas_item.simulate_drag((-250, -500), (-250, -250)) + document_controller.periodic() self.assertEqual(initial_bounds, Geometry.FloatRect.make(spot_graphic.bounds)) def test_spot_mask_is_sensible_when_smaller_than_one_pixel(self): @@ -487,6 +504,7 @@ def do_drag_test(d, e=0.00001): display_item.add_graphic(region) display_item.graphic_selection.set(0) display_panel.display_canvas_item.simulate_drag(*d["drag"]) + document_controller.periodic() for property, expected_value in d["output"]["properties"].items(): actual_value = get_extended_attr(region, property) # logging.debug("%s: %s == %s ?", property, actual_value, expected_value) @@ -1164,6 +1182,7 @@ def test_selected_graphics_get_priority_when_dragging_middles(self): display_item.graphic_selection.set(0) # now the smaller rectangle (selected) is behind the larger one display_panel.display_canvas_item.simulate_drag((500, 500), (600, 600)) + document_controller.periodic() # make sure the smaller one gets dragged self.assertAlmostEqualPoint(Geometry.FloatRect.make(rect_graphic1.bounds).center, Geometry.FloatPoint(y=0.6, x=0.6)) self.assertAlmostEqualPoint(Geometry.FloatRect.make(rect_graphic2.bounds).center, Geometry.FloatPoint(y=0.5, x=0.5)) diff --git a/nion/swift/test/ImageCanvasItem_test.py b/nion/swift/test/ImageCanvasItem_test.py index 62f462130..6341b2710 100644 --- a/nion/swift/test/ImageCanvasItem_test.py +++ b/nion/swift/test/ImageCanvasItem_test.py @@ -172,8 +172,10 @@ def test_selected_item_takes_priority_over_all_part(self): display_item.add_graphic(rect_region) display_item = document_model.get_display_item_for_data_item(data_item) display_panel.display_canvas_item.simulate_click((50, 950)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((0, ))) display_panel.display_canvas_item.simulate_click((500, 500)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((0, ))) def test_specific_parts_take_priority_over_all_part(self): @@ -200,6 +202,7 @@ def test_specific_parts_take_priority_over_all_part(self): display_item = document_model.get_display_item_for_data_item(data_item) # clicking on line should select it display_panel.display_canvas_item.simulate_click((500, 600)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((0, ))) def test_specific_parts_take_priority_when_another_selected(self): @@ -224,8 +227,10 @@ def test_specific_parts_take_priority_when_another_selected(self): display_item = document_model.get_display_item_for_data_item(data_item) # clicking on line should select it display_panel.display_canvas_item.simulate_click((700, 700)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((1, ))) display_panel.display_canvas_item.simulate_click((600, 200)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((0, ))) def test_hit_testing_occurs_same_as_draw_order(self): @@ -250,6 +255,7 @@ def test_hit_testing_occurs_same_as_draw_order(self): display_item.add_graphic(rect_region2) display_item = document_model.get_display_item_for_data_item(data_item) display_panel.display_canvas_item.simulate_click((500, 500)) + document_controller.periodic() self.assertEqual(display_item.graphic_selection.indexes, set((1, ))) def test_1d_data_displayed_as_2d(self): From 3f487129cfa427519cec7e1af42cbade4bb97749 Mon Sep 17 00:00:00 2001 From: Chris Meyer Date: Tue, 14 Nov 2023 17:21:07 -0800 Subject: [PATCH 11/11] Remove old mouse tracking code in raster image display. --- nion/swift/ImageCanvasItem.py | 116 ---------------------------------- 1 file changed, 116 deletions(-) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 2b4dac5cb..6091ea339 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -831,14 +831,7 @@ def __init__(self, ui_settings: UISettings.UISettings, self.__graphics: typing.List[Graphics.Graphic] = list() self.__graphic_selection: DisplayItem.GraphicSelection = DisplayItem.GraphicSelection() - # used for tracking undo - self.__undo_command: typing.Optional[Undo.UndoableCommand] = None - # used for dragging graphic items - self.__graphic_drag_items: typing.List[Graphics.Graphic] = list() - self.__graphic_drag_item: typing.Optional[Graphics.Graphic] = None - self.__graphic_part_data: typing.Dict[int, Graphics.DragPartData] = dict() - self.__graphic_drag_indexes: typing.Set[int] = set() self.__last_mouse: typing.Optional[Geometry.IntPoint] = None self.__mouse_in = False self.__mouse_handler: typing.Optional[MouseHandler] = None @@ -849,9 +842,6 @@ def __init__(self, ui_settings: UISettings.UISettings, self.__display_latency = False def close(self) -> None: - if self.__undo_command: - self.__undo_command.close() - self.__undo_command = None with self.__closing_lock: with self.__update_layout_handle_lock: update_layout_handle = self.__update_layout_handle @@ -1082,11 +1072,8 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie return False mouse_pos = Geometry.FloatPoint(y, x) image_position = widget_mapping.map_point_widget_to_image(mouse_pos) - pos = widget_mapping.map_point_widget_to_image_norm(mouse_pos) - start_drag_pos = mouse_pos if delegate.image_mouse_pressed(image_position, modifiers): return True - self.__undo_command = None if delegate.tool_mode == "pointer": assert not self.__mouse_handler assert self.__event_loop @@ -1102,73 +1089,6 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie assert self.__event_loop self.__mouse_handler = CreateGraphicMouseHandler(self, self.__event_loop, graphic_type_map[delegate.tool_mode]) self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers) - else: - delegate.begin_mouse_tracking() - # figure out clicked graphic - self.__graphic_drag_items = list() - self.__graphic_drag_item = None - self.graphic_drag_item_was_selected = False - self.__graphic_part_data = dict() - self.__graphic_drag_indexes = set() - if delegate.tool_mode == "pointer__": - graphics = self.__graphics - selection_indexes = self.__graphic_selection.indexes - multiple_items_selected = len(selection_indexes) > 1 - part_specs: typing.List[typing.Tuple[int, Graphics.Graphic, bool, str]] = list() - part_spec: typing.Optional[typing.Tuple[int, Graphics.Graphic, bool, str]] = None - specific_part_spec: typing.Optional[typing.Tuple[int, Graphics.Graphic, bool, str]] = None - # the graphics are drawn in order, which means the graphics with the higher index are "on top" of the - # graphics with the lower index. but priority should also be given to selected graphics. so sort the - # graphics according to whether they are selected or not (selected ones go later), then by their index. - for graphic_index, graphic in sorted(enumerate(graphics), key=lambda ig: (ig[0] in selection_indexes, ig[0])): - if isinstance(graphic, (Graphics.PointTypeGraphic, Graphics.LineTypeGraphic, Graphics.RectangleTypeGraphic, Graphics.SpotGraphic, Graphics.WedgeGraphic, Graphics.RingGraphic, Graphics.LatticeGraphic)): - already_selected = graphic_index in selection_indexes - move_only = not already_selected or multiple_items_selected - try: - part, specific = graphic.test(widget_mapping, self.__ui_settings, start_drag_pos, move_only) - except Exception as e: - import traceback - logging.debug("Graphic Test Error: %s", e) - traceback.print_exc() - traceback.print_stack() - continue - if part: - part_spec = graphic_index, graphic, already_selected, "all" if move_only and not part.startswith("inverted") else part - part_specs.append(part_spec) - if specific: - specific_part_spec = part_spec - # import logging - # logging.debug(specific_part_spec) - # logging.debug(part_specs) - part_spec = specific_part_spec if specific_part_spec is not None else part_specs[-1] if len(part_specs) > 0 else None - if part_spec is not None: - graphic_index, graphic, already_selected, part = part_spec - part = part if specific_part_spec is not None else part_spec[-1] - # select item and prepare for drag - self.graphic_drag_item_was_selected = already_selected - if not self.graphic_drag_item_was_selected: - if modifiers.control: - delegate.add_index_to_selection(graphic_index) - selection_indexes.add(graphic_index) - elif not already_selected: - delegate.set_selection(graphic_index) - selection_indexes.clear() - selection_indexes.add(graphic_index) - # keep track of general drag information - self.__graphic_drag_start_pos = start_drag_pos - self.__graphic_drag_changed = False - # keep track of info for the specific item that was clicked - self.__graphic_drag_item = graphics[graphic_index] - self.__graphic_drag_part = part - # keep track of drag information for each item in the set - self.__graphic_drag_indexes = selection_indexes - for index in self.__graphic_drag_indexes: - graphic = graphics[index] - self.__graphic_drag_items.append(graphic) - self.__graphic_part_data[index] = graphic.begin_drag() - if not self.__graphic_drag_items and not modifiers.control: - delegate.clear_selection() - return True def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool: @@ -1183,33 +1103,9 @@ def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifi if delegate.image_mouse_released(image_position, modifiers): return True graphics = self.__graphics - for index in self.__graphic_drag_indexes: - graphic = graphics[index] - graphic.end_drag(self.__graphic_part_data[index]) - if self.__graphic_drag_items and not self.__graphic_drag_changed: - graphic_index = graphics.index(self.__graphic_drag_item) if self.__graphic_drag_item else 0 - # user didn't move graphic - if not modifiers.control: - # user clicked on a single graphic - delegate.set_selection(graphic_index) - else: - # user control clicked. toggle selection - # if control is down and item is already selected, toggle selection of item - if self.graphic_drag_item_was_selected: - delegate.remove_index_from_selection(graphic_index) - else: - delegate.add_index_to_selection(graphic_index) if self.__mouse_handler: self.__mouse_handler.mouse_released(Geometry.IntPoint(y, x), modifiers) self.__mouse_handler = None - else: - # mouse handler will do this part - delegate.end_mouse_tracking(self.__undo_command) - self.__undo_command = None - self.__graphic_drag_items = list() - self.__graphic_drag_item = None - self.__graphic_part_data = dict() - self.__graphic_drag_indexes = set() if delegate.tool_mode != "hand": delegate.tool_mode = "pointer" return True @@ -1263,18 +1159,6 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa # x,y already have transform applied self.__last_mouse = mouse_pos.to_int_point() self.__update_cursor_info() - if self.__graphic_drag_items: - if not self.__undo_command: - self.__undo_command = delegate.create_change_graphics_command() - force_drag = modifiers.only_option - if force_drag and self.__graphic_drag_part == "all": - if Geometry.distance(mouse_pos, self.__graphic_drag_start_pos) <= 2: - delegate.drag_graphics(self.__graphic_drag_items) - return True - widget_mapping = self.__get_mouse_mapping() - delegate.adjust_graphics(widget_mapping, self.__graphic_drag_items, self.__graphic_drag_part, - self.__graphic_part_data, self.__graphic_drag_start_pos, mouse_pos, modifiers) - self.__graphic_drag_changed = True if self.__mouse_handler: self.__mouse_handler.mouse_position_changed(Geometry.IntPoint(y, x), modifiers) return True