From 51163a30d3d9bb31f4fdb34589ffd221424c89b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:33:52 -0800 Subject: [PATCH] updates to handling movement for nodes/shapes on canvas, added initial canvas zoom/panning, fixed some issues with mobility player event handling when dialog is closed --- coretk/coretk/app.py | 9 +- coretk/coretk/coreclient.py | 9 +- coretk/coretk/dialogs/mobilityplayer.py | 15 ++- coretk/coretk/graph/graph.py | 133 +++++++++++++++--------- coretk/coretk/graph/node.py | 93 +++++------------ coretk/coretk/graph/shape.py | 17 +-- 6 files changed, 128 insertions(+), 148 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4d51cd1e..2c4826a9 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -69,14 +69,7 @@ class Application(tk.Frame): def draw_canvas(self): width = self.guiconfig["preferences"]["width"] height = self.guiconfig["preferences"]["height"] - self.canvas = CanvasGraph( - self, - self.core, - width, - height, - background="#cccccc", - scrollregion=(0, 0, 1200, 1000), - ) + self.canvas = CanvasGraph(self, self.core, width, height) self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = ttk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1b1a30da..5ba2384a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -177,7 +177,7 @@ class CoreClient: x = event.node.position.x y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y, update=False) + canvas_node.move(x, y) def handle_throughputs(self, event): # interface_throughputs = event.interface_throughputs @@ -429,10 +429,11 @@ class CoreClient: show_grpc_error(e) self.app.close() - def edit_node(self, node_id, x, y): - position = core_pb2.Position(x=x, y=y) + def edit_node(self, core_node): try: - self.client.edit_node(self.session_id, node_id, position, source="gui") + self.client.edit_node( + self.session_id, core_node.id, core_node.position, source="gui" + ) except grpc.RpcError as e: show_grpc_error(e) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 6c9799b9..63698644 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -27,6 +27,7 @@ class MobilityPlayer: self.dialog = MobilityPlayerDialog( self.master, self.app, self.canvas_node, self.config ) + self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close) if self.state == MobilityAction.START: self.set_play() elif self.state == MobilityAction.PAUSE: @@ -35,17 +36,24 @@ class MobilityPlayer: self.set_stop() self.dialog.show() + def handle_close(self): + self.dialog.destroy() + self.dialog = None + def set_play(self): - self.dialog.set_play() self.state = MobilityAction.START + if self.dialog: + self.dialog.set_play() def set_pause(self): - self.dialog.set_pause() self.state = MobilityAction.PAUSE + if self.dialog: + self.dialog.set_pause() def set_stop(self): - self.dialog.set_stop() self.state = MobilityAction.STOP + if self.dialog: + self.dialog.set_stop() class MobilityPlayerDialog(Dialog): @@ -92,7 +100,6 @@ class MobilityPlayerDialog(Dialog): self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) - self.stop_button.state(["pressed"]) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index c2c0935d..612088a0 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -15,14 +15,21 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils +SCROLL_BUFFER = 25 +ZOOM_IN = 1.1 +ZOOM_OUT = 0.9 + class CanvasGraph(tk.Canvas): - def __init__(self, master, core, width, height, cnf=None, **kwargs): - if cnf is None: - cnf = {} - kwargs["highlightthickness"] = 0 - super().__init__(master, cnf, **kwargs) + def __init__(self, master, core, width, height): + super().__init__( + master, + highlightthickness=0, + background="#cccccc", + scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER), + ) self.app = master + self.core = core self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} @@ -35,12 +42,13 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.setup_bindings() - self.core = core self.throughput_draw = Throughput(self, core) self.shape_drawing = False self.default_width = width self.default_height = height + self.ratio = 1.0 + self.offset = (0, 0) + self.cursor = (0, 0) # background related self.wallpaper_id = None @@ -51,6 +59,9 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # bindings + self.setup_bindings() + # draw base canvas self.draw_canvas() self.draw_grid() @@ -99,17 +110,19 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.click_context) + self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) self.bind("", self.double_click) + self.bind("", self.zoom) + self.bind("", lambda e: self.zoom(e, ZOOM_IN)) + self.bind("", lambda e: self.zoom(e, ZOOM_OUT)) + self.bind("", lambda e: self.scan_mark(e.x, e.y)) + self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def draw_grid(self): """ - Create grid - - :param int width: the width - :param int height: the height + Create grid. :return: nothing """ @@ -213,7 +226,8 @@ class CanvasGraph(tk.Canvas): :rtype: int :return: the item that the mouse point to """ - overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + x, y = self.canvas_xy(event) + overlapping = self.find_overlapping(x, y, x, y) selected = None for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: @@ -330,10 +344,10 @@ class CanvasGraph(tk.Canvas): self.delete(_id) self.selection.clear() - def object_drag(self, object_id, offset_x, offset_y): + def move_selection(self, object_id, x_offset, y_offset): select_id = self.selection.get(object_id) if select_id is not None: - self.move(select_id, offset_x, offset_y) + self.move(select_id, x_offset, y_offset) def delete_selection_objects(self): edges = set() @@ -382,6 +396,20 @@ class CanvasGraph(tk.Canvas): self.selection.clear() return nodes + def zoom(self, event, factor=None): + if not factor: + factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT + event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) + self.scale("all", event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox("all")) + self.ratio *= float(factor) + self.offset = ( + self.offset[0] * factor + event.x * (1 - factor), + self.offset[1] * factor + event.y * (1 - factor), + ) + logging.info("ratio: %s", self.ratio) + logging.info("offset: %s", self.offset) + def click_press(self, event): """ Start drawing an edge if mouse click is on a node @@ -389,40 +417,46 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - logging.debug(f"click press: {event}") + x, y = self.canvas_xy(event) + self.cursor = x, y selected = self.get_selected(event) + logging.debug(f"click press: %s", selected) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - x, y = self.canvas_xy(event) shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape - if self.mode == GraphMode.SELECT: - if selected is not None: + if selected is not None: + if selected not in self.selection: if selected in self.shapes: - x, y = self.canvas_xy(event) shape = self.shapes[selected] - shape.cursor_x = x - shape.cursor_y = y - if selected not in self.selection: - self.select_object(shape.id) + self.select_object(shape.id) self.selected = selected - else: - self.clear_selection() + elif selected in self.nodes: + node = self.nodes[selected] + self.select_object(node.id) + self.selected = selected + else: + self.clear_selection() def ctrl_click(self, event): + # update cursor location + x, y = self.canvas_xy(event) + self.cursor = x, y + + # handle multiple selections logging.debug("control left click: %s", event) selected = self.get_selected(event) if ( - self.mode == GraphMode.SELECT - and selected is not None + selected not in self.selection and selected in self.shapes + or selected in self.nodes ): self.select_object(selected, choose_multiple=True) @@ -433,36 +467,31 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + x, y = self.canvas_xy(event) + x_offset = x - self.cursor[0] + y_offset = y - self.cursor[1] + self.cursor = x, y + if self.mode == GraphMode.EDGE and self.drawing_edge is not None: - x2, y2 = self.canvas_xy(event) x1, y1, _, _ = self.coords(self.drawing_edge.id) - self.coords(self.drawing_edge.id, x1, y1, x2, y2) + self.coords(self.drawing_edge.id, x1, y1, x, y) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: - x, y = self.canvas_xy(event) shape = self.shapes[self.selected] shape.shape_motion(x, y) - if ( - self.mode == GraphMode.SELECT - and self.selected is not None - and self.selected in self.shapes - ): - x, y = self.canvas_xy(event) - shape = self.shapes[self.selected] - delta_x = x - shape.cursor_x - delta_y = y - shape.cursor_y - shape.motion(event) - # move other selected components - for _id in self.selection: - if _id != self.selected and _id in self.shapes: - shape = self.shapes[_id] - shape.motion(None, delta_x, delta_y) - if _id != self.selected and _id in self.nodes: - node = self.nodes[_id] - node_x = node.core_node.position.x - node_y = node.core_node.position.y - node.move(node_x + delta_x, node_y + delta_y) + if self.mode == GraphMode.EDGE: + return + + # move selected objects + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) + + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) def click_context(self, event): logging.info("context event: %s", self.context) @@ -592,7 +621,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # resize canvas and scrollregion - self.config(scrollregion=(0, 0, width + 200, height + 200)) + self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER)) self.coords(self.grid, 0, 0, width, height) # redraw gridlines to new canvas size diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 79401c04..acd03de3 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import font @@ -11,7 +10,6 @@ from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.errors import show_grpc_error from coretk.graph import tags -from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip from coretk.nodeutils import NodeUtils @@ -29,12 +27,11 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) - image_box = self.canvas.bbox(self.id) - y = image_box[3] + NODE_TEXT_OFFSET text_font = font.Font(family="TkIconFont", size=12) + label_y = self._get_label_y() self.text_id = self.canvas.create_text( x, - y, + label_y, text=self.core_node.name, tags=tags.NODE_NAME, font=text_font, @@ -44,17 +41,11 @@ class CanvasNode: self.edges = set() self.interfaces = [] self.wireless_edges = set() - self.moving = None self.antennae = [] self.setup_bindings() def setup_bindings(self): - # self.canvas.bind("", self.click_context) - self.canvas.tag_bind(self.id, "", self.click_press) - self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.double_click) - self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) @@ -95,30 +86,32 @@ class CanvasNode: self.canvas.delete(antenna_id) self.antennae.clear() - def move_antennae(self, x_offset, y_offset): - """ - redraw antennas of a node according to the new node position - - :return: nothing - """ - for antenna_id in self.antennae: - self.canvas.move(antenna_id, x_offset, y_offset) - def redraw(self): self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) - def move(self, x, y, update=True): - old_x = self.core_node.position.x - old_y = self.core_node.position.y - x_offset = x - old_x - y_offset = y - old_y - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) + def _get_label_y(self): + image_box = self.canvas.bbox(self.id) + return image_box[3] + NODE_TEXT_OFFSET + + def move(self, x, y): + x_offset = x - self.core_node.position.x + y_offset = y - self.core_node.position.y + self.motion(x_offset, y_offset, update=False) + + def motion(self, x_offset, y_offset, update=True): self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) - self.move_antennae(x_offset, y_offset) - self.canvas.object_drag(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + + # move antennae + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) + + # move edges for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -126,16 +119,18 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.throughput_draw.move(edge) - edge.link_info.recalculate_info() + for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) + + # update core with new location if self.app.core.is_runtime() and update: - self.app.core.edit_node(self.core_node.id, int(x), int(y)) + self.app.core.edit_node(self.core_node) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: @@ -164,39 +159,6 @@ class CanvasNode: self.core_node.position.x = int(x) self.core_node.position.y = int(y) - def click_press(self, event): - logging.debug(f"node click press {self.core_node.name}: {event}") - self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.selection: - self.canvas.select_object(self.id) - self.canvas.selected = self.id - - def click_release(self, event): - logging.debug(f"node click release {self.core_node.name}: {event}") - self.update_coords() - self.moving = None - - def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: - return - x, y = self.canvas.canvas_xy(event) - my_x = self.core_node.position.x - my_y = self.core_node.position.y - self.move(x, y) - - # move other selected components - for object_id, selection_id in self.canvas.selection.items(): - if object_id != self.id and object_id in self.canvas.nodes: - canvas_node = self.canvas.nodes[object_id] - other_old_x = canvas_node.core_node.position.x - other_old_y = canvas_node.core_node.position.y - other_new_x = x + other_old_x - my_x - other_new_y = y + other_old_y - my_y - self.canvas.nodes[object_id].move(other_new_x, other_new_y) - elif object_id in self.canvas.shapes: - shape = self.canvas.shapes[object_id] - shape.motion(None, x - my_x, y - my_y) - def create_context(self): is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE @@ -244,9 +206,6 @@ class CanvasNode: context.add_command(label="Hide", state=tk.DISABLED) return context - def select_multiple(self, event): - self.canvas.select_object(self.id, choose_multiple=True) - def show_config(self): self.canvas.context = None dialog = NodeConfigDialog(self.app, self.app, self) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 2928a695..56e679d2 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -49,13 +49,9 @@ class Shape: if data is None: self.created = False self.shape_data = AnnotationData() - self.cursor_x = x1 - self.cursor_y = y1 else: self.created = True self.shape_data = data - self.cursor_x = None - self.cursor_y = None self.draw() def draw(self): @@ -136,16 +132,11 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() - def motion(self, event, delta_x=None, delta_y=None): - if event is not None: - delta_x = event.x - self.cursor_x - delta_y = event.y - self.cursor_y - self.cursor_x = event.x - self.cursor_y = event.y - self.canvas.move(self.id, delta_x, delta_y) - self.canvas.object_drag(self.id, delta_x, delta_y) + def motion(self, x_offset, y_offset): + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: - self.canvas.move(self.text_id, delta_x, delta_y) + self.canvas.move(self.text_id, x_offset, y_offset) def delete(self): self.canvas.delete(self.id)