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 all 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
13 changes: 13 additions & 0 deletions artwork/zoom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions nion/swift/DocumentController.py
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,7 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult:

Window.register_action(SetToolModeAction("pointer", _("Pointer"), "pointer_icon.png", _("Pointer tool for selecting graphics")))
Window.register_action(SetToolModeAction("hand", _("Hand"), "hand_icon.png", _("Hand tool for dragging images within panel")))
Window.register_action(SetToolModeAction("zoom", _("Zoom"), "zoom.png", _("Zoom in/out on image")))
Window.register_action(SetToolModeAction("line", _("Line"), "line_icon.png", _("Line tool for making line regions on images")))
Window.register_action(SetToolModeAction("ellipse", _("Ellipse"), "ellipse_icon.png", _("Ellipse tool for making ellipse regions on images")))
Window.register_action(SetToolModeAction("rectangle", _("Rectangle"), "rectangle_icon.png", _("Rectangle tool for making rectangle regions on images")))
Expand Down
103 changes: 97 additions & 6 deletions nion/swift/ImageCanvasItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,15 +1232,35 @@ 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(mapped_coord / shape_dim for mapped_coord, shape_dim in zip(iter(mapped), iter(self.__data_shape)))
return Geometry.FloatPoint(norm_coord[0], norm_coord[1]) # y,x
return Geometry.FloatPoint(-1, -1)

# Apply a zoom factor to the widget, optionally focused on a specific point
def apply_fixed_zoom(self, zoom_in: bool, coord: typing.Optional[Geometry.IntPoint]) -> None:
if zoom_in:
self.zoom_in(focused_position=coord)
else:
self.zoom_out(focused_position=coord)

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 @@ -1264,6 +1284,8 @@ 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":
self.apply_fixed_zoom(not modifiers.alt, Geometry.IntPoint(y=y, x=x))
elif delegate.tool_mode in graphic_type_map.keys():
assert not self.__mouse_handler
assert self.__event_loop
Expand All @@ -1274,6 +1296,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 @@ -1286,7 +1309,12 @@ 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":

if delegate.tool_mode == "hand":
pass
elif delegate.tool_mode == "zoom":
pass
else:
delegate.tool_mode = "pointer"
return True

Expand Down Expand Up @@ -1336,6 +1364,9 @@ 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":
self.cursor_shape = "cross"

# x,y already have transform applied
self.__last_mouse = mouse_pos.to_int_point()
self.__update_cursor_info()
Expand Down Expand Up @@ -1476,11 +1507,71 @@ def set_one_to_one_mode(self) -> None:
def set_two_to_one_mode(self) -> None:
self.__apply_display_properties_command({"image_zoom": 0.5, "image_position": (0.5, 0.5), "image_canvas_mode": "2:1"})

def zoom_in(self) -> None:
self.__apply_display_properties_command({"image_zoom": self.__image_zoom * 1.25, "image_canvas_mode": "custom"})

def zoom_out(self) -> None:
self.__apply_display_properties_command({"image_zoom": self.__image_zoom / 1.25, "image_canvas_mode": "custom"})
def get_current_zoom(self) -> float:
if self.image_canvas_mode == "custom":
# Already in custom, just return current zoom
return self.__image_zoom

if self.canvas_bounds and self.__data_shape:
new_zoom = 1.0
if self.image_canvas_mode == "fit":
# defaults to zoom of 1
pass
elif self.image_canvas_mode == "fill":
x_zoom = self.canvas_bounds.width / self.__data_shape[1]
y_zoom = self.canvas_bounds.height / self.__data_shape[0]
new_zoom = max(y_zoom, x_zoom) / min(y_zoom, x_zoom)
pass
elif self.image_canvas_mode == "1:1":
x_zoom = self.__data_shape[1] / self.canvas_bounds.width
y_zoom = self.__data_shape[0] / self.canvas_bounds.height
new_zoom = max(y_zoom, x_zoom)
elif self.image_canvas_mode == "2:1":
x_zoom = self.__data_shape[1] / self.canvas_bounds.width
y_zoom = self.__data_shape[0] / self.canvas_bounds.height
new_zoom = max(y_zoom, x_zoom)
new_zoom /= 2
return new_zoom
return float("nan") # No defined canvas or data sizes, zoom is undefined

def zoom(self, factor: float, focused_position: typing.Optional[Geometry.IntPoint] = None) -> None:
display_properties: dict[str, float | str | list[float]] = {}

# Image zoom appears undefined in some non-custom modes
if self.__image_canvas_mode != "custom":
previous_zoom = self.get_current_zoom()
new_zoom = previous_zoom * factor
display_properties["image_zoom"] = new_zoom
display_properties["image_canvas_mode"] = "custom"
else:
display_properties["image_zoom"] = self.__image_zoom * factor

if focused_position and self.scroll_area_canvas_item.canvas_size and self.__data_shape:
# Position to focus the zoom on here, so need to keep that position under the cursor
mapped = self.map_widget_to_image(focused_position)
if mapped:
mapped_center = self.map_widget_to_image(
Geometry.IntPoint(self.scroll_area_canvas_item.canvas_size.height // 2,
self.scroll_area_canvas_item.canvas_size.width // 2))
if mapped_center is not None:
# Vector from clicked position to image center
mapped_vector = (mapped_center[0] - mapped[0], mapped_center[1] - mapped[1])
# Scale by the inverse of the zoom factor to get the vector from
# Click point to what the new center needs to be
scaled_mapped_vector = (mapped_vector[0] / factor, mapped_vector[1] / factor)
new_image_center = (mapped[0] + scaled_mapped_vector[0],
mapped[1] + scaled_mapped_vector[1])
norm_coord = tuple(new_mapped_coord / shape_dim for new_mapped_coord, shape_dim in
zip(iter(new_image_center), iter(self.__data_shape)))
display_properties["image_position"] = list(Geometry.FloatPoint(norm_coord[0], norm_coord[1]))

self.__apply_display_properties_command(display_properties)

def zoom_in(self, factor: float = 1.25, focused_position: typing.Optional[Geometry.IntPoint] = None) -> None:
self.zoom(factor, focused_position)

def zoom_out(self, factor: float = 1.25, focused_position: typing.Optional[Geometry.IntPoint] = None) -> None:
self.zoom(1 / factor, focused_position)

def move_left(self, amount: float = 10.0) -> None:
self.apply_move_command(Geometry.FloatSize(0.0, amount))
Expand Down
3 changes: 3 additions & 0 deletions nion/swift/ToolbarPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def __init__(self, *, document_controller: DocumentController.DocumentController
else:
bottom_row_items.append(radio_button)

if len(tool_actions) % 2 == 1:
bottom_row_items.append(u.create_spacing(32))

top_row = u.create_row(*top_row_items)
bottom_row = u.create_row(*bottom_row_items)

Expand Down
3 changes: 3 additions & 0 deletions nion/swift/resources/key_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@
"window.set_tool_mode.hand": {
"display_panel": "h"
},
"window.set_tool_mode.zoom": {
"display_panel": "z"
},
"window.set_tool_mode.line": {
"display_panel": "n"
},
Expand Down
Binary file added nion/swift/resources/zoom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions nion/swift/test/ImageCanvasItem_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# standard libraries
import logging
import math
import typing
import unittest

# third party libraries
Expand All @@ -13,8 +14,12 @@
from nion.swift.model import DataItem
from nion.swift.model import Graphics
from nion.swift.test import TestContext
from nion.ui import CanvasItem
from nion.ui import DrawingContext
from nion.ui import TestUI
from nion.utils import Geometry




class TestImageCanvasItemClass(unittest.TestCase):
Expand All @@ -26,6 +31,18 @@ def setUp(self):
def tearDown(self):
TestContext.end_leaks(self)

# Wrapper function allowing comparison of Tuples with the AlmostEqual unit test function
def assertTupleAlmostEqual(self, tuple1: typing.Tuple[typing.Any, ...], tuple2: typing.Tuple[typing.Any, ...],
delta: typing.Any=None, places: typing.Any=None):
self.assertEqual(len(tuple1), len(tuple2), "Tuples are of different lengths.")
for a, b in zip(tuple1, tuple2):
if delta is not None:
self.assertAlmostEqual(a, b, delta=delta)
elif places is not None:
self.assertAlmostEqual(a, b, places=places)
else:
self.assertAlmostEqual(a, b)

def test_mapping_widget_to_image_on_2d_data_stack_uses_signal_dimensions(self):
with TestContext.create_memory_context() as test_context:
document_controller = test_context.create_document_controller()
Expand Down Expand Up @@ -291,6 +308,104 @@ 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_in_and_out_around_clicked_point_fit_mode(self):
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((50, 50)))
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((100 + header_height, 100))
display_panel.perform_action("set_fit_mode")
# run test. each data pixel will be spanned by two display pixels. clicking at an odd value
# will be a click in the center of a data pixel. zoom should expand and contract around that
# pixel. there is also a check to make sure zooming is not just ending up with the same exact
# zoom, which would also succeed.
self.assertEqual((25, 25),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(51, 51)))
self.assertEqual((30, 30),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(61, 61)))
document_controller.tool_mode = "zoom"
display_panel.display_canvas_item.simulate_click((20, 20)) # 10,10 in image space
# the zoom tool runs asynchronously, so give it a slice of async time.
document_controller.periodic()
# check results
# After zooming in on 20,20 canvas (10,10 image) the canvas will have zoomed in 1.25X so the display
# now only shows 40x40 of the image on the canvas. Since it was centered around the 10,10 position of
# the image the Canvas is now displaying 2->42 of the image with 2 image pixels off the side to the left,
# and 8 to the right. A click at 51,51 (image centre) now is 51/2.5 (20) image pixels in from the left,
# plus the 2 extra based on which bit of the image we are viewing, so 20+2 = 22.

self.assertTupleAlmostEqual((22, 22),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(51, 51)),
delta=1)
# A click at 61,61 should NOT be that 22 plus the 5 from before. This second test verifies that we are
# zoomed in and not just translated
self.assertNotEqual((30, 30),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(61, 61)))

# zoom out is done by passing the alt key to the canvas item.
display_panel.display_canvas_item.simulate_click((20, 20), CanvasItem.KeyboardModifiers(alt=True))
document_controller.periodic()
# zoom in at the center of a data pixel followed by zoom out in the same spot should end up in the same original mapping.
self.assertEqual((25, 25),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(51, 51)))

self.assertEqual((30, 30),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(61, 61)))

cmeyer marked this conversation as resolved.
Show resolved Hide resolved
def test_zoom_tool_in_and_out_around_clicked_point_1_to_1_mode(self):
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((40, 40)))
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((100 + header_height, 100))
display_panel.perform_action("set_one_to_one_mode")
# run test. Each canvas pixel starts as 1 image pixel, with image centered
self.assertEqual((20, 20),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(50, 50)))
self.assertEqual((30, 30),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(60, 60)))
document_controller.tool_mode = "zoom"
display_panel.display_canvas_item.simulate_click((40, 40)) # 10,10 in image space
# the zoom tool runs asynchronously, so give it a slice of async time.
document_controller.periodic()
# check results
# After zooming in on 40,40 canvas (10,10 image) the canvas will have zoomed in 1.25X so the display
# displays the full new 50x50, but a quarter of the new pixels (2.5) are on the left and 3/4 on the right.
# So the canvas starts displaying at 27.5 to 77.5 the image which is now 50 canvas pixels wide
# Clicking centrally again, pixel 50 is 45% of the image across. 45% of 40 pixels is 18

self.assertTupleAlmostEqual((18, 18),
display_panel.display_canvas_item.map_widget_to_image(
Geometry.IntPoint(50, 50)),
delta=1)
# A click at 61,61 should NOT be that 22 plus the 5 from before. This second test verifies that we are
# zoomed in and not just translated
self.assertNotEqual((35, 35),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(60, 60)))

# zoom out is done by passing the alt key to the canvas item.
display_panel.display_canvas_item.simulate_click((40, 40), CanvasItem.KeyboardModifiers(alt=True))
document_controller.periodic()
# zoom in at the center of a data pixel followed by zoom out in the same spot should end up in the
# same original mapping.
self.assertTupleAlmostEqual((20, 20),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(50, 50)),
delta=1)

self.assertTupleAlmostEqual((30, 30),
display_panel.display_canvas_item.map_widget_to_image(Geometry.IntPoint(60, 60)),
delta=1)


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