Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Zoombutton #1073

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
be0caf6
Adding zoom in/out buttons to toolbar.
Tiomat85 May 14, 2024
a5c13c6
Adding svg files, and fixing an issue with the glass_in having an ext…
Tiomat85 May 15, 2024
3eca6b2
Fixed zoom-by-drag scaling.
Tiomat85 May 17, 2024
45833be
Made it return True if the release is handled by the base class
Tiomat85 May 17, 2024
94b264e
Merge branch 'nion-software:master' into zoombutton
Tiomat85 Jun 19, 2024
8058d57
Update ImageCanvasItem.py
Tiomat85 Jun 19, 2024
b7c3cd3
Changed icons
Tiomat85 Jun 20, 2024
01ebe1f
Remove unused files
Tiomat85 Jun 20, 2024
623072c
Missed reference
Tiomat85 Jun 20, 2024
901fc1d
Fixes as per comments
Tiomat85 Jul 8, 2024
617ba24
Zoom Out Error
Tiomat85 Jul 8, 2024
72ed328
Types
Tiomat85 Jul 8, 2024
6495018
Updates to align with new requirements setup.
Tiomat85 Sep 16, 2024
fd5e5f4
Removing unnecessary files and whitespace
Tiomat85 Sep 17, 2024
21469fc
Merge branch 'nion-software:master' into zoombutton
Tiomat85 Sep 30, 2024
6f1a70d
Adding zoom in/out buttons to toolbar.
Tiomat85 May 14, 2024
853ac50
Adding svg files, and fixing an issue with the glass_in having an ext…
Tiomat85 May 15, 2024
7761dff
Fixed zoom-by-drag scaling.
Tiomat85 May 17, 2024
f740530
Made it return True if the release is handled by the base class
Tiomat85 May 17, 2024
64c4d88
Update ImageCanvasItem.py
Tiomat85 Jun 19, 2024
6ff51a5
Changed icons
Tiomat85 Jun 20, 2024
deef9bc
Missed reference and removed unused files
Tiomat85 Jun 20, 2024
1cd42b3
Fixes as per comments
Tiomat85 Jul 8, 2024
439c3ab
Zoom Out Error
Tiomat85 Jul 8, 2024
214b008
Types
Tiomat85 Jul 8, 2024
44c0da4
Updates to align with new requirements setup
Tiomat85 Sep 16, 2024
6955eef
Icon Update and bug fixes on Zoom Tool
Tiomat85 Sep 30, 2024
08cb7d2
Merge branch 'zoombutton' of https://github.com/Phasefocus/nionswift …
Tiomat85 Sep 30, 2024
1f02377
Removed Merge Duplicated Code
Tiomat85 Sep 30, 2024
02cd65b
Fix typing issue with inputs
Tiomat85 Sep 30, 2024
2c1da07
Fix bug that had disabled keyboard zooming
Tiomat85 Oct 1, 2024
3379976
Tidying code from review
Tiomat85 Oct 2, 2024
26fa492
Adjusting Test to check accuracy
Tiomat85 Oct 2, 2024
3dd78a5
Refactor ZoomMouseHandler
Tiomat85 Oct 3, 2024
b5f9fc1
Fixed Zoom Unit Tests
Tiomat85 Oct 3, 2024
5836671
Adding Zoom keybind 'z'
Tiomat85 Oct 3, 2024
e3248f2
Fixing mypy type verification issues and removed unused function
Tiomat85 Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions artwork/zoom_in.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions artwork/zoom_out.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion nion/swift/DocumentController.py
Original file line number Diff line number Diff line change
Expand Up @@ -3292,7 +3292,8 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult:
Window.register_action(SetToolModeAction("wedge", _("Wedge"), "wedge_icon.png", _("Wedge tool for creating wedge masks")))
Window.register_action(SetToolModeAction("ring", _("Ring"), "annular_ring.png", _("Ring tool for creating ring masks")))
Window.register_action(SetToolModeAction("lattice", _("Lattice"), "lattice_icon.png", _("Lattice tool for creating periodic lattice masks")))

Window.register_action(SetToolModeAction("zoom-in", _("Zoom In"), "zoom_in.png", _("Zoom in on image")))
Window.register_action(SetToolModeAction("zoom-out", _("Zoom Out"), "zoom_out.png", _("Zoom out on image")))

class WorkspaceChangeSplits(Window.Action):
# this is for internal testing only. since it requires passing the splitter and splits,
Expand Down
152 changes: 151 additions & 1 deletion nion/swift/ImageCanvasItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,70 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP
change_display_properties_task.commit()


class ZoomMouseHandler(MouseHandler):
Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop, is_zooming_in: bool) -> None:
super().__init__(image_canvas_item, event_loop)
self.cursor_shape = "mag_glass"
Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
self._is_zooming_in = is_zooming_in

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

image_position: typing.Optional[Geometry.FloatPoint] = None

# preliminary setup for the tracking loop.
mouse_pos, modifiers = value_change_value
start_drag_pos = mouse_pos



# Commented code block for retrieving display_item for use on displaying rectangle for drag-zoom
# start_drag_pos_norm = image_canvas_item.convert_pixel_to_normalised(start_drag_pos)
# document_controller = image_canvas_item.__document_controller
# document_model = document_controller.document_model
# display_item = document_model.get_display_item_for_data_item(image_canvas_item.data_item)

with (delegate.create_change_display_properties_task() as change_display_properties_task):
Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
# 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:
if value_change.value is not None:
mouse_pos, modifiers = value_change.value
end_drag_pos = mouse_pos
if (self._is_zooming_in and
((abs(start_drag_pos[0] - end_drag_pos[0]) > 3)
or (abs(start_drag_pos[1] - end_drag_pos[1]) > 3))):
image_canvas_item._apply_selection_zoom(start_drag_pos, end_drag_pos)
else:
image_canvas_item._apply_fixed_zoom(self._is_zooming_in, start_drag_pos)
break
if value_change.value is not None:
pass
# Not released for the zoom target, we could do with drawing a rectangle
# mouse_pos, modifiers = value_change.value
# assert start_drag_pos
# if crop_region:
# display_item.remove_graphic(crop_region)
# else:
# crop_region = Graphics.RectangleGraphic()

# end_drag_pos_norm = image_canvas_item.convert_pixel_to_normalised(mouse_pos)
# crop_region.bounds = (start_drag_pos_norm, end_drag_pos_norm)
# display_item.add_graphic(crop_region)

# 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 CreateGraphicMouseHandler(MouseHandler):
def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.AbstractEventLoop, graphic_type: str) -> None:
super().__init__(image_canvas_item, event_loop)
Expand Down Expand Up @@ -1073,15 +1137,76 @@ def _update_image_canvas_position(self, widget_delta: Geometry.FloatSize) -> Geo
self._set_image_canvas_position(new_image_canvas_position)
return new_image_canvas_position

def convert_pixel_to_normalised(self, coord: Geometry.IntPoint) -> Geometry.FloatPoint:
if coord:
widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds,
list())
if widget_mapping:
mapped = self.map_widget_to_image(coord)
if (mapped is not None) and (self.__data_shape is not None):
norm_coord = tuple(ele1 / ele2 for ele1, ele2 in zip(iter(mapped), iter(self.__data_shape)))
cmeyer marked this conversation as resolved.
Show resolved Hide resolved
return Geometry.FloatPoint(norm_coord[0], norm_coord[1]) # y,x
return Geometry.FloatPoint(-1, -1)

#Apply a zoom factor to the widget, optionally focussed on a specific point
def _apply_fixed_zoom(self, zoom_in: bool, coord: typing.Optional[Geometry.IntPoint]) -> None:
if coord:
# Coordinate specified, so needing to recenter to that point before we adjust zoom levels
widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds, list())
if widget_mapping:
mapped = self.map_widget_to_image(coord)
if mapped is not None and self.__data_shape is not None:
norm_coord = tuple(ele1 / ele2 for ele1, ele2 in zip(iter(mapped), iter(self.__data_shape)))
self._set_image_canvas_position(Geometry.FloatPoint(norm_coord[0], norm_coord[1]))

# ensure that at least half of the image is always visible
new_image_norm_center_0 = max(min(norm_coord[0], 1.0), 0.0)
new_image_norm_center_1 = max(min(norm_coord[1], 1.0), 0.0)
# save the new image norm center
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)

if zoom_in:
self.zoom_in()
else:
self.zoom_out()

def _apply_selection_zoom(self, coord1: Geometry.IntPoint, coord2: Geometry.IntPoint) -> None:
assert coord1
assert coord2
widget_mapping = ImageCanvasItemMapping.make(self.__data_shape, self.__composite_canvas_item.canvas_bounds, list())
if widget_mapping:
coord1_mapped = self.map_widget_to_image(coord1)
coord2_mapped = self.map_widget_to_image(coord2)
if (coord1_mapped is not None) and (coord2_mapped is not None) and (self.__data_shape is not None):
norm_coord1 = tuple(ele1 / ele2 for ele1, ele2 in zip(iter(coord1_mapped), iter(self.__data_shape)))
norm_coord2 = tuple(ele1 / ele2 for ele1, ele2 in zip(iter(coord2_mapped), iter(self.__data_shape)))

norm_coord = tuple((ele1 + ele2)/2 for ele1, ele2 in zip(iter(norm_coord1), iter(norm_coord2)))
self._set_image_canvas_position(Geometry.FloatPoint(norm_coord[0], norm_coord[1]))
# image now centered on middle of selection, need to calculate new zoom level required
# selection size in widget pixels
selection_size_screen_space = tuple(
abs(ele1 - ele2) for ele1, ele2 in zip(iter(coord1), iter(coord2))) # y,x
if self.__composite_canvas_item.canvas_bounds is not None:
widget_width = self.__composite_canvas_item.canvas_bounds.width / self.__image_zoom
widget_height = self.__composite_canvas_item.canvas_bounds.height / self.__image_zoom
widget_width_factor = widget_width / selection_size_screen_space[1]
widget_height_factor = widget_height / selection_size_screen_space[0]
widget_overall_factor = max(widget_height_factor, widget_width_factor)
self.__apply_display_properties_command({"image_zoom": widget_overall_factor * self.__image_zoom, "image_canvas_mode": "custom"})

def mouse_clicked(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool:
if super().mouse_clicked(x, y, modifiers):
return True
delegate = self.delegate
widget_mapping = self.mouse_mapping

if delegate and widget_mapping:
# now let the image panel handle mouse clicking if desired
image_position = widget_mapping.map_point_widget_to_image(Geometry.FloatPoint(y, x))
return delegate.image_clicked(image_position, modifiers)

return False

def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool:
Expand All @@ -1105,6 +1230,16 @@ 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)
elif delegate.tool_mode == "zoom-in":
assert not self.__mouse_handler
assert self.__event_loop
self.__mouse_handler = ZoomMouseHandler(self, self.__event_loop, True)
self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers)
elif delegate.tool_mode == "zoom-out":
assert not self.__mouse_handler
assert self.__event_loop
self.__mouse_handler = ZoomMouseHandler(self, self.__event_loop, False)
self.__mouse_handler.mouse_pressed(Geometry.IntPoint(y=y, x=x), modifiers)
elif delegate.tool_mode in graphic_type_map.keys():
assert not self.__mouse_handler
assert self.__event_loop
Expand All @@ -1115,6 +1250,7 @@ def mouse_pressed(self, x: int, y: int, modifiers: UserInterface.KeyboardModifie
def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifiers) -> bool:
if super().mouse_released(x, y, modifiers):
return True

delegate = self.delegate
widget_mapping = self.mouse_mapping
if not delegate or not widget_mapping:
Expand All @@ -1127,7 +1263,15 @@ def mouse_released(self, x: int, y: int, modifiers: UserInterface.KeyboardModifi
if self.__mouse_handler:
self.__mouse_handler.mouse_released(Geometry.IntPoint(y, x), modifiers)
self.__mouse_handler = None
if delegate.tool_mode != "hand":

# Should probably wrap this into a function of 'Non-Toggle' UI elements
Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
if delegate.tool_mode == "hand":
pass
elif delegate.tool_mode == "zoom-in":
pass
elif delegate.tool_mode == "zoom-out":
pass
else:
delegate.tool_mode = "pointer"
return True

Expand Down Expand Up @@ -1157,6 +1301,7 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa
image_position = widget_mapping.map_point_widget_to_image(mouse_pos)
if delegate.image_mouse_position_changed(image_position, modifiers):
return True

Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
if delegate.tool_mode == "pointer":
self.cursor_shape = self.__mouse_handler.cursor_shape if self.__mouse_handler else "arrow"
elif delegate.tool_mode == "line":
Expand All @@ -1177,6 +1322,11 @@ def mouse_position_changed(self, x: int, y: int, modifiers: UserInterface.Keyboa
self.cursor_shape = "cross"
elif delegate.tool_mode == "hand":
self.cursor_shape = "hand"
elif delegate.tool_mode == "zoom-in":
self.cursor_shape = "cross"
elif delegate.tool_mode == "zoom-out":
self.cursor_shape = "cross"

# x,y already have transform applied
self.__last_mouse = mouse_pos.to_int_point()
self.__update_cursor_info()
Expand Down
Binary file added nion/swift/resources/zoom_in.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added nion/swift/resources/zoom_out.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions nion/swift/test/ImageCanvasItem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,27 @@ def test_hand_tool_on_one_image_of_multiple_displays(self):
document_controller.tool_mode = "hand"
display_panel.display_canvas_item.simulate_press((100,125))

def test_zoom_tool_on_one_image_of_multiple_displays(self):
Tiomat85 marked this conversation as resolved.
Show resolved Hide resolved
# setup
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)
copy_display_item = document_model.get_display_item_copy_new(display_item)
display_panel.set_display_panel_display_item(copy_display_item)
header_height = display_panel.header_canvas_item.header_height
display_panel.root_container.layout_immediate((1000 + header_height, 1000))
# run test
document_controller.tool_mode = "zoom-in"
display_panel.display_canvas_item.simulate_press((100, 125))
display_panel.display_canvas_item.simulate_release((100, 125))

cmeyer marked this conversation as resolved.
Show resolved Hide resolved
document_controller.tool_mode = "zoom-out"
display_panel.display_canvas_item.simulate_press((125, 100))
display_panel.display_canvas_item.simulate_release((125, 100))

if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
Expand Down