def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ edge = CanvasEdge(self.app, src) edge.complete(dst) return edge
def click_press(self, event: tk.Event): """ Start drawing an edge if mouse click is on a node """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y selected = self.get_selected(event) logging.debug("click press(%s): %s", self.cursor, selected) x_check = self.cursor[0] - self.offset[0] y_check = self.cursor[1] - self.offset[1] logging.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: pos = self.coords(selected) self.drawing_edge = CanvasEdge(self, selected, pos, pos) if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_tool.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), state=self.show_annotations.state(), ) return if selected is None: shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: if selected in self.shapes: shape = self.shapes[selected] self.select_object(shape.id) self.selected = selected elif selected in self.nodes: node = self.nodes[selected] self.select_object(node.id) self.selected = selected logging.debug( "selected node(%s), coords: (%s, %s)", node.core_node.name, node.core_node.position.x, node.core_node.position.y, ) else: if self.mode == GraphMode.SELECT: shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) self.select_box = shape self.clear_selection()
def create_link(self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode) -> None: """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. """ src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) src_iface = None if NodeUtils.is_container_node(src_node.type): src_iface = self.create_iface(canvas_src_node) self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token edge.src_iface = src_iface canvas_src_node.ifaces[src_iface.id] = src_iface dst_iface = None if NodeUtils.is_container_node(dst_node.type): dst_iface = self.create_iface(canvas_dst_node) self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token edge.dst_iface = dst_iface canvas_dst_node.ifaces[dst_iface.id] = dst_iface link = Link( type=LinkType.WIRED, node1_id=src_node.id, node2_id=dst_node.id, iface1=src_iface, iface2=dst_iface, ) edge.set_link(link) self.links[edge.token] = edge logging.info("added link between %s and %s", src_node.name, dst_node.name)
def create_link( self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode ): """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. """ src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node # determine subnet self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) src_interface = None if NodeUtils.is_container_node(src_node.type): src_interface = self.create_interface(canvas_src_node) edge.src_interface = src_interface self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token dst_interface = None if NodeUtils.is_container_node(dst_node.type): dst_interface = self.create_interface(canvas_dst_node) edge.dst_interface = dst_interface self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( type=core_pb2.LinkType.WIRED, node_one_id=src_node.id, node_two_id=dst_node.id, interface_one=src_interface, interface_two=dst_interface, ) edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name)
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: token = create_edge_token(src.id, dst.id) if token in self.edges and link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link elif token not in self.edges: node1 = src.core_node node2 = dst.core_node src_pos = (node1.position.x, node1.position.y) dst_pos = (node2.position.x, node2.position.y) edge = CanvasEdge(self, src.id, src_pos, dst_pos) edge.token = token edge.dst = dst.id edge.set_link(link) edge.check_wireless() src.edges.add(edge) dst.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.iface1: iface1 = link.iface1 self.core.iface_to_edge[(node1.id, iface1.id)] = token src.ifaces[iface1.id] = iface1 edge.src_iface = iface1 if link.iface2: iface2 = link.iface2 self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token dst.ifaces[iface2.id] = iface2 edge.dst_iface = iface2
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: token = create_edge_token(link) if token in self.edges and link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link elif token not in self.edges: edge = CanvasEdge(self.app, src, dst) edge.complete(dst, link)
def create_edge(self, source: CanvasNode, dest: CanvasNode): """ create an edge between source node and destination node """ token = create_edge_token(source.id, dest.id) if token not in self.edges: pos = (source.core_node.position.x, source.core_node.position.y) edge = CanvasEdge(self, source.id, pos, pos) edge.complete(dest.id) self.edges[edge.token] = edge self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest)
def create_edge(self, source: CanvasNode, dest: CanvasNode): """ create an edge between source node and destination node """ if (source.id, dest.id) not in self.edges: pos0 = source.core_node.position x0 = pos0.x y0 = pos0.y edge = CanvasEdge(x0, y0, x0, y0, source.id, self) edge.complete(dest.id) self.edges[edge.token] = edge self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest)
def complete_edge( self, src: CanvasNode, dst: CanvasNode, edge: CanvasEdge, link: Optional[Link] = None, ) -> None: linked_wireless = self.is_linked_wireless(src.id, dst.id) edge.complete(dst.id, linked_wireless) if link is None: link = self.core.create_link(edge, src, dst) edge.link = link if link.iface1: iface1 = link.iface1 src.ifaces[iface1.id] = iface1 if link.iface2: iface2 = link.iface2 dst.ifaces[iface2.id] = iface2 src.edges.add(edge) dst.edges.add(edge) edge.token = create_edge_token(edge.link) self.arc_common_edges(edge) edge.draw_labels() edge.check_options() self.edges[edge.token] = edge self.core.save_edge(edge, src, dst)
def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ pos = (src.core_node.position.x, src.core_node.position.y) edge = CanvasEdge(self, src.id, pos, pos) self.complete_edge(src, dst, edge) return edge
def delete_edge(self, edge: CanvasEdge) -> None: edge.delete() del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) if edge.src_iface: del src_node.ifaces[edge.src_iface.id] dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) if edge.dst_iface: del dst_node.ifaces[edge.dst_iface.id] src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) if dst_wireless: src_node.delete_antenna() self.core.deleted_canvas_edges([edge])
def delete_edge(self, edge: CanvasEdge): edge.delete() del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) if edge.src_interface in src_node.interfaces: src_node.interfaces.remove(edge.src_interface) dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) if edge.dst_interface in dst_node.interfaces: dst_node.interfaces.remove(edge.dst_interface) src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) if dst_wireless: src_node.delete_antenna() self.core.deleted_graph_edges([edge])
def create_link( self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode ) -> None: """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. """ src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node # determine subnet self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) src_iface = None if NodeUtils.is_container_node(src_node.type): src_iface = self.create_iface(canvas_src_node) self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token dst_iface = None if NodeUtils.is_container_node(dst_node.type): dst_iface = self.create_iface(canvas_dst_node) self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token link = Link( type=LinkType.WIRED, node1_id=src_node.id, node2_id=dst_node.id, iface1=src_iface, iface2=dst_iface, ) # assign after creating link proto, since interfaces are copied if src_iface: iface1 = link.iface1 edge.src_iface = iface1 canvas_src_node.ifaces[iface1.id] = iface1 if dst_iface: iface2 = link.iface2 edge.dst_iface = iface2 canvas_dst_node.ifaces[iface2.id] = iface2 edge.set_link(link) self.links[edge.token] = edge logging.info("Add link between %s and %s", src_node.name, dst_node.name)
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: token = create_edge_token(link) if token in self.edges and link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link elif token not in self.edges: node1 = src.core_node node2 = dst.core_node src_pos = (node1.position.x, node1.position.y) dst_pos = (node2.position.x, node2.position.y) edge = CanvasEdge(self, src.id, src_pos, dst_pos) self.complete_edge(src, dst, edge, link)
def click_link(self, node: "CanvasNode") -> None: edge = CanvasEdge(self.app, self, node) edge.complete(node)
def click_unlink(self, edge: CanvasEdge) -> None: edge.delete() self.app.default_info()
class CanvasGraph(tk.Canvas): def __init__( self, master: tk.BaseWidget, app: "Application", manager: "CanvasManager", core: "CoreClient", _id: int, dimensions: Tuple[int, int], ) -> None: super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR) self.id: int = _id self.app: "Application" = app self.manager: "CanvasManager" = manager self.core: "CoreClient" = core self.selection: Dict[int, int] = {} self.select_box: Optional[Shape] = None self.selected: Optional[int] = None self.nodes: Dict[int, CanvasNode] = {} self.shadow_nodes: Dict[int, ShadowNode] = {} self.shapes: Dict[int, Shape] = {} self.shadow_core_nodes: Dict[int, ShadowNode] = {} # map wireless/EMANE node to the set of MDRs connected to that node self.wireless_network: Dict[int, Set[int]] = {} self.drawing_edge: Optional[CanvasEdge] = None self.rect: Optional[int] = None self.shape_drawing: bool = False self.current_dimensions: Tuple[int, int] = dimensions self.ratio: float = 1.0 self.offset: Tuple[int, int] = (0, 0) self.cursor: Tuple[int, int] = (0, 0) self.to_copy: List[CanvasNode] = [] # background related self.wallpaper_id: Optional[int] = None self.wallpaper: Optional[Image.Image] = None self.wallpaper_drawn: Optional[PhotoImage] = None self.wallpaper_file: str = "" self.scale_option: tk.IntVar = tk.IntVar(value=1) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) # bindings self.setup_bindings() # draw base canvas self.draw_canvas() self.draw_grid() def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: if self.rect is not None: self.delete(self.rect) if not dimensions: dimensions = self.manager.default_dimensions self.current_dimensions = dimensions self.rect = self.create_rectangle( 0, 0, *dimensions, outline="#000000", fill="#ffffff", width=1, tags="rectangle", ) self.configure(scrollregion=self.bbox(tk.ALL)) def setup_bindings(self) -> None: """ Bind any mouse events or hot keys to the matching action """ self.bind("<Control-c>", self.copy_selected) self.bind("<Control-v>", self.paste_selected) self.bind("<Control-x>", self.cut_selected) self.bind("<Control-d>", self.delete_selected) self.bind("<Control-h>", self.hide_selected) self.bind("<ButtonPress-1>", self.click_press) self.bind("<ButtonRelease-1>", self.click_release) self.bind("<B1-Motion>", self.click_motion) self.bind("<Delete>", self.delete_selected) self.bind("<Control-1>", self.ctrl_click) self.bind("<Double-Button-1>", self.double_click) self.bind("<MouseWheel>", self.zoom) self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN)) self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT)) self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y)) self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def get_shadow(self, node: CanvasNode) -> ShadowNode: shadow_node = self.shadow_core_nodes.get(node.core_node.id) if not shadow_node: shadow_node = ShadowNode(self.app, self, node) return shadow_node def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright def draw_grid(self) -> None: """ Create grid. """ width, height = self.width_and_height() width = int(width) height = int(height) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate """ x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y def get_selected(self, event: tk.Event) -> int: """ Retrieve the item id that is on the mouse position """ 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: continue elif _id in self.nodes: selected = _id elif _id in self.shapes: selected = _id elif _id in self.shadow_nodes: selected = _id return selected def click_release(self, event: tk.Event) -> None: """ Draw a node or finish drawing an edge according to the current graph mode """ logger.debug("click release") x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return if self.manager.mode == GraphMode.ANNOTATION: self.focus_set() if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False elif self.manager.mode == GraphMode.SELECT: self.focus_set() if self.select_box: x0, y0, x1, y1 = self.coords(self.select_box.id) inside = [ x for x in self.find_enclosed(x0, y0, x1, y1) if "node" in self.gettags(x) or "shape" in self.gettags(x) ] for i in inside: self.select_object(i, True) self.select_box.disappear() self.select_box = None else: self.focus_set() self.selected = self.get_selected(event) logger.debug("click release selected(%s) mode(%s)", self.selected, self.manager.mode) if self.manager.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.manager.mode == GraphMode.NODE: self.add_node(x, y) elif self.manager.mode == GraphMode.PICKNODE: self.manager.mode = GraphMode.NODE self.selected = None def handle_edge_release(self, _event: tk.Event) -> None: # not drawing edge return if not self.drawing_edge: return edge = self.drawing_edge self.drawing_edge = None # edge dst must be a node logger.debug("current selected: %s", self.selected) dst_node = self.nodes.get(self.selected) if not dst_node: edge.delete() return # check if node can be linked if not edge.src.is_linkable(dst_node): edge.delete() return # finalize edge creation edge.drawing(dst_node.position()) edge.complete(dst_node) def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ create a bounding box when a node is selected """ if not choose_multiple: self.clear_selection() # draw a bounding box if node hasn't been selected yet if object_id not in self.selection: x0, y0, x1, y1 = self.bbox(object_id) selection_id = self.create_rectangle( (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-", tags=tags.SELECTION, ) self.selection[object_id] = selection_id else: selection_id = self.selection.pop(object_id) self.delete(selection_id) def clear_selection(self) -> None: """ Clear current selection boxes. """ for _id in self.selection.values(): self.delete(_id) self.selection.clear() def move_selection(self, object_id: int, x_offset: float, y_offset: float) -> None: select_id = self.selection.get(object_id) if select_id is not None: self.move(select_id, x_offset, y_offset) def delete_selected_objects(self, _event: tk.Event = None) -> None: edges = set() nodes = [] for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] self.delete(selection_id) # delete node and related edges if object_id in self.nodes: canvas_node = self.nodes.pop(object_id) # delete related edges while canvas_node.edges: edge = canvas_node.edges.pop() if edge in edges: continue edges.add(edge) edge.delete() # delete node canvas_node.delete() nodes.append(canvas_node) # delete shape if object_id in self.shapes: shape = self.shapes.pop(object_id) shape.delete() self.selection.clear() self.core.deleted_canvas_nodes(nodes) def hide_selected(self, _event: tk.Event = None) -> None: for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] self.delete(selection_id) # hide node and related edges if object_id in self.nodes: canvas_node = self.nodes[object_id] canvas_node.hide() def show_hidden(self) -> None: for node in self.nodes.values(): if node.hidden: node.show() def zoom(self, event: tk.Event, factor: float = None) -> 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(tk.ALL, event.x, event.y, factor, factor) self.configure(scrollregion=self.bbox(tk.ALL)) self.ratio *= float(factor) self.offset = ( self.offset[0] * factor + event.x * (1 - factor), self.offset[1] * factor + event.y * (1 - factor), ) logger.debug("ratio: %s", self.ratio) logger.debug("offset: %s", self.offset) self.app.statusbar.set_zoom(self.ratio) if self.wallpaper: self.redraw_wallpaper() def click_press(self, event: tk.Event) -> None: """ Start drawing an edge if mouse click is on a node """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y selected = self.get_selected(event) logger.debug("click press(%s): %s", self.cursor, selected) x_check = self.cursor[0] - self.offset[0] y_check = self.cursor[1] - self.offset[1] logger.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.manager.mode == GraphMode.EDGE and is_node: node = self.nodes[selected] self.drawing_edge = CanvasEdge(self.app, node) self.organize() if self.manager.mode == GraphMode.ANNOTATION: if is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), state=self.manager.show_annotations.state(), ) return if selected is None: shape = Shape(self.app, self, self.manager.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: if selected in self.shapes: shape = self.shapes[selected] self.select_object(shape.id) self.selected = selected elif selected in self.nodes: node = self.nodes[selected] self.select_object(node.id) self.selected = selected logger.debug( "selected node(%s), coords: (%s, %s)", node.core_node.name, node.core_node.position.x, node.core_node.position.y, ) elif selected in self.shadow_nodes: shadow_node = self.shadow_nodes[selected] self.select_object(shadow_node.id) self.selected = selected logger.debug( "selected shadow node(%s), coords: (%s, %s)", shadow_node.node.core_node.name, shadow_node.node.core_node.position.x, shadow_node.node.core_node.position.y, ) else: if self.manager.mode == GraphMode.SELECT: shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) self.select_box = shape self.clear_selection() def ctrl_click(self, event: tk.Event) -> None: # update cursor location x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y # handle multiple selections logger.debug("control left click: %s", event) selected = self.get_selected(event) if (selected not in self.selection and selected in self.shapes or selected in self.nodes): self.select_object(selected, choose_multiple=True) def click_motion(self, event: tk.Event) -> None: x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: self.select_box.delete() self.select_box = None if is_draw_shape( self.manager.annotation_type) and self.shape_drawing: shape = self.shapes.pop(self.selected) shape.delete() self.shape_drawing = False return x_offset = x - self.cursor[0] y_offset = y - self.cursor[1] self.cursor = x, y if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None: self.drawing_edge.drawing(self.cursor) if self.manager.mode == GraphMode.ANNOTATION: if is_draw_shape( self.manager.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) return elif is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), ) return if self.manager.mode == GraphMode.EDGE: return # move selected objects if self.selection: for selected_id in self.selection: if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes: shape = self.shapes[selected_id] shape.motion(x_offset, y_offset) elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes: node = self.nodes[selected_id] node.motion(x_offset, y_offset, update=self.core.is_runtime()) elif (self.manager.mode in MOVE_NODE_MODES and selected_id in self.shadow_nodes): shadow_node = self.shadow_nodes[selected_id] shadow_node.motion(x_offset, y_offset) else: if self.select_box and self.manager.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) def double_click(self, event: tk.Event) -> None: selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] dialog = ShapeDialog(self.app, shape) dialog.show() def add_node(self, x: float, y: float) -> None: if self.selected is not None and self.selected not in self.shapes: return actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( actual_x, actual_y, self.manager.node_draw.node_type, self.manager.node_draw.model, ) if not core_node: return core_node.canvas = self.id node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image) self.nodes[node.id] = node self.core.set_canvas_node(core_node, node) def width_and_height(self) -> Tuple[int, int]: """ retrieve canvas width and height in pixels """ x0, y0, x1, y1 = self.coords(self.rect) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h def get_wallpaper_image(self) -> Image.Image: width = int(self.wallpaper.width * self.ratio) height = int(self.wallpaper.height * self.ratio) image = self.wallpaper.resize((width, height), Image.ANTIALIAS) return image def draw_wallpaper(self, image: PhotoImage, x: float = None, y: float = None) -> None: if x is None and y is None: x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 y = (y1 + y2) / 2 self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image def wallpaper_upper_left(self) -> None: self.delete(self.wallpaper_id) # create new scaled image, cropped if needed width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = image.width cropy = image.height if image.width > width: cropx = image.width if image.height > height: cropy = image.height cropped = image.crop((0, 0, cropx, cropy)) image = PhotoImage(cropped) # draw on canvas x1, y1, _, _ = self.bbox(self.rect) x = (cropx / 2) + x1 y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) def wallpaper_center(self) -> None: """ place the image at the center of canvas """ self.delete(self.wallpaper_id) # dimension of the cropped image width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = 0 if image.width > width: cropx = (image.width - width) / 2 cropy = 0 if image.height > height: cropy = (image.height - height) / 2 x1 = 0 + cropx y1 = 0 + cropy x2 = image.width - cropx y2 = image.height - cropy cropped = image.crop((x1, y1, x2, y2)) image = PhotoImage(cropped) self.draw_wallpaper(image) def wallpaper_scaled(self) -> None: """ scale image based on canvas dimension """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image = PhotoImage(image) self.draw_wallpaper(image) def resize_to_wallpaper(self) -> None: self.delete(self.wallpaper_id) image = PhotoImage(self.wallpaper) self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: logger.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position logger.debug("resetting scaling: %s %s", self.ratio, self.offset) factor = 1 / self.ratio self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.move(tk.ALL, -self.offset[0], -self.offset[1]) # reset ratio and offset self.ratio = 1.0 self.offset = (0, 0) # redraw canvas rectangle self.draw_canvas(dimensions) # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) self.draw_grid() self.app.manager.show_grid.click_handler() def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): logger.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) logger.debug("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: self.wallpaper_center() elif option == ScaleOption.SCALED: self.wallpaper_scaled() elif option == ScaleOption.TILED: logger.warning("tiled background not implemented yet") self.organize() def organize(self) -> None: for tag in tags.ORGANIZE_TAGS: self.tag_raise(tag) def set_wallpaper(self, filename: Optional[str]) -> None: logger.info("setting canvas(%s) background: %s", self.id, filename) if filename: img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename self.redraw_wallpaper() else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) self.wallpaper = None self.wallpaper_file = None def is_selection_mode(self) -> bool: return self.manager.mode == GraphMode.SELECT def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ edge = CanvasEdge(self.app, src) edge.complete(dst) return edge def copy_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("copy is disabled during runtime state") return if self.selection: logger.debug("to copy nodes: %s", self.selection) self.to_copy.clear() for node_id in self.selection.keys(): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) def cut_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("cut is disabled during runtime state") return self.copy_selected() self.delete_selected() def delete_selected(self, _event: tk.Event = None) -> None: """ delete selected nodes and any data that relates to it """ logger.debug("press delete key") if self.core.is_runtime(): logger.debug("node deletion is disabled during runtime state") return self.delete_selected_objects() self.app.default_info() def paste_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over to_copy_edges = set() to_copy_ids = {x.id for x in self.to_copy} for canvas_node in self.to_copy: core_node = canvas_node.core_node actual_x = core_node.position.x + 50 actual_y = core_node.position.y + 50 scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y) copy = self.core.create_node(actual_x, actual_y, core_node.type, core_node.model) if not copy: continue node = CanvasNode(self.app, self, scaled_x, scaled_y, copy, canvas_node.image) # copy configurations and services node.core_node.services = core_node.services.copy() node.core_node.config_services = core_node.config_services.copy() node.core_node.emane_model_configs = deepcopy( core_node.emane_model_configs) node.core_node.wlan_config = deepcopy(core_node.wlan_config) node.core_node.mobility_config = deepcopy( core_node.mobility_config) node.core_node.service_configs = deepcopy( core_node.service_configs) node.core_node.service_file_configs = deepcopy( core_node.service_file_configs) node.core_node.config_service_configs = deepcopy( core_node.config_service_configs) copy_map[canvas_node.id] = node.id self.nodes[node.id] = node self.core.set_canvas_node(copy, node) for edge in canvas_node.edges: if edge.src not in to_copy_ids or edge.dst not in to_copy_ids: if canvas_node.id == edge.src: dst_node = self.nodes[edge.dst] copy_edge = self.create_edge(node, dst_node) elif canvas_node.id == edge.dst: src_node = self.nodes[edge.src] copy_edge = self.create_edge(src_node, node) else: continue copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None options = edge.link.options if options: copy_edge.link.options = deepcopy(options) if options and options.unidirectional: asym_iface1 = None if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, iface1=asym_iface2, iface2=asym_iface1, options=deepcopy(edge.asymmetric_link.options), ) copy_edge.redraw() else: to_copy_edges.add(edge) # copy link and link config for edge in to_copy_edges: src_node_id = copy_map[edge.src] dst_node_id = copy_map[edge.dst] src_node_copy = self.nodes[src_node_id] dst_node_copy = self.nodes[dst_node_id] copy_edge = self.create_edge(src_node_copy, dst_node_copy) copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None options = edge.link.options if options: copy_link.options = deepcopy(options) if options and options.unidirectional: asym_iface1 = None if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, iface1=asym_iface2, iface2=asym_iface1, options=deepcopy(edge.asymmetric_link.options), ) copy_edge.redraw() self.itemconfig( copy_edge.id, width=self.itemcget(edge.id, "width"), fill=self.itemcget(edge.id, "fill"), ) self.tag_raise(tags.NODE) def scale_graph(self) -> None: for node_id, canvas_node in self.nodes.items(): image = nutils.get_icon(canvas_node.core_node, self.app) self.itemconfig(node_id, image=image) canvas_node.image = image canvas_node.scale_text() canvas_node.scale_antennas() for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) def get_metadata(self) -> Dict[str, Any]: wallpaper_path = None if self.wallpaper_file: wallpaper = Path(self.wallpaper_file) if appconfig.BACKGROUNDS_PATH == wallpaper.parent: wallpaper_path = wallpaper.name else: wallpaper_path = str(wallpaper) return dict( id=self.id, wallpaper=wallpaper_path, wallpaper_style=self.scale_option.get(), fit_image=self.adjust_to_dim.get(), dimensions=self.current_dimensions, ) def parse_metadata(self, config: Dict[str, Any]) -> None: fit_image = config.get("fit_image", False) self.adjust_to_dim.set(fit_image) wallpaper_style = config.get("wallpaper_style", 1) self.scale_option.set(wallpaper_style) dimensions = config.get("dimensions") if dimensions: self.redraw_canvas(dimensions) wallpaper = config.get("wallpaper") if wallpaper: wallpaper = Path(wallpaper) if not wallpaper.is_file(): wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper) logger.info("canvas(%s), wallpaper: %s", self.id, wallpaper) if wallpaper.is_file(): self.set_wallpaper(str(wallpaper)) else: self.app.show_error("Background Error", f"background file not found: {wallpaper}")
def draw_session(self, session): """ Draw existing session. :return: nothing """ # draw existing nodes for core_node in session.nodes: # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas logging.info("drawing core node: %s", core_node) image = NodeUtils.node_icon(core_node.type, core_node.model) if core_node.icon: try: image = Images.create(core_node.icon, nodeutils.ICON_SIZE) except OSError: logging.error("invalid icon: %s", core_node.icon) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.master, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: logging.info("drawing link: %s", link) canvas_node_one = self.core.canvas_nodes[link.node_one_id] node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: if token not in self.edges: edge = CanvasEdge( node_one.position.x, node_one.position.y, node_two.position.x, node_two.position.y, canvas_node_one.id, self, ) edge.token = token edge.dst = canvas_node_two.id edge.set_link(link) edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) if link.HasField("interface_two"): canvas_node_two.interfaces.append(link.interface_two) elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link else: logging.error("duplicate link received: %s", link) # raise the nodes so they on top of the links self.tag_raise(tags.NODE)
def draw_session(self, session: core_pb2.Session): """ Draw existing session. """ # draw existing nodes for core_node in session.nodes: logging.debug("drawing node %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) # if the gui can't find node's image, default to the "edit-node" image if not image: image = Images.get(ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.app, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: logging.debug("drawing link: %s", link) canvas_node_one = self.core.canvas_nodes[link.node_one_id] node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node token = create_edge_token(canvas_node_one.id, canvas_node_two.id) if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two, link) else: if token not in self.edges: src_pos = (node_one.position.x, node_one.position.y) dst_pos = (node_two.position.x, node_two.position.y) edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos) edge.token = token edge.dst = canvas_node_two.id edge.set_link(link) edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) edge.src_interface = link.interface_one if link.HasField("interface_two"): canvas_node_two.interfaces.append(link.interface_two) edge.dst_interface = link.interface_two elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link else: logging.error("duplicate link received: %s", link) # raise the nodes so they on top of the links self.tag_raise(tags.NODE)
class CanvasGraph(tk.Canvas): def __init__( self, master: tk.Widget, app: "Application", core: "CoreClient", width: int, height: int, ): super().__init__(master, highlightthickness=0, background="#cccccc") self.app = app self.core = core self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} self.select_box = None self.selected = None self.node_draw = None self.nodes = {} self.edges = {} self.shapes = {} self.wireless_edges = {} # map wireless/EMANE node to the set of MDRs connected to that node self.wireless_network = {} self.drawing_edge = None self.rect = None self.shape_drawing = False self.default_dimensions = (width, height) self.current_dimensions = self.default_dimensions self.ratio = 1.0 self.offset = (0, 0) self.cursor = (0, 0) self.marker_tool = None self.to_copy = [] # background related self.wallpaper_id = None self.wallpaper = None self.wallpaper_drawn = None self.wallpaper_file = "" self.scale_option = tk.IntVar(value=1) self.adjust_to_dim = tk.BooleanVar(value=False) # throughput related self.throughput_threshold = 250.0 self.throughput_width = 10 self.throughput_color = "#FF0000" # drawing related self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) self.show_interface_names = BooleanVar(value=False) self.show_ip4s = BooleanVar(value=True) self.show_ip6s = BooleanVar(value=True) # bindings self.setup_bindings() # draw base canvas self.draw_canvas() self.draw_grid() def draw_canvas(self, dimensions: Tuple[int, int] = None): if self.rect is not None: self.delete(self.rect) if not dimensions: dimensions = self.default_dimensions self.current_dimensions = dimensions self.rect = self.create_rectangle( 0, 0, *dimensions, outline="#000000", fill="#ffffff", width=1, tags="rectangle", ) self.configure(scrollregion=self.bbox(tk.ALL)) def reset_and_redraw(self, session: core_pb2.Session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc client. :param session: session to draw """ # reset view options to default state self.show_node_labels.set(True) self.show_link_labels.set(True) self.show_grid.set(True) self.show_annotations.set(True) self.show_interface_names.set(False) self.show_ip4s.set(True) self.show_ip6s.set(True) # delete any existing drawn items for tag in tags.COMPONENT_TAGS: self.delete(tag) # set the private variables to default value self.mode = GraphMode.SELECT self.annotation_type = None self.node_draw = None self.selected = None self.nodes.clear() self.edges.clear() self.shapes.clear() self.wireless_edges.clear() self.wireless_network.clear() self.drawing_edge = None self.draw_session(session) def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action """ self.bind("<ButtonPress-1>", self.click_press) self.bind("<ButtonRelease-1>", self.click_release) self.bind("<B1-Motion>", self.click_motion) self.bind("<Delete>", self.press_delete) self.bind("<Control-1>", self.ctrl_click) self.bind("<Double-Button-1>", self.double_click) self.bind("<MouseWheel>", self.zoom) self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN)) self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT)) self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y)) self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def get_actual_coords(self, x: float, y: float) -> [float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y def get_scaled_coords(self, x: float, y: float) -> [float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y def inside_canvas(self, x: float, y: float) -> [bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> [bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent): for interface_throughput in throughputs_event.interface_throughputs: node_id = interface_throughput.node_id interface_id = interface_throughput.interface_id throughput = interface_throughput.throughput interface_to_edge_id = (node_id, interface_id) token = self.core.interface_to_edge.get(interface_to_edge_id) if not token: continue edge = self.edges.get(token) if edge: edge.set_throughput(throughput) else: del self.core.interface_to_edge[interface_to_edge_id] def draw_grid(self): """ Create grid. """ width, height = self.width_and_height() width = int(width) height = int(height) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link) -> None: network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) if token in self.wireless_edges: logging.warning("ignoring link that already exists: %s", link) return src_pos = self.coords(src.id) dst_pos = self.coords(dst.id) edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) if link.label: edge.middle_label_text(link.label) if link.color: edge.color = link.color self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) self.tag_raise(src.id) self.tag_raise(dst.id) # update arcs when there are multiple links common_edges = list(src.wireless_edges & dst.wireless_edges) arc_edges(common_edges) def delete_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link) -> None: network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) if token not in self.wireless_edges: return edge = self.wireless_edges.pop(token) edge.delete() src.wireless_edges.remove(edge) dst.wireless_edges.remove(edge) # update arcs when there are multiple links common_edges = list(src.wireless_edges & dst.wireless_edges) arc_edges(common_edges) def update_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link) -> None: if not link.label: return network_id = link.network_id if link.network_id else None token = create_edge_token(src.id, dst.id, network_id) if token not in self.wireless_edges: self.add_wireless_edge(src, dst, link) else: edge = self.wireless_edges[token] edge.middle_label_text(link.label) def draw_session(self, session: core_pb2.Session): """ Draw existing session. """ # draw existing nodes for core_node in session.nodes: logging.debug("drawing node %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) # if the gui can't find node's image, default to the "edit-node" image if not image: image = Images.get(ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)) x = core_node.position.x y = core_node.position.y node = CanvasNode(self.app, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: logging.debug("drawing link: %s", link) canvas_node_one = self.core.canvas_nodes[link.node_one_id] node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node token = create_edge_token(canvas_node_one.id, canvas_node_two.id) if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two, link) else: if token not in self.edges: src_pos = (node_one.position.x, node_one.position.y) dst_pos = (node_two.position.x, node_two.position.y) edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos) edge.token = token edge.dst = canvas_node_two.id edge.set_link(link) edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = edge if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) edge.src_interface = link.interface_one if link.HasField("interface_two"): canvas_node_two.interfaces.append(link.interface_two) edge.dst_interface = link.interface_two elif link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link else: logging.error("duplicate link received: %s", link) # raise the nodes so they on top of the links self.tag_raise(tags.NODE) def stopped_session(self): # clear wireless edges for edge in self.wireless_edges.values(): edge.delete() src_node = self.nodes[edge.src] src_node.wireless_edges.remove(edge) dst_node = self.nodes[edge.dst] dst_node.wireless_edges.remove(edge) self.wireless_edges.clear() # clear all middle edge labels for edge in self.edges.values(): edge.reset() def canvas_xy(self, event: tk.Event) -> [float, float]: """ Convert window coordinate to canvas coordinate """ x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y def get_selected(self, event: tk.Event) -> int: """ Retrieve the item id that is on the mouse position """ 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: continue if _id in self.nodes: selected = _id break if _id in self.shapes: selected = _id return selected def click_release(self, event: tk.Event): """ Draw a node or finish drawing an edge according to the current graph mode """ logging.debug("click release") x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return if self.mode == GraphMode.ANNOTATION: self.focus_set() if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False elif self.mode == GraphMode.SELECT: self.focus_set() if self.select_box: x0, y0, x1, y1 = self.coords(self.select_box.id) inside = [ x for x in self.find_enclosed(x0, y0, x1, y1) if "node" in self.gettags(x) or "shape" in self.gettags(x) ] for i in inside: self.select_object(i, True) self.select_box.disappear() self.select_box = None else: self.focus_set() self.selected = self.get_selected(event) logging.debug( f"click release selected({self.selected}) mode({self.mode})") if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE self.selected = None def handle_edge_release(self, _event: tk.Event): edge = self.drawing_edge self.drawing_edge = None # not drawing edge return if edge is None: return # edge dst must be a node logging.debug("current selected: %s", self.selected) dst_node = self.nodes.get(self.selected) if not dst_node: edge.delete() return # edge dst is same as src, delete edge if edge.src == self.selected: edge.delete() return # ignore repeated edges token = create_edge_token(edge.src, self.selected) if token in self.edges: edge.delete() return # set dst node and snap edge to center edge.complete(self.selected) self.edges[edge.token] = edge node_src = self.nodes[edge.src] node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) self.core.create_link(edge, node_src, node_dst) def select_object(self, object_id: int, choose_multiple: bool = False): """ create a bounding box when a node is selected """ if not choose_multiple: self.clear_selection() # draw a bounding box if node hasn't been selected yet if object_id not in self.selection: x0, y0, x1, y1 = self.bbox(object_id) selection_id = self.create_rectangle( (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-", tags=tags.SELECTION, ) self.selection[object_id] = selection_id else: selection_id = self.selection.pop(object_id) self.delete(selection_id) def clear_selection(self): """ Clear current selection boxes. """ for _id in self.selection.values(): self.delete(_id) self.selection.clear() def move_selection(self, object_id: int, x_offset: float, y_offset: float): select_id = self.selection.get(object_id) if select_id is not None: self.move(select_id, x_offset, y_offset) def delete_selected_objects(self) -> None: edges = set() nodes = [] for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] self.delete(selection_id) # delete node and related edges if object_id in self.nodes: canvas_node = self.nodes.pop(object_id) canvas_node.delete() nodes.append(canvas_node) is_wireless = NodeUtils.is_wireless_node( canvas_node.core_node.type) # delete related edges for edge in canvas_node.edges: if edge in edges: continue edges.add(edge) del self.edges[edge.token] edge.delete() # update node connected to edge being deleted other_id = edge.src other_interface = edge.src_interface if edge.src == object_id: other_id = edge.dst other_interface = edge.dst_interface other_node = self.nodes[other_id] other_node.edges.remove(edge) if other_interface in other_node.interfaces: other_node.interfaces.remove(other_interface) if is_wireless: other_node.delete_antenna() # delete shape if object_id in self.shapes: shape = self.shapes.pop(object_id) shape.delete() self.selection.clear() self.core.deleted_graph_nodes(nodes) self.core.deleted_graph_edges(edges) def delete_edge(self, edge: CanvasEdge): edge.delete() del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) if edge.src_interface in src_node.interfaces: src_node.interfaces.remove(edge.src_interface) dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) if edge.dst_interface in dst_node.interfaces: dst_node.interfaces.remove(edge.dst_interface) src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) if dst_wireless: src_node.delete_antenna() self.core.deleted_graph_edges([edge]) def zoom(self, event: tk.Event, factor: float = 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(tk.ALL, event.x, event.y, factor, factor) self.configure(scrollregion=self.bbox(tk.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) self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%") if self.wallpaper: self.redraw_wallpaper() def click_press(self, event: tk.Event): """ Start drawing an edge if mouse click is on a node """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y selected = self.get_selected(event) logging.debug("click press(%s): %s", self.cursor, selected) x_check = self.cursor[0] - self.offset[0] y_check = self.cursor[1] - self.offset[1] logging.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: pos = self.coords(selected) self.drawing_edge = CanvasEdge(self, selected, pos, pos) if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_tool.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), state=self.show_annotations.state(), ) return if selected is None: shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: if selected in self.shapes: shape = self.shapes[selected] self.select_object(shape.id) self.selected = selected elif selected in self.nodes: node = self.nodes[selected] self.select_object(node.id) self.selected = selected logging.debug( "selected node(%s), coords: (%s, %s)", node.core_node.name, node.core_node.position.x, node.core_node.position.y, ) else: if self.mode == GraphMode.SELECT: shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) self.select_box = shape self.clear_selection() def ctrl_click(self, event: tk.Event): # update cursor location x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y # handle multiple selections logging.debug("control left click: %s", event) selected = self.get_selected(event) if (selected not in self.selection and selected in self.shapes or selected in self.nodes): self.select_object(selected, choose_multiple=True) def click_motion(self, event: tk.Event): """ Redraw drawing edge according to the current position of the mouse """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: self.select_box.delete() self.select_box = None if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes.pop(self.selected) shape.delete() self.shape_drawing = False return 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: self.drawing_edge.move_dst(self.cursor) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) elif is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_tool.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), ) return if self.mode == GraphMode.EDGE: return # move selected objects if self.selection: 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()) else: if self.select_box and self.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) def press_delete(self, _event: tk.Event): """ delete selected nodes and any data that relates to it """ logging.debug("press delete key") if not self.app.core.is_runtime(): self.delete_selected_objects() else: logging.info("node deletion is disabled during runtime state") def double_click(self, event: tk.Event): selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] dialog = ShapeDialog(self.app, shape) dialog.show() def add_node(self, x: float, y: float) -> None: if self.selected is not None and self.selected not in self.shapes: return actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node(actual_x, actual_y, self.node_draw.node_type, self.node_draw.model) if not core_node: return try: self.node_draw.image = Images.get( self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)) except AttributeError: self.node_draw.image = Images.get_custom( self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)) node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node def width_and_height(self): """ retrieve canvas width and height in pixels """ x0, y0, x1, y1 = self.coords(self.rect) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h def get_wallpaper_image(self) -> Image.Image: width = int(self.wallpaper.width * self.ratio) height = int(self.wallpaper.height * self.ratio) image = self.wallpaper.resize((width, height), Image.ANTIALIAS) return image def draw_wallpaper(self, image: ImageTk.PhotoImage, x: float = None, y: float = None): if x is None and y is None: x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 y = (y1 + y2) / 2 self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image def wallpaper_upper_left(self): self.delete(self.wallpaper_id) # create new scaled image, cropped if needed width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = image.width cropy = image.height if image.width > width: cropx = image.width if image.height > height: cropy = image.height cropped = image.crop((0, 0, cropx, cropy)) image = ImageTk.PhotoImage(cropped) # draw on canvas x1, y1, _, _ = self.bbox(self.rect) x = (cropx / 2) + x1 y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) def wallpaper_center(self): """ place the image at the center of canvas """ self.delete(self.wallpaper_id) # dimension of the cropped image width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = 0 if image.width > width: cropx = (image.width - width) / 2 cropy = 0 if image.height > height: cropy = (image.height - height) / 2 x1 = 0 + cropx y1 = 0 + cropy x2 = image.width - cropx y2 = image.height - cropy cropped = image.crop((x1, y1, x2, y2)) image = ImageTk.PhotoImage(cropped) self.draw_wallpaper(image) def wallpaper_scaled(self): """ scale image based on canvas dimension """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image = ImageTk.PhotoImage(image) self.draw_wallpaper(image) def resize_to_wallpaper(self): self.delete(self.wallpaper_id) image = ImageTk.PhotoImage(self.wallpaper) self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) def redraw_canvas(self, dimensions: Tuple[int, int] = None): logging.info("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position logging.info("resetting scaling: %s %s", self.ratio, self.offset) factor = 1 / self.ratio self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.move(tk.ALL, -self.offset[0], -self.offset[1]) # reset ratio and offset self.ratio = 1.0 self.offset = (0, 0) # redraw canvas rectangle self.draw_canvas(dimensions) # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) self.draw_grid() self.app.canvas.show_grid.click_handler() def redraw_wallpaper(self): if self.adjust_to_dim.get(): logging.info("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) logging.info("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: self.wallpaper_center() elif option == ScaleOption.SCALED: self.wallpaper_scaled() elif option == ScaleOption.TILED: logging.warning("tiled background not implemented yet") # raise items above wallpaper for component in tags.ABOVE_WALLPAPER_TAGS: self.tag_raise(component) def set_wallpaper(self, filename: str): logging.debug("setting wallpaper: %s", filename) if filename: img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename self.redraw_wallpaper() else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) self.wallpaper = None self.wallpaper_file = None def is_selection_mode(self) -> bool: return self.mode == GraphMode.SELECT def create_edge(self, source: CanvasNode, dest: CanvasNode): """ create an edge between source node and destination node """ token = create_edge_token(source.id, dest.id) if token not in self.edges: pos = (source.core_node.position.x, source.core_node.position.y) edge = CanvasEdge(self, source.id, pos, pos) edge.complete(dest.id) self.edges[edge.token] = edge self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) self.core.create_link(edge, source, dest) def copy(self): if self.core.is_runtime(): logging.info("copy is disabled during runtime state") return if self.selection: logging.info("to copy nodes: %s", self.selection) self.to_copy.clear() for node_id in self.selection.keys(): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) def paste(self): if self.core.is_runtime(): logging.info("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over to_copy_edges = [] for canvas_node in self.to_copy: core_node = canvas_node.core_node actual_x = core_node.position.x + 50 actual_y = core_node.position.y + 50 scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y) copy = self.core.create_node(actual_x, actual_y, core_node.type, core_node.model) if not copy: continue node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) # copy configurations and services node.core_node.services[:] = canvas_node.core_node.services node.core_node.config_services[:] = canvas_node.core_node.config_services node.emane_model_configs = deepcopy( canvas_node.emane_model_configs) node.wlan_config = deepcopy(canvas_node.wlan_config) node.mobility_config = deepcopy(canvas_node.mobility_config) node.service_configs = deepcopy(canvas_node.service_configs) node.service_file_configs = deepcopy( canvas_node.service_file_configs) node.config_service_configs = deepcopy( canvas_node.config_service_configs) copy_map[canvas_node.id] = node.id self.core.canvas_nodes[copy.id] = node self.nodes[node.id] = node for edge in canvas_node.edges: if edge.src not in self.to_copy or edge.dst not in self.to_copy: if canvas_node.id == edge.src: dst_node = self.nodes[edge.dst] self.create_edge(node, dst_node) elif canvas_node.id == edge.dst: src_node = self.nodes[edge.src] self.create_edge(src_node, node) else: to_copy_edges.append(edge) # copy link and link config for edge in to_copy_edges: src_node_id = copy_map[edge.token[0]] dst_node_id = copy_map[edge.token[1]] src_node_copy = self.nodes[src_node_id] dst_node_copy = self.nodes[dst_node_id] self.create_edge(src_node_copy, dst_node_copy) token = create_edge_token(src_node_copy.id, dst_node_copy.id) copy_edge = self.edges[token] copy_link = copy_edge.link options = edge.link.options copy_link.options.CopyFrom(options) interface_one = None if copy_link.HasField("interface_one"): interface_one = copy_link.interface_one.id interface_two = None if copy_link.HasField("interface_two"): interface_two = copy_link.interface_two.id if not options.unidirectional: copy_edge.asymmetric_link = None else: asym_interface_one = None if interface_one: asym_interface_one = core_pb2.Interface(id=interface_one) asym_interface_two = None if interface_two: asym_interface_two = core_pb2.Interface(id=interface_two) copy_edge.asymmetric_link = core_pb2.Link( node_one_id=copy_link.node_two_id, node_two_id=copy_link.node_one_id, interface_one=asym_interface_one, interface_two=asym_interface_two, options=edge.asymmetric_link.options, ) self.itemconfig( copy_edge.id, width=self.itemcget(edge.id, "width"), fill=self.itemcget(edge.id, "fill"), ) self.tag_raise(tags.NODE) def scale_graph(self): for nid, canvas_node in self.nodes.items(): img = None if NodeUtils.is_custom(canvas_node.core_node.type, canvas_node.core_node.model): for custom_node in self.app.guiconfig.nodes: if custom_node.name == canvas_node.core_node.model: img = Images.get_custom( custom_node.image, int(ICON_SIZE * self.app.app_scale)) else: image_enum = TypeToImage.get(canvas_node.core_node.type, canvas_node.core_node.model) img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale)) self.itemconfig(nid, image=img) canvas_node.image = img canvas_node.scale_text() canvas_node.scale_antennas() for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))