2019-09-18 19:25:33 +01:00
|
|
|
import logging
|
2019-09-15 23:20:00 +01:00
|
|
|
import tkinter as tk
|
|
|
|
|
2019-12-04 21:40:35 +00:00
|
|
|
from PIL import Image, ImageTk
|
2019-11-14 20:58:27 +00:00
|
|
|
|
2019-10-05 00:52:07 +01:00
|
|
|
from core.api.grpc import core_pb2
|
2019-12-04 01:17:45 +00:00
|
|
|
from coretk.dialogs.shapemod import ShapeDialog
|
2019-12-09 22:13:21 +00:00
|
|
|
from coretk.graph import tags
|
2019-12-05 19:12:25 +00:00
|
|
|
from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge
|
|
|
|
from coretk.graph.enums import GraphMode, ScaleOption
|
|
|
|
from coretk.graph.linkinfo import LinkInfo, Throughput
|
|
|
|
from coretk.graph.node import CanvasNode
|
|
|
|
from coretk.graph.shape import Shape
|
2019-12-07 06:10:27 +00:00
|
|
|
from coretk.graph.shapeutils import is_draw_shape
|
2019-12-06 22:01:03 +00:00
|
|
|
from coretk.images import Images
|
2019-11-16 07:31:41 +00:00
|
|
|
from coretk.nodeutils import NodeUtils
|
2019-10-04 00:50:49 +01:00
|
|
|
|
2019-12-10 22:33:52 +00:00
|
|
|
SCROLL_BUFFER = 25
|
|
|
|
ZOOM_IN = 1.1
|
|
|
|
ZOOM_OUT = 0.9
|
|
|
|
|
2019-09-15 23:20:00 +01:00
|
|
|
|
|
|
|
class CanvasGraph(tk.Canvas):
|
2019-12-10 22:33:52 +00:00
|
|
|
def __init__(self, master, core, width, height):
|
|
|
|
super().__init__(
|
|
|
|
master,
|
|
|
|
highlightthickness=0,
|
|
|
|
background="#cccccc",
|
|
|
|
scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER),
|
|
|
|
)
|
2019-11-27 21:15:04 +00:00
|
|
|
self.app = master
|
2019-12-10 22:33:52 +00:00
|
|
|
self.core = core
|
2019-09-15 23:20:00 +01:00
|
|
|
self.mode = GraphMode.SELECT
|
2019-11-28 00:39:48 +00:00
|
|
|
self.annotation_type = None
|
2019-12-05 21:13:35 +00:00
|
|
|
self.selection = {}
|
2019-09-15 23:20:00 +01:00
|
|
|
self.selected = None
|
2019-11-21 07:16:04 +00:00
|
|
|
self.node_draw = None
|
|
|
|
self.context = None
|
2019-09-15 23:20:00 +01:00
|
|
|
self.nodes = {}
|
|
|
|
self.edges = {}
|
2019-11-28 00:39:48 +00:00
|
|
|
self.shapes = {}
|
2019-11-27 23:40:54 +00:00
|
|
|
self.wireless_edges = {}
|
2019-09-15 23:20:00 +01:00
|
|
|
self.drawing_edge = None
|
2019-10-25 00:50:24 +01:00
|
|
|
self.grid = None
|
2019-11-01 20:42:49 +00:00
|
|
|
self.throughput_draw = Throughput(self, core)
|
2019-12-03 00:05:10 +00:00
|
|
|
self.shape_drawing = False
|
2019-12-09 20:07:42 +00:00
|
|
|
self.default_width = width
|
|
|
|
self.default_height = height
|
2019-12-10 22:33:52 +00:00
|
|
|
self.ratio = 1.0
|
|
|
|
self.offset = (0, 0)
|
|
|
|
self.cursor = (0, 0)
|
2019-09-30 18:11:29 +01:00
|
|
|
|
2019-11-14 19:26:20 +00:00
|
|
|
# background related
|
|
|
|
self.wallpaper_id = None
|
|
|
|
self.wallpaper = None
|
|
|
|
self.wallpaper_drawn = None
|
|
|
|
self.wallpaper_file = ""
|
|
|
|
self.scale_option = tk.IntVar(value=1)
|
2019-11-14 20:58:27 +00:00
|
|
|
self.show_grid = tk.BooleanVar(value=True)
|
|
|
|
self.adjust_to_dim = tk.BooleanVar(value=False)
|
2019-11-14 19:26:20 +00:00
|
|
|
|
2019-12-10 22:33:52 +00:00
|
|
|
# bindings
|
|
|
|
self.setup_bindings()
|
|
|
|
|
2019-12-09 20:07:42 +00:00
|
|
|
# draw base canvas
|
|
|
|
self.draw_canvas()
|
|
|
|
self.draw_grid()
|
|
|
|
|
|
|
|
def draw_canvas(self):
|
|
|
|
self.grid = self.create_rectangle(
|
|
|
|
0,
|
|
|
|
0,
|
|
|
|
self.default_width,
|
|
|
|
self.default_height,
|
|
|
|
outline="#000000",
|
|
|
|
fill="#ffffff",
|
|
|
|
width=1,
|
|
|
|
tags="rectangle",
|
|
|
|
)
|
|
|
|
|
2019-11-15 20:39:08 +00:00
|
|
|
def reset_and_redraw(self, session):
|
2019-10-19 00:42:00 +01:00
|
|
|
"""
|
2019-11-01 17:45:47 +00:00
|
|
|
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
|
|
|
|
client.
|
|
|
|
|
2019-11-01 18:34:38 +00:00
|
|
|
:param core.api.grpc.core_pb2.Session session: session to draw
|
|
|
|
:return: nothing
|
2019-10-19 00:42:00 +01:00
|
|
|
"""
|
|
|
|
# delete any existing drawn items
|
2019-12-09 22:13:21 +00:00
|
|
|
for tag in tags.COMPONENT_TAGS:
|
2019-12-06 00:37:48 +00:00
|
|
|
self.delete(tag)
|
2019-10-19 00:42:00 +01:00
|
|
|
|
|
|
|
# set the private variables to default value
|
|
|
|
self.mode = GraphMode.SELECT
|
2019-12-09 16:53:54 +00:00
|
|
|
self.annotation_type = None
|
2019-11-16 07:31:41 +00:00
|
|
|
self.node_draw = None
|
2019-10-19 00:42:00 +01:00
|
|
|
self.selected = None
|
2019-11-04 06:58:45 +00:00
|
|
|
self.nodes.clear()
|
|
|
|
self.edges.clear()
|
2019-12-05 18:12:31 +00:00
|
|
|
self.shapes.clear()
|
2019-11-27 23:40:54 +00:00
|
|
|
self.wireless_edges.clear()
|
2019-10-19 00:42:00 +01:00
|
|
|
self.drawing_edge = None
|
2019-11-15 18:22:30 +00:00
|
|
|
self.draw_session(session)
|
2019-10-19 00:42:00 +01:00
|
|
|
|
2019-10-04 00:50:49 +01:00
|
|
|
def setup_bindings(self):
|
|
|
|
"""
|
|
|
|
Bind any mouse events or hot keys to the matching action
|
2019-10-02 00:25:26 +01:00
|
|
|
|
2019-10-04 00:50:49 +01:00
|
|
|
:return: nothing
|
|
|
|
"""
|
|
|
|
self.bind("<ButtonPress-1>", self.click_press)
|
|
|
|
self.bind("<ButtonRelease-1>", self.click_release)
|
|
|
|
self.bind("<B1-Motion>", self.click_motion)
|
2019-12-10 22:33:52 +00:00
|
|
|
self.bind("<ButtonRelease-3>", self.click_context)
|
2019-11-07 21:23:02 +00:00
|
|
|
self.bind("<Delete>", self.press_delete)
|
2019-12-03 00:05:10 +00:00
|
|
|
self.bind("<Control-1>", self.ctrl_click)
|
2019-12-04 00:18:00 +00:00
|
|
|
self.bind("<Double-Button-1>", self.double_click)
|
2019-12-10 22:33:52 +00:00
|
|
|
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))
|
2019-10-02 00:25:26 +01:00
|
|
|
|
2019-12-09 20:07:42 +00:00
|
|
|
def draw_grid(self):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
2019-12-10 22:33:52 +00:00
|
|
|
Create grid.
|
2019-09-30 18:11:29 +01:00
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-12-09 20:07:42 +00:00
|
|
|
width, height = self.width_and_height()
|
|
|
|
width = int(width)
|
|
|
|
height = int(height)
|
2019-09-30 18:11:29 +01:00
|
|
|
for i in range(0, width, 27):
|
2019-12-09 22:13:21 +00:00
|
|
|
self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE)
|
2019-09-30 18:11:29 +01:00
|
|
|
for i in range(0, height, 27):
|
2019-12-09 22:13:21 +00:00
|
|
|
self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
|
|
|
|
self.tag_lower(tags.GRIDLINE)
|
2019-11-14 20:58:27 +00:00
|
|
|
self.tag_lower(self.grid)
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-11-27 23:40:54 +00:00
|
|
|
def add_wireless_edge(self, src, dst):
|
|
|
|
token = tuple(sorted((src.id, dst.id)))
|
|
|
|
x1, y1 = self.coords(src.id)
|
|
|
|
x2, y2 = self.coords(dst.id)
|
|
|
|
position = (x1, y1, x2, y2)
|
|
|
|
edge = CanvasWirelessEdge(token, position, src.id, dst.id, self)
|
|
|
|
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)
|
|
|
|
|
|
|
|
def delete_wireless_edge(self, src, dst):
|
|
|
|
token = tuple(sorted((src.id, dst.id)))
|
|
|
|
edge = self.wireless_edges.pop(token)
|
|
|
|
edge.delete()
|
|
|
|
src.wireless_edges.remove(edge)
|
|
|
|
dst.wireless_edges.remove(edge)
|
|
|
|
|
2019-11-15 18:22:30 +00:00
|
|
|
def draw_session(self, session):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
2019-11-15 18:22:30 +00:00
|
|
|
Draw existing session.
|
2019-09-30 18:11:29 +01:00
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-11-15 18:22:30 +00:00
|
|
|
# draw existing nodes
|
|
|
|
for core_node in session.nodes:
|
2019-10-05 00:52:07 +01:00
|
|
|
# peer to peer node is not drawn on the GUI
|
2019-11-25 23:40:09 +00:00
|
|
|
if NodeUtils.is_ignore_node(core_node.type):
|
2019-11-15 18:22:30 +00:00
|
|
|
continue
|
2019-10-29 16:04:16 +00:00
|
|
|
|
2019-11-15 18:22:30 +00:00
|
|
|
# draw nodes on the canvas
|
2019-11-16 07:31:41 +00:00
|
|
|
image = NodeUtils.node_icon(core_node.type, core_node.model)
|
2019-11-20 18:59:30 +00:00
|
|
|
node = CanvasNode(self.master, core_node, image)
|
2019-11-15 18:22:30 +00:00
|
|
|
self.nodes[node.id] = node
|
|
|
|
self.core.canvas_nodes[core_node.id] = node
|
2019-10-11 01:02:28 +01:00
|
|
|
|
2019-10-05 00:52:07 +01:00
|
|
|
# draw existing links
|
|
|
|
for link in session.links:
|
2019-11-15 18:22:30 +00:00
|
|
|
canvas_node_one = self.core.canvas_nodes[link.node_one_id]
|
2019-11-20 18:59:30 +00:00
|
|
|
node_one = canvas_node_one.core_node
|
2019-11-15 18:22:30 +00:00
|
|
|
canvas_node_two = self.core.canvas_nodes[link.node_two_id]
|
2019-11-20 18:59:30 +00:00
|
|
|
node_two = canvas_node_two.core_node
|
2019-11-22 00:41:38 +00:00
|
|
|
if link.type == core_pb2.LinkType.WIRELESS:
|
2019-11-27 23:40:54 +00:00
|
|
|
self.add_wireless_edge(canvas_node_one, canvas_node_two)
|
2019-11-22 00:41:38 +00:00
|
|
|
else:
|
|
|
|
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 = tuple(sorted((canvas_node_one.id, canvas_node_two.id)))
|
|
|
|
edge.dst = canvas_node_two.id
|
2019-12-06 00:37:48 +00:00
|
|
|
edge.check_wireless()
|
2019-11-22 00:41:38 +00:00
|
|
|
canvas_node_one.edges.add(edge)
|
|
|
|
canvas_node_two.edges.add(edge)
|
|
|
|
self.edges[edge.token] = edge
|
|
|
|
self.core.links[edge.token] = link
|
2019-11-25 22:52:00 +00:00
|
|
|
edge.link_info = LinkInfo(self, edge, link)
|
|
|
|
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)
|
2019-10-11 01:02:28 +01:00
|
|
|
|
2019-11-14 20:58:27 +00:00
|
|
|
# raise the nodes so they on top of the links
|
2019-12-09 22:13:21 +00:00
|
|
|
self.tag_raise(tags.NODE)
|
2019-09-15 23:20:00 +01:00
|
|
|
|
|
|
|
def canvas_xy(self, event):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
|
|
|
Convert window coordinate to canvas coordinate
|
|
|
|
|
|
|
|
:param event:
|
|
|
|
:rtype: (int, int)
|
|
|
|
:return: x, y canvas coordinate
|
|
|
|
"""
|
2019-09-15 23:20:00 +01:00
|
|
|
x = self.canvasx(event.x)
|
|
|
|
y = self.canvasy(event.y)
|
|
|
|
return x, y
|
|
|
|
|
|
|
|
def get_selected(self, event):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
|
|
|
Retrieve the item id that is on the mouse position
|
|
|
|
|
|
|
|
:param event: mouse event
|
|
|
|
:rtype: int
|
|
|
|
:return: the item that the mouse point to
|
|
|
|
"""
|
2019-12-10 22:33:52 +00:00
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
overlapping = self.find_overlapping(x, y, x, y)
|
2019-09-15 23:20:00 +01:00
|
|
|
selected = None
|
|
|
|
for _id in overlapping:
|
|
|
|
if self.drawing_edge and self.drawing_edge.id == _id:
|
|
|
|
continue
|
|
|
|
|
2019-11-27 22:25:29 +00:00
|
|
|
if _id in self.nodes:
|
2019-09-15 23:20:00 +01:00
|
|
|
selected = _id
|
|
|
|
break
|
|
|
|
|
2019-12-03 00:05:10 +00:00
|
|
|
if _id in self.shapes:
|
|
|
|
selected = _id
|
|
|
|
|
2019-09-15 23:20:00 +01:00
|
|
|
return selected
|
|
|
|
|
|
|
|
def click_release(self, event):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
|
|
|
Draw a node or finish drawing an edge according to the current graph mode
|
|
|
|
|
|
|
|
:param event: mouse event
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-11-21 07:16:04 +00:00
|
|
|
if self.context:
|
|
|
|
self.context.unpost()
|
|
|
|
self.context = None
|
2019-10-30 20:33:22 +00:00
|
|
|
else:
|
2019-11-28 00:39:48 +00:00
|
|
|
if self.mode == GraphMode.ANNOTATION:
|
2019-12-07 06:10:27 +00:00
|
|
|
self.focus_set()
|
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
if self.shape_drawing:
|
|
|
|
shape = self.shapes[self.selected]
|
|
|
|
shape.shape_complete(x, y)
|
|
|
|
self.shape_drawing = False
|
2019-11-28 00:39:48 +00:00
|
|
|
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:
|
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
self.add_node(x, y)
|
|
|
|
elif self.mode == GraphMode.PICKNODE:
|
|
|
|
self.mode = GraphMode.NODE
|
2019-12-04 00:18:00 +00:00
|
|
|
self.selected = None
|
2019-09-15 23:20:00 +01:00
|
|
|
|
|
|
|
def handle_edge_release(self, event):
|
|
|
|
edge = self.drawing_edge
|
|
|
|
self.drawing_edge = None
|
|
|
|
|
|
|
|
# not drawing edge return
|
|
|
|
if edge is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
# edge dst must be a node
|
2019-09-18 19:25:33 +01:00
|
|
|
logging.debug(f"current selected: {self.selected}")
|
2019-11-22 20:51:58 +00:00
|
|
|
dst_node = self.nodes.get(self.selected)
|
|
|
|
if not dst_node:
|
2019-09-15 23:20:00 +01:00
|
|
|
edge.delete()
|
|
|
|
return
|
|
|
|
|
|
|
|
# edge dst is same as src, delete edge
|
|
|
|
if edge.src == self.selected:
|
|
|
|
edge.delete()
|
2019-11-15 18:22:30 +00:00
|
|
|
return
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-12-06 00:37:48 +00:00
|
|
|
# ignore repeated edges
|
|
|
|
token = tuple(sorted((edge.src, self.selected)))
|
|
|
|
if token in self.edges:
|
2019-09-15 23:20:00 +01:00
|
|
|
edge.delete()
|
2019-12-06 00:37:48 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
# set dst node and snap edge to center
|
|
|
|
edge.complete(self.selected)
|
|
|
|
logging.debug("drawing edge token: %s", edge.token)
|
|
|
|
|
|
|
|
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)
|
|
|
|
link = self.core.create_link(edge, node_src, node_dst)
|
|
|
|
edge.link_info = LinkInfo(self, edge, link)
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-12-05 21:13:35 +00:00
|
|
|
def select_object(self, object_id, choose_multiple=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="-",
|
2019-12-09 22:13:21 +00:00
|
|
|
tags=tags.SELECTION,
|
2019-12-05 21:13:35 +00:00
|
|
|
)
|
|
|
|
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.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
|
|
|
for _id in self.selection.values():
|
|
|
|
self.delete(_id)
|
|
|
|
self.selection.clear()
|
|
|
|
|
2019-12-10 22:33:52 +00:00
|
|
|
def move_selection(self, object_id, x_offset, y_offset):
|
2019-12-05 21:13:35 +00:00
|
|
|
select_id = self.selection.get(object_id)
|
|
|
|
if select_id is not None:
|
2019-12-10 22:33:52 +00:00
|
|
|
self.move(select_id, x_offset, y_offset)
|
2019-12-05 21:13:35 +00:00
|
|
|
|
|
|
|
def delete_selection_objects(self):
|
|
|
|
edges = set()
|
|
|
|
nodes = []
|
|
|
|
for object_id in self.selection:
|
2019-12-06 01:01:48 +00:00
|
|
|
# delete selection box
|
|
|
|
selection_id = self.selection[object_id]
|
|
|
|
self.delete(selection_id)
|
|
|
|
|
|
|
|
# delete node and related edges
|
2019-12-05 21:13:35 +00:00
|
|
|
if object_id in self.nodes:
|
|
|
|
canvas_node = self.nodes.pop(object_id)
|
2019-12-06 01:01:48 +00:00
|
|
|
canvas_node.delete()
|
2019-12-05 21:13:35 +00:00
|
|
|
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)
|
2019-12-06 20:46:00 +00:00
|
|
|
self.throughput_draw.delete(edge)
|
2019-12-06 01:01:48 +00:00
|
|
|
del self.edges[edge.token]
|
|
|
|
edge.delete()
|
|
|
|
|
|
|
|
# update node connected to edge being deleted
|
2019-12-05 21:13:35 +00:00
|
|
|
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)
|
|
|
|
try:
|
|
|
|
other_node.interfaces.remove(other_interface)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
if is_wireless:
|
2019-12-06 00:37:48 +00:00
|
|
|
other_node.delete_antenna()
|
2019-12-06 01:01:48 +00:00
|
|
|
|
|
|
|
# delete shape
|
2019-12-05 21:13:35 +00:00
|
|
|
if object_id in self.shapes:
|
2019-12-06 01:01:48 +00:00
|
|
|
shape = self.shapes.pop(object_id)
|
|
|
|
shape.delete()
|
2019-12-05 21:13:35 +00:00
|
|
|
|
|
|
|
self.selection.clear()
|
|
|
|
return nodes
|
|
|
|
|
2019-12-10 22:33:52 +00:00
|
|
|
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)
|
|
|
|
|
2019-09-15 23:20:00 +01:00
|
|
|
def click_press(self, event):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
|
|
|
Start drawing an edge if mouse click is on a node
|
|
|
|
|
|
|
|
:param event: mouse event
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-12-10 22:33:52 +00:00
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
self.cursor = x, y
|
2019-09-15 23:20:00 +01:00
|
|
|
selected = self.get_selected(event)
|
2019-12-10 22:33:52 +00:00
|
|
|
logging.debug(f"click press: %s", selected)
|
2019-12-05 21:13:35 +00:00
|
|
|
is_node = selected in self.nodes
|
2019-09-15 23:20:00 +01:00
|
|
|
if self.mode == GraphMode.EDGE and is_node:
|
|
|
|
x, y = self.coords(selected)
|
|
|
|
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
|
2019-12-06 17:03:21 +00:00
|
|
|
|
|
|
|
if self.mode == GraphMode.ANNOTATION and selected is None:
|
2019-12-07 06:10:27 +00:00
|
|
|
shape = Shape(self.app, self, self.annotation_type, x, y)
|
|
|
|
self.selected = shape.id
|
|
|
|
self.shape_drawing = True
|
|
|
|
self.shapes[shape.id] = shape
|
2019-12-06 17:03:21 +00:00
|
|
|
|
2019-12-10 22:33:52 +00:00
|
|
|
if selected is not None:
|
|
|
|
if selected not in self.selection:
|
2019-12-05 21:13:35 +00:00
|
|
|
if selected in self.shapes:
|
|
|
|
shape = self.shapes[selected]
|
2019-12-10 22:33:52 +00:00
|
|
|
self.select_object(shape.id)
|
2019-12-04 23:08:05 +00:00
|
|
|
self.selected = selected
|
2019-12-10 22:33:52 +00:00
|
|
|
elif selected in self.nodes:
|
|
|
|
node = self.nodes[selected]
|
|
|
|
self.select_object(node.id)
|
|
|
|
self.selected = selected
|
|
|
|
else:
|
|
|
|
self.clear_selection()
|
2019-12-03 00:05:10 +00:00
|
|
|
|
|
|
|
def ctrl_click(self, event):
|
2019-12-10 22:33:52 +00:00
|
|
|
# update cursor location
|
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
self.cursor = x, y
|
|
|
|
|
|
|
|
# handle multiple selections
|
2019-12-05 21:13:35 +00:00
|
|
|
logging.debug("control left click: %s", event)
|
2019-12-03 00:05:10 +00:00
|
|
|
selected = self.get_selected(event)
|
2019-12-04 23:12:31 +00:00
|
|
|
if (
|
2019-12-10 22:33:52 +00:00
|
|
|
selected not in self.selection
|
2019-12-05 21:13:35 +00:00
|
|
|
and selected in self.shapes
|
2019-12-10 22:33:52 +00:00
|
|
|
or selected in self.nodes
|
2019-12-04 23:12:31 +00:00
|
|
|
):
|
2019-12-05 21:13:35 +00:00
|
|
|
self.select_object(selected, choose_multiple=True)
|
2019-09-15 23:20:00 +01:00
|
|
|
|
|
|
|
def click_motion(self, event):
|
2019-09-30 18:11:29 +01:00
|
|
|
"""
|
|
|
|
Redraw drawing edge according to the current position of the mouse
|
|
|
|
|
|
|
|
:param event: mouse event
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-12-10 22:33:52 +00:00
|
|
|
x, y = self.canvas_xy(event)
|
|
|
|
x_offset = x - self.cursor[0]
|
|
|
|
y_offset = y - self.cursor[1]
|
|
|
|
self.cursor = x, y
|
|
|
|
|
2019-09-15 23:20:00 +01:00
|
|
|
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
|
|
|
|
x1, y1, _, _ = self.coords(self.drawing_edge.id)
|
2019-12-10 22:33:52 +00:00
|
|
|
self.coords(self.drawing_edge.id, x1, y1, x, y)
|
2019-11-28 00:39:48 +00:00
|
|
|
if self.mode == GraphMode.ANNOTATION:
|
2019-12-06 22:01:03 +00:00
|
|
|
if is_draw_shape(self.annotation_type) and self.shape_drawing:
|
2019-12-07 06:10:27 +00:00
|
|
|
shape = self.shapes[self.selected]
|
|
|
|
shape.shape_motion(x, y)
|
2019-12-10 22:33:52 +00:00
|
|
|
|
|
|
|
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())
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-11-21 07:16:04 +00:00
|
|
|
def click_context(self, event):
|
|
|
|
logging.info("context event: %s", self.context)
|
|
|
|
if not self.context:
|
2019-10-30 20:33:22 +00:00
|
|
|
selected = self.get_selected(event)
|
2019-11-21 07:16:04 +00:00
|
|
|
canvas_node = self.nodes.get(selected)
|
|
|
|
if canvas_node:
|
2019-10-30 20:33:22 +00:00
|
|
|
logging.debug(f"node context: {selected}")
|
2019-12-06 17:10:50 +00:00
|
|
|
self.context = canvas_node.create_context()
|
2019-11-21 07:16:04 +00:00
|
|
|
self.context.post(event.x_root, event.y_root)
|
2019-10-30 20:33:22 +00:00
|
|
|
else:
|
2019-11-21 07:16:04 +00:00
|
|
|
self.context.unpost()
|
|
|
|
self.context = None
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-11-07 21:23:02 +00:00
|
|
|
def press_delete(self, event):
|
2019-11-07 23:19:01 +00:00
|
|
|
"""
|
|
|
|
delete selected nodes and any data that relates to it
|
|
|
|
:param event:
|
|
|
|
:return:
|
|
|
|
"""
|
2019-12-03 00:05:10 +00:00
|
|
|
logging.debug("press delete key")
|
2019-12-05 21:13:35 +00:00
|
|
|
nodes = self.delete_selection_objects()
|
2019-11-23 07:48:10 +00:00
|
|
|
self.core.delete_graph_nodes(nodes)
|
2019-11-07 21:23:02 +00:00
|
|
|
|
2019-12-04 00:18:00 +00:00
|
|
|
def double_click(self, event):
|
|
|
|
selected = self.get_selected(event)
|
2019-12-05 21:13:35 +00:00
|
|
|
if selected is not None and selected in self.shapes:
|
|
|
|
shape = self.shapes[selected]
|
|
|
|
dialog = ShapeDialog(self.app, self.app, shape)
|
|
|
|
dialog.show()
|
2019-12-04 00:18:00 +00:00
|
|
|
|
2019-11-16 07:31:41 +00:00
|
|
|
def add_node(self, x, y):
|
2019-12-05 21:13:35 +00:00
|
|
|
if self.selected is None or self.selected in self.shapes:
|
2019-11-16 07:31:41 +00:00
|
|
|
core_node = self.core.create_node(
|
|
|
|
int(x), int(y), self.node_draw.node_type, self.node_draw.model
|
|
|
|
)
|
2019-11-20 18:59:30 +00:00
|
|
|
node = CanvasNode(self.master, core_node, self.node_draw.image)
|
2019-11-15 18:22:30 +00:00
|
|
|
self.core.canvas_nodes[core_node.id] = node
|
2019-10-11 01:02:28 +01:00
|
|
|
self.nodes[node.id] = node
|
|
|
|
return node
|
2019-09-15 23:20:00 +01:00
|
|
|
|
2019-11-14 20:58:27 +00:00
|
|
|
def width_and_height(self):
|
|
|
|
"""
|
|
|
|
retrieve canvas width and height in pixels
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-12-05 21:17:12 +00:00
|
|
|
x0, y0, x1, y1 = self.coords(self.grid)
|
2019-11-14 20:58:27 +00:00
|
|
|
canvas_w = abs(x0 - x1)
|
|
|
|
canvas_h = abs(y0 - y1)
|
|
|
|
return canvas_w, canvas_h
|
|
|
|
|
|
|
|
def wallpaper_upper_left(self):
|
|
|
|
tk_img = ImageTk.PhotoImage(self.wallpaper)
|
|
|
|
# crop image if it is bigger than canvas
|
|
|
|
canvas_w, canvas_h = self.width_and_height()
|
|
|
|
cropx = img_w = tk_img.width()
|
|
|
|
cropy = img_h = tk_img.height()
|
|
|
|
if img_w > canvas_w:
|
|
|
|
cropx -= img_w - canvas_w
|
|
|
|
if img_h > canvas_h:
|
|
|
|
cropy -= img_h - canvas_h
|
|
|
|
cropped = self.wallpaper.crop((0, 0, cropx, cropy))
|
|
|
|
cropped_tk = ImageTk.PhotoImage(cropped)
|
|
|
|
self.delete(self.wallpaper_id)
|
|
|
|
# place left corner of image to the left corner of the canvas
|
|
|
|
self.wallpaper_id = self.create_image(
|
2019-12-09 22:13:21 +00:00
|
|
|
(cropx / 2, cropy / 2), image=cropped_tk, tags=tags.WALLPAPER
|
2019-11-14 20:58:27 +00:00
|
|
|
)
|
|
|
|
self.wallpaper_drawn = cropped_tk
|
|
|
|
|
|
|
|
def wallpaper_center(self):
|
|
|
|
"""
|
|
|
|
place the image at the center of canvas
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
|
|
|
tk_img = ImageTk.PhotoImage(self.wallpaper)
|
|
|
|
canvas_w, canvas_h = self.width_and_height()
|
|
|
|
cropx = img_w = tk_img.width()
|
|
|
|
cropy = img_h = tk_img.height()
|
|
|
|
# dimension of the cropped image
|
|
|
|
if img_w > canvas_w:
|
|
|
|
cropx -= img_w - canvas_w
|
|
|
|
if img_h > canvas_h:
|
|
|
|
cropy -= img_h - canvas_h
|
|
|
|
x0 = (img_w - cropx) / 2
|
|
|
|
y0 = (img_h - cropy) / 2
|
|
|
|
x1 = x0 + cropx
|
|
|
|
y1 = y0 + cropy
|
|
|
|
cropped = self.wallpaper.crop((x0, y0, x1, y1))
|
|
|
|
cropped_tk = ImageTk.PhotoImage(cropped)
|
|
|
|
# place the center of the image at the center of the canvas
|
|
|
|
self.delete(self.wallpaper_id)
|
|
|
|
self.wallpaper_id = self.create_image(
|
2019-12-09 22:13:21 +00:00
|
|
|
(canvas_w / 2, canvas_h / 2), image=cropped_tk, tags=tags.WALLPAPER
|
2019-11-14 20:58:27 +00:00
|
|
|
)
|
|
|
|
self.wallpaper_drawn = cropped_tk
|
|
|
|
|
|
|
|
def wallpaper_scaled(self):
|
|
|
|
"""
|
|
|
|
scale image based on canvas dimension
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
|
|
|
canvas_w, canvas_h = self.width_and_height()
|
|
|
|
image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h))
|
|
|
|
self.delete(self.wallpaper_id)
|
|
|
|
self.wallpaper_id = self.create_image(
|
2019-12-09 22:13:21 +00:00
|
|
|
(canvas_w / 2, canvas_h / 2), image=image, tags=tags.WALLPAPER
|
2019-11-14 20:58:27 +00:00
|
|
|
)
|
|
|
|
self.wallpaper_drawn = image
|
|
|
|
|
|
|
|
def resize_to_wallpaper(self):
|
|
|
|
image_tk = ImageTk.PhotoImage(self.wallpaper)
|
|
|
|
img_w = image_tk.width()
|
|
|
|
img_h = image_tk.height()
|
|
|
|
self.delete(self.wallpaper_id)
|
2019-12-09 20:07:42 +00:00
|
|
|
self.redraw_canvas(img_w, img_h)
|
2019-11-14 20:58:27 +00:00
|
|
|
self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk)
|
|
|
|
self.wallpaper_drawn = image_tk
|
|
|
|
|
2019-12-09 20:07:42 +00:00
|
|
|
def redraw_canvas(self, width, height):
|
2019-11-14 20:58:27 +00:00
|
|
|
"""
|
|
|
|
redraw grid with new dimension
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2019-12-09 20:07:42 +00:00
|
|
|
# resize canvas and scrollregion
|
2019-12-10 22:33:52 +00:00
|
|
|
self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER))
|
2019-12-09 20:07:42 +00:00
|
|
|
self.coords(self.grid, 0, 0, width, height)
|
2019-11-14 20:58:27 +00:00
|
|
|
|
2019-12-09 20:07:42 +00:00
|
|
|
# redraw gridlines to new canvas size
|
2019-12-09 22:13:21 +00:00
|
|
|
self.delete(tags.GRIDLINE)
|
2019-12-09 20:07:42 +00:00
|
|
|
self.draw_grid()
|
2019-11-14 20:58:27 +00:00
|
|
|
self.update_grid()
|
|
|
|
|
|
|
|
def redraw(self):
|
|
|
|
if self.adjust_to_dim.get():
|
|
|
|
self.resize_to_wallpaper()
|
|
|
|
else:
|
|
|
|
option = ScaleOption(self.scale_option.get())
|
2019-12-09 20:07:42 +00:00
|
|
|
logging.info("canvas scale option: %s", option)
|
2019-11-14 20:58:27 +00:00
|
|
|
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")
|
|
|
|
|
2019-12-09 20:07:42 +00:00
|
|
|
# raise items above wallpaper
|
2019-12-09 22:13:21 +00:00
|
|
|
for component in tags.ABOVE_WALLPAPER_TAGS:
|
2019-12-09 20:07:42 +00:00
|
|
|
self.tag_raise(component)
|
|
|
|
|
2019-11-14 20:58:27 +00:00
|
|
|
def update_grid(self):
|
|
|
|
logging.info("updating grid show: %s", self.show_grid.get())
|
|
|
|
if self.show_grid.get():
|
2019-12-09 22:13:21 +00:00
|
|
|
self.itemconfig(tags.GRIDLINE, state=tk.NORMAL)
|
2019-11-14 20:58:27 +00:00
|
|
|
else:
|
2019-12-09 22:13:21 +00:00
|
|
|
self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN)
|
2019-11-14 20:58:27 +00:00
|
|
|
|
2019-12-04 21:40:35 +00:00
|
|
|
def set_wallpaper(self, filename):
|
|
|
|
logging.info("setting wallpaper: %s", filename)
|
|
|
|
if filename is not None:
|
|
|
|
img = Image.open(filename)
|
|
|
|
self.wallpaper = img
|
|
|
|
self.wallpaper_file = filename
|
|
|
|
self.redraw()
|
|
|
|
else:
|
|
|
|
if self.wallpaper_id is not None:
|
|
|
|
self.delete(self.wallpaper_id)
|
|
|
|
self.wallpaper = None
|
|
|
|
self.wallpaper_file = None
|
|
|
|
|
2019-12-03 00:05:10 +00:00
|
|
|
def is_selection_mode(self):
|
|
|
|
return self.mode == GraphMode.SELECT
|