From be0caf6fd0121144397456ec438aeddf32b65e36 Mon Sep 17 00:00:00 2001 From: Matthew Stagg Date: Tue, 14 May 2024 14:07:08 +0100 Subject: [PATCH 01/12] Adding zoom in/out buttons to toolbar. Created ZoomMouseHandler, and an apply_fixed_zoom that takes a zoom target coordinate --- nion/swift/DocumentController.py | 3 +- nion/swift/ImageCanvasItem.py | 80 +++++++++++++++++++++++- nion/swift/resources/mag_glass_in.png | Bin 0 -> 1800 bytes nion/swift/resources/mag_glass_out.png | Bin 0 -> 1586 bytes nion/swift/test/ImageCanvasItem_test.py | 19 ++++++ 5 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 nion/swift/resources/mag_glass_in.png create mode 100644 nion/swift/resources/mag_glass_out.png diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index 9d6b9fae4..c71609093 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -3265,7 +3265,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"), "mag_glass_in.png", _("Zoom in on image"))) +Window.register_action(SetToolModeAction("zoom-out", _("Zoom Out"), "mag_glass_out.png", _("Zoom out on image"))) class WorkspaceChangeSplits(Window.Action): # this is for internal testing only. since it requires passing the splitter and splits, diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 30a9f4650..aa46dffb7 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -641,6 +641,31 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP change_display_properties_task.commit() +class ZoomMouseHandler(MouseHandler): + 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" + 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 + image_canvas_item._apply_fixed_zoom(self._is_zooming_in, mouse_pos) + + with delegate.create_change_display_properties_task() as change_display_properties_task: + 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) @@ -1048,15 +1073,52 @@ 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 + + #Apply a zoom factor to the widget, optionally focussed on a specific point + def _apply_fixed_zoom(self, zoom_in: bool, coord: tuple[int, int] = None): + # print('Applying zoom factor {0}, at coordinate {1},{2}'.format(zoom_in, coord[0], coord[1])) + 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) + norm_coord = tuple(ele1 / ele2 for ele1, ele2 in zip(mapped, self.__data_shape)) + self._set_image_canvas_position(norm_coord) + + # 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 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: + if 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) + self.__mouse_handler = None + 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) + self.__mouse_handler = None + elif 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: @@ -1102,7 +1164,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 + 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 @@ -1132,6 +1202,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 + if delegate.tool_mode == "pointer": self.cursor_shape = self.__mouse_handler.cursor_shape if self.__mouse_handler else "arrow" elif delegate.tool_mode == "line": @@ -1152,6 +1223,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 = "mag_glass" + elif delegate.tool_mode == "zoom-out": + self.cursor_shape = "mag_glass" + # x,y already have transform applied self.__last_mouse = mouse_pos.to_int_point() self.__update_cursor_info() diff --git a/nion/swift/resources/mag_glass_in.png b/nion/swift/resources/mag_glass_in.png new file mode 100644 index 0000000000000000000000000000000000000000..98d408050c026248717b1ea8f1e820111a714fda GIT binary patch literal 1800 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 literal 0 HcmV?d00001 diff --git a/nion/swift/test/ImageCanvasItem_test.py b/nion/swift/test/ImageCanvasItem_test.py index 6341b2710..855e98ced 100644 --- a/nion/swift/test/ImageCanvasItem_test.py +++ b/nion/swift/test/ImageCanvasItem_test.py @@ -291,6 +291,25 @@ 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): + # 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)) + + document_controller.tool_mode = "zoom-out" + display_panel.display_canvas_item.simulate_press((125, 100)) if __name__ == '__main__': logging.getLogger().setLevel(logging.DEBUG) From a5c13c65c6580bde1ca864f5bded011e5cee03c7 Mon Sep 17 00:00:00 2001 From: Matt Stagg Date: Wed, 15 May 2024 13:13:19 +0100 Subject: [PATCH 02/12] Adding svg files, and fixing an issue with the glass_in having an extra white background. --- artwork/mag_glass_in.svg | 85 ++++++++++++++++++++++++++ artwork/mag_glass_out.svg | 85 ++++++++++++++++++++++++++ nion/swift/resources/mag_glass_in.png | Bin 1800 -> 1706 bytes 3 files changed, 170 insertions(+) create mode 100644 artwork/mag_glass_in.svg create mode 100644 artwork/mag_glass_out.svg diff --git a/artwork/mag_glass_in.svg b/artwork/mag_glass_in.svg new file mode 100644 index 000000000..edb80ecf4 --- /dev/null +++ b/artwork/mag_glass_in.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + diff --git a/artwork/mag_glass_out.svg b/artwork/mag_glass_out.svg new file mode 100644 index 000000000..26310fc70 --- /dev/null +++ b/artwork/mag_glass_out.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + diff --git a/nion/swift/resources/mag_glass_in.png b/nion/swift/resources/mag_glass_in.png index 98d408050c026248717b1ea8f1e820111a714fda..f199d0daff2afbf1f45aa1bde1521acb7b9128f6 100644 GIT binary patch delta 1631 zcmV-l2B7(f4yp~1UVk@9L_t(&f$f@mY!pQt$3L@o6e?KLS|1e?MM0%%6iqZJqW*!8 zAO<7#5jF9ZqC`}DA#itdvpoVuj1giD{DB%Zn$}mN@hvt+MU)rxs(5JSBy zy&eC!UAnopx3_y++6KSLCi9!w`Tc(L-Ff_GW`)+8!x_d%(!hG49e7ttnJ&v{ zhzLx85JDuA$p&C4FwsZVF5u}gW5zTuT)1#wAmbq-5CN%FYChxSLw*jcOvs!^Wq?BDj3@fE>0@eYQcGd$@%2h#hm4Abh2w1&(_2_guy%{*e&RUmD zChsn{+bX550^T9NYV-h?N-4iAN2gzu6bS9vvu7T?jsK#ire{Ulf`89b004}yyibH~;1gu!G;z-~!Tih(A+%Z^=2qD_-WJCxtbFl0bq9_5`Z1yx@ zv@L!#n2rb`K6KJr>$3;bmK#Oq0L!w*yTqLXy?>{a$}ljQ4=be}bji0$DO>wE5{>Tc z?mieqo>0uMFyS=^9PcEA5S>Nds{ls1ok5uPx3D5~g-_b?v~r@c`Gj(i{BR35ucwG&VLK0N%I7lT6b* z&#!^4TerT6N8@c`XWL>I@UdSzLE#w*3V$Kqa?+M%Eh$0gUSQR@apRtkMx!mcT&}~b zf>LS@aJnsi7<7vr1fH9rwQJXo>g??N4mb_KFpO#Oc>Ig79I&_A-;qC8)P`Y9^>e^d zEiVzjNTEuxfK4m8{P9Xk^& z%i5@vx-*F0An!3e?$IyMbg~UELac#qdN^Q`04uWvv7*01!fGt#yCPYfDW{ z%|r9&&mYvNFvzj^iNIUH`L68kz<=|GVYJ8N@vVh*mMvR$Om}zpEb_mOeUtdt9ffe=j^-CHW8ghXh?gwRGjsh%SzPvh{&0Y=6BL4-QufC7`HQ6p; zeIycTkHum?7IjDnp_IB`YyAX$N$mh`mQsecR|tiG!GL`Q_bR;;u7lqU!?-c5U~Jg_ zAR(pv3^*6~q@ZZLWmz4jX@4#Z;|^ii_5cU={|7N3PTvpK4%{xK+}o#Oi5T_-I516f zme%?OdhaG3`~=LAQf>*Nci0YrLL?H2x1-VM6yTeJqDjC;(=_i2qIbmf00&B`G2~;A z`}-+=v8t+SuAhIBMjQbSOw+tWYyAwp*Xs_Rlu|xYs-6)u1PYNzBv_sUE&_gbW&c`H zQL)UAo)Jp`fRu7G`8`P+kTneB`bCQt?e?WaYdzv0K$xa^PLR4Ib`A&&BYFsgf&U2@ dY6pfI{{hCA-@InJbd>-A002ovPDHLkV1lcw_3Z!v delta 1726 zcmV;v20{6%4TuhqUVoKIL_t(&f$f@qY*WV_z(3~~hzSW`ifAAS3O|}9M5T!!4eF$h zKL9bJ6hVtBR849(DXl8eim4J}jeUN`RSDBHZP7MOf=$yTRwbirRB22olnRxGXfRb3 zfslg60yHRE9D<35W$fD@@$<|h_VbVDhN$~UPrmQneZTj8-+y;^zIXS8Qi^Xgj+u5~ zruo(fpvZ26-QC^VrAwEd1eR)=7MwqS{@45V?fZ}2?bwCLcMi~Xy$E<7cmZeuRsx!- zS^)S2=mGvJr3~cNsEyR7*iJ=N9KXqvhAPcb?=%D}(?{r&xf!(k&^0SCQauUE~`|8Z6U&~<$) z@G&qKVEOXpY~Q|}ii(PiZG=K0PMURngMYlK0I6tXZ>$ty{Mm zViE9jVSk6=EU*kfM@Pp(rPR9snx?UH=gyQ9T`p|gxRL7WYD0Y1>-D~9oAC^=TnXO4 zfBzNW34o@iCR{F8+WNs@kgl$-guHIuI+~iA(&}v9yqP0Mju_%z;Llcm*+nKp-~}Vm z+}xb59srR@gqt^SCaybo?pW$nR8&w|S!syh)qiz;NxJ%Wk~RQ`4<9ZAHUX5Em$PWm zqJlY6TU%=+H6cVx!5T7=HUQypxCST&ShZ?ZK^*L(Ha|wCZu3gmE*FRFvWPf=&zwE?n5-c|oWo2b4D@CKxlx0e( zhkt^TDo{?R6HU_$Ti;j!(hdP3M8rs^yGgr<#bQP>oUWdor0oGpsSzU`3s!UN%!vEOYR+CTRCnQRzo1+$TY9B3HI#SlSl^w0XjQ7 z6LMW$9b2|+v2?^_|F)%qO{5(H%a$#>41e4K7#bQ%IezV6XlN)|>?>G9Cej9=t*tEr z{0(4YVuIVZ3-e%UY5n>Mjw!v-9V z%ud;chle?N@+7xz-I{Vh2&I&oZWrj+ix)5c@!q|A3%V-IWm~)w_y|~U2OioMb~>Gum6at9M(*Cd%jD$bbc?9Z91h1DZnt|NZnJ@@ouJjGdSgO} zA9y^T6InZ&0ePJb`u+Z=qS2@y*oi3+Y4}nI;qiDpJ>~*k*B1c41I+t^3OKxW?b>&D z@7`_eTUp+RqX35v9dd=k;eYP~txP?^3gAg#0vG{q0-tD_*3;hJekH9#LI_>g-%?8b zlt+X5DX>RMY1@+2mH-rj_z~O(#3$S&ZaEx|m)&mnSv%T!Y(J2YQl0@CfK#TTidZc6 zsn6%zZ^s$JW7`7^;@=;%18yFDU_HRAQp(&GB+P;U82Eg?R;AP*h<~3=Ch-;Ul9ckx zTjGfl^Z8y=O8p9$XDa%cl=7WS?aZ1XV8-L|{2q7?xSEvxueo#QzMqYqSsMTVDdiVT zy_0kUVTZ%<;(-GP#y2d~9>zGhn9Ua;d_LbZx#-T?IlwMv^$@Uw{|8{E9hhnU3q&Xm UI_))CjsO4v07*qoM6N<$f(Cp_3;+NC From 3eca6b2cfbb30b6b3b5d19ae089749276f335cc8 Mon Sep 17 00:00:00 2001 From: Matt Stagg Date: Fri, 17 May 2024 14:09:30 +0100 Subject: [PATCH 03/12] Fixed zoom-by-drag scaling. --- nion/swift/ImageCanvasItem.py | 113 ++++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index aa46dffb7..4f0852244 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -647,7 +647,8 @@ def __init__(self, image_canvas_item: ImageCanvasItem, event_loop: asyncio.Abstr self.cursor_shape = "mag_glass" self._is_zooming_in = is_zooming_in - async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], image_canvas_item: ImageCanvasItem) -> None: + async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MousePositionAndModifiers], + image_canvas_item: ImageCanvasItem) -> None: delegate = image_canvas_item.delegate assert delegate @@ -661,10 +662,44 @@ async def _reactor_loop(self, r: Stream.ValueChangeStreamReactorInterface[MouseP # preliminary setup for the tracking loop. mouse_pos, modifiers = value_change_value - image_canvas_item._apply_fixed_zoom(self._is_zooming_in, mouse_pos) + start_drag_pos = mouse_pos - with delegate.create_change_display_properties_task() as change_display_properties_task: - change_display_properties_task.commit() + 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): + # 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: + # 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: @@ -1073,6 +1108,14 @@ 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: tuple[int, int]) -> 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) + norm_coord = tuple(ele1 / ele2 for ele1, ele2 in zip(mapped, self.__data_shape)) + return Geometry.FloatPoint(norm_coord[0], norm_coord[1]) # y,x #Apply a zoom factor to the widget, optionally focussed on a specific point def _apply_fixed_zoom(self, zoom_in: bool, coord: tuple[int, int] = None): @@ -1097,24 +1140,47 @@ def _apply_fixed_zoom(self, zoom_in: bool, coord: tuple[int, int] = None): else: self.zoom_out() + def _apply_selection_zoom(self, coord1: tuple[int, int], coord2: tuple[int, int]): + # print('Applying zoom factor {0}, at coordinate {1},{2}'.format(zoom_in, coord[0], coord[1])) + assert coord1 + assert coord2 + # print('from {0} to {1}'.format(coord1, 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) + norm_coord1 = tuple(ele1 / ele2 for ele1, ele2 in zip(coord1_mapped, self.__data_shape)) + norm_coord2 = tuple(ele1 / ele2 for ele1, ele2 in zip(coord2_mapped, self.__data_shape)) + # print('norm from {0} to {1}'.format(norm_coord1, norm_coord2)) + + norm_coord = tuple((ele1 + ele2)/2 for ele1, ele2 in zip(norm_coord1, norm_coord2)) + self._set_image_canvas_position(norm_coord) + # 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(coord1, coord2)) # y,x + # print(selection_size_screen_space) + widget_width = self.__composite_canvas_item.canvas_bounds.width / self.__image_zoom + widget_height = self.__composite_canvas_item.canvas_bounds.height / self.__image_zoom + # print(widget_width) + # print(widget_height) + 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) + # print('factor {0}'.format(widget_overall_factor)) + # print('old zoom {0}'.format(self.__image_zoom)) + self.__apply_display_properties_command({"image_zoom": widget_overall_factor * self.__image_zoom, "image_canvas_mode": "custom"}) + # print('new zoom {0}'.format(self.__image_zoom)) + # print(self.__composite_canvas_item.canvas_bounds) + + 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.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) - self.__mouse_handler = None - 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) - self.__mouse_handler = None - elif delegate and widget_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) @@ -1142,6 +1208,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 @@ -1151,7 +1227,8 @@ 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 + return + delegate = self.delegate widget_mapping = self.mouse_mapping if not delegate or not widget_mapping: From b89d0403f660a25eabe0c7e85982b4ca1219f0f3 Mon Sep 17 00:00:00 2001 From: Matt Stagg Date: Fri, 17 May 2024 16:18:09 +0100 Subject: [PATCH 04/12] Added check to see if any panels are available/selected before attempt to split them. --- nion/swift/DocumentController.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/nion/swift/DocumentController.py b/nion/swift/DocumentController.py index c71609093..4c91c533d 100755 --- a/nion/swift/DocumentController.py +++ b/nion/swift/DocumentController.py @@ -3485,15 +3485,19 @@ def execute(self, context: Window.ActionContext) -> Window.ActionResult: workspace_controller = window.workspace_controller display_panels = context.display_panels selected_display_panel = context.selected_display_panel - if workspace_controller and display_panels and selected_display_panel: - h = self.get_int_property(context, "horizontal_count") - v = self.get_int_property(context, "vertical_count") - h = max(1, min(8, h)) - v = max(1, min(8, v)) - display_panels = workspace_controller.apply_layouts(selected_display_panel, display_panels, h, v) - action_result = Window.ActionResult(Window.ActionStatus.FINISHED) - action_result.results["display_panels"] = list(display_panels) - return action_result + if workspace_controller: + if display_panels and selected_display_panel: + h = self.get_int_property(context, "horizontal_count") + v = self.get_int_property(context, "vertical_count") + h = max(1, min(8, h)) + v = max(1, min(8, v)) + display_panels = workspace_controller.apply_layouts(selected_display_panel, display_panels, h, v) + action_result = Window.ActionResult(Window.ActionStatus.FINISHED) + action_result.results["display_panels"] = list(display_panels) + return action_result + + # no selected panel, cannot split + return Window.ActionResult(Window.ActionStatus.FINISHED) raise ValueError("Missing workspace controller") def is_enabled(self, context: Window.ActionContext) -> bool: From 45833beb315ed7fba3acc13533b047195be0616e Mon Sep 17 00:00:00 2001 From: Matt Stagg Date: Fri, 17 May 2024 16:31:05 +0100 Subject: [PATCH 05/12] Made it return True if the release is handled by the base class --- nion/swift/ImageCanvasItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nion/swift/ImageCanvasItem.py b/nion/swift/ImageCanvasItem.py index 4f0852244..99d8caa79 100644 --- a/nion/swift/ImageCanvasItem.py +++ b/nion/swift/ImageCanvasItem.py @@ -1227,7 +1227,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 + return True delegate = self.delegate widget_mapping = self.mouse_mapping From 3339ca37346dc9d9524d5f735fb50a9eae8ce5cf Mon Sep 17 00:00:00 2001 From: Tiomat85 Date: Mon, 20 May 2024 10:08:52 +0100 Subject: [PATCH 06/12] Create main.yml --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..bd4cd25c7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: Add Issue to Kanban Board + +on: + issues: + types: [opened] + +jobs: + add-to-kanban: + runs-on: ubuntu-latest + + steps: + - name: Add issue to Kanban board + uses: actions/github-script@v4 + with: + script: | + const issueNumber = context.payload.issue.number; + const projectId = '1'; + const columnId = '1'; + const octokit = github.getOctokit('${{ secrets.GITHUB_TOKEN }}'); + await octokit.projects.createCard({ + column_id: columnId, + content_id: issueNumber, + content_type: 'Issue' + }); From 57aeb3a7ba0a4b5ff73a2568a8e709c084a36334 Mon Sep 17 00:00:00 2001 From: Tiomat85 Date: Mon, 20 May 2024 10:19:17 +0100 Subject: [PATCH 07/12] Update main.yml --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd4cd25c7..66d3d858f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,15 +10,17 @@ jobs: steps: - name: Add issue to Kanban board - uses: actions/github-script@v4 + uses: actions/github-script@0.7.0 with: script: | const issueNumber = context.payload.issue.number; const projectId = '1'; const columnId = '1'; - const octokit = github.getOctokit('${{ secrets.GITHUB_TOKEN }}'); + const octokit = new Octokit({ auth: process.env.PAT }); await octokit.projects.createCard({ column_id: columnId, content_id: issueNumber, content_type: 'Issue' }); + env: + PAT: ${{ secrets.PAT }} From d5aac2e3f3d906531bec23c51bf60cd9424276be Mon Sep 17 00:00:00 2001 From: lisham2000 Date: Fri, 24 May 2024 16:08:48 +0100 Subject: [PATCH 08/12] Changing the SVG export to only require height an accept multiple units --- nion/swift/DisplayPanel.py | 2 +- nion/swift/ExportDialog.py | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/nion/swift/DisplayPanel.py b/nion/swift/DisplayPanel.py index 557a6b3e2..19831f929 100644 --- a/nion/swift/DisplayPanel.py +++ b/nion/swift/DisplayPanel.py @@ -3066,7 +3066,7 @@ def preview(ui_settings: UISettings.UISettings, display_item: DisplayItem.Displa display_data_delta.mark_changed() display_canvas_item.update_display_data_delta(display_data_delta) with drawing_context.saver(): - frame_width, frame_height = width, int(width / display_canvas_item.default_aspect_ratio) + frame_width, frame_height = width, height display_canvas_item._prepare_render() display_canvas_item.repaint_immediate(drawing_context, Geometry.IntSize(height=frame_height, width=frame_width)) shape = Geometry.IntSize(height=frame_height, width=frame_width) diff --git a/nion/swift/ExportDialog.py b/nion/swift/ExportDialog.py index 0f24810dc..af3eb43e7 100644 --- a/nion/swift/ExportDialog.py +++ b/nion/swift/ExportDialog.py @@ -202,14 +202,16 @@ def __init__(self, display_item: DisplayItem.DisplayItem, display_size: Geometry self.width_model = Model.PropertyModel(display_size.width) self.height_model = Model.PropertyModel(display_size.height) - self.int_converter = Converter.IntegerToStringConverter() - + self.units = Model.PropertyModel() u = Declarative.DeclarativeUI() + width_row = u.create_row(u.create_label(text=_("Width (in)"), width=80), u.create_line_edit(text="@binding(width_model.value, converter=int_converter)"), spacing=12) - height_row = u.create_row(u.create_label(text=_("Height (in)"), width=80), u.create_line_edit(text="@binding(height_model.value, converter=int_converter)"), spacing=12) - main_page = u.create_column(width_row, height_row, spacing=12, margin=12) + height_row = u.create_row(u.create_label(text=_("Height: "), width=80), u.create_line_edit(text="@binding(height_model.value, converter=int_converter)"), + u.create_combo_box(items=["Inches", "Centimeters","Pixels"], + current_index="@binding(units.value)"), spacing=12) + main_page = u.create_column(height_row, spacing=12, margin=12) self.ui_view = main_page def close(self) -> None: @@ -233,17 +235,25 @@ def __init__(self, document_controller: DocumentController.DocumentController, d display_item.display_properties = display_properties if display_item.display_data_shape and len(display_item.display_data_shape) == 2: - display_size = Geometry.IntSize(height=4, width=4) + display_size = Geometry.IntSize(height=10, width=10) else: display_size = Geometry.IntSize(height=3, width=4) handler = ExportSVGHandler(display_item, display_size) def ok_clicked() -> bool: - dpi = 96 - width_px = (handler.width_model.value or display_size.width) * dpi - height_px = (handler.height_model.value or display_size.height) * dpi + pixels_per_unit = 96 + if handler.units.value ==1: + pixels_per_unit = 37.7953 + elif handler.units.value ==2: + pixels_per_unit = 1 + + + display_item_ratio = display_item.display_data_shape[1]/display_item.display_data_shape[0] + + height_px = (handler.height_model.value) * pixels_per_unit + width_px = int(height_px*display_item_ratio) ui = document_controller.ui filter = "SVG File (*.svg);;All Files (*.*)" export_dir = ui.get_persistent_string("export_directory", ui.get_document_location()) @@ -254,7 +264,18 @@ def ok_clicked() -> bool: if path: ui.set_persistent_string("export_directory", selected_directory) display_shape = Geometry.IntSize(height=height_px, width=width_px) + document_controller.export_svg_file(DisplayPanel.DisplayPanelUISettings(ui), display_item, display_shape, pathlib.Path(path)) + + drawing_context, shape = DisplayPanel.preview(DisplayPanel.DisplayPanelUISettings(ui), display_item, display_shape.width, display_shape.height) + view_box = Geometry.IntRect(Geometry.IntPoint(), shape) + + svg = drawing_context.to_svg(shape, view_box) + + with Utility.AtomicFileWriter(pathlib.Path(path)) as fp: + fp.write(svg) + + return True def cancel_clicked() -> bool: From dd33fdb19b7ae85cecc4be2deb3ed3bd1c031a7d Mon Sep 17 00:00:00 2001 From: lisham2000 Date: Fri, 24 May 2024 16:17:25 +0100 Subject: [PATCH 09/12] TypeChecking --- nion/swift/ExportDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nion/swift/ExportDialog.py b/nion/swift/ExportDialog.py index af3eb43e7..ffbc525b9 100644 --- a/nion/swift/ExportDialog.py +++ b/nion/swift/ExportDialog.py @@ -203,7 +203,7 @@ def __init__(self, display_item: DisplayItem.DisplayItem, display_size: Geometry self.width_model = Model.PropertyModel(display_size.width) self.height_model = Model.PropertyModel(display_size.height) self.int_converter = Converter.IntegerToStringConverter() - self.units = Model.PropertyModel() + self.units: int = Model.PropertyModel() u = Declarative.DeclarativeUI() width_row = u.create_row(u.create_label(text=_("Width (in)"), width=80), u.create_line_edit(text="@binding(width_model.value, converter=int_converter)"), spacing=12) From 55981fe146f954cc08433fd5c58e6bf0693021a4 Mon Sep 17 00:00:00 2001 From: lisham2000 Date: Fri, 24 May 2024 16:21:44 +0100 Subject: [PATCH 10/12] TypeChecking2 --- nion/swift/ExportDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nion/swift/ExportDialog.py b/nion/swift/ExportDialog.py index ffbc525b9..4e9dff3e9 100644 --- a/nion/swift/ExportDialog.py +++ b/nion/swift/ExportDialog.py @@ -203,7 +203,7 @@ def __init__(self, display_item: DisplayItem.DisplayItem, display_size: Geometry self.width_model = Model.PropertyModel(display_size.width) self.height_model = Model.PropertyModel(display_size.height) self.int_converter = Converter.IntegerToStringConverter() - self.units: int = Model.PropertyModel() + self.units: Model.PropertyModel = Model.PropertyModel() u = Declarative.DeclarativeUI() width_row = u.create_row(u.create_label(text=_("Width (in)"), width=80), u.create_line_edit(text="@binding(width_model.value, converter=int_converter)"), spacing=12) From 1da39cc83f94543fd8f30b3ac2ca381581b96efd Mon Sep 17 00:00:00 2001 From: lisham2000 Date: Fri, 24 May 2024 16:35:08 +0100 Subject: [PATCH 11/12] TypeChecking3 --- nion/swift/ExportDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nion/swift/ExportDialog.py b/nion/swift/ExportDialog.py index 4e9dff3e9..633a92dc0 100644 --- a/nion/swift/ExportDialog.py +++ b/nion/swift/ExportDialog.py @@ -203,7 +203,7 @@ def __init__(self, display_item: DisplayItem.DisplayItem, display_size: Geometry self.width_model = Model.PropertyModel(display_size.width) self.height_model = Model.PropertyModel(display_size.height) self.int_converter = Converter.IntegerToStringConverter() - self.units: Model.PropertyModel = Model.PropertyModel() + self.units = Model.PropertyModel(0) u = Declarative.DeclarativeUI() width_row = u.create_row(u.create_label(text=_("Width (in)"), width=80), u.create_line_edit(text="@binding(width_model.value, converter=int_converter)"), spacing=12) From 52d45ebcee7c718359bb377ea709ef15a99e6c20 Mon Sep 17 00:00:00 2001 From: lisham2000 Date: Tue, 28 May 2024 13:11:32 +0100 Subject: [PATCH 12/12] TypeChecking4 --- nion/swift/ExportDialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nion/swift/ExportDialog.py b/nion/swift/ExportDialog.py index 633a92dc0..9a1a55695 100644 --- a/nion/swift/ExportDialog.py +++ b/nion/swift/ExportDialog.py @@ -242,17 +242,17 @@ def __init__(self, document_controller: DocumentController.DocumentController, d handler = ExportSVGHandler(display_item, display_size) def ok_clicked() -> bool: - pixels_per_unit = 96 + pixels_per_unit: float = 96.0 if handler.units.value ==1: pixels_per_unit = 37.7953 elif handler.units.value ==2: pixels_per_unit = 1 + display_shape: tuple = display_item.display_data_shape + display_item_ratio = display_shape[1]/display_shape[0] - display_item_ratio = display_item.display_data_shape[1]/display_item.display_data_shape[0] - - height_px = (handler.height_model.value) * pixels_per_unit + height_px = int((handler.height_model.value)) * pixels_per_unit width_px = int(height_px*display_item_ratio) ui = document_controller.ui filter = "SVG File (*.svg);;All Files (*.*)"