updates to handling movement for nodes/shapes on canvas, added initial canvas zoom/panning, fixed some issues with mobility player event handling when dialog is closed

This commit is contained in:
Blake Harnden 2019-12-10 14:33:52 -08:00
parent 17c78d59cd
commit 51163a30d3
6 changed files with 128 additions and 148 deletions

View file

@ -69,14 +69,7 @@ class Application(tk.Frame):
def draw_canvas(self): def draw_canvas(self):
width = self.guiconfig["preferences"]["width"] width = self.guiconfig["preferences"]["width"]
height = self.guiconfig["preferences"]["height"] height = self.guiconfig["preferences"]["height"]
self.canvas = CanvasGraph( self.canvas = CanvasGraph(self, self.core, width, height)
self,
self.core,
width,
height,
background="#cccccc",
scrollregion=(0, 0, 1200, 1000),
)
self.canvas.pack(fill=tk.BOTH, expand=True) self.canvas.pack(fill=tk.BOTH, expand=True)
scroll_x = ttk.Scrollbar( scroll_x = ttk.Scrollbar(
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview

View file

@ -177,7 +177,7 @@ class CoreClient:
x = event.node.position.x x = event.node.position.x
y = event.node.position.y y = event.node.position.y
canvas_node = self.canvas_nodes[node_id] canvas_node = self.canvas_nodes[node_id]
canvas_node.move(x, y, update=False) canvas_node.move(x, y)
def handle_throughputs(self, event): def handle_throughputs(self, event):
# interface_throughputs = event.interface_throughputs # interface_throughputs = event.interface_throughputs
@ -429,10 +429,11 @@ class CoreClient:
show_grpc_error(e) show_grpc_error(e)
self.app.close() self.app.close()
def edit_node(self, node_id, x, y): def edit_node(self, core_node):
position = core_pb2.Position(x=x, y=y)
try: try:
self.client.edit_node(self.session_id, node_id, position, source="gui") self.client.edit_node(
self.session_id, core_node.id, core_node.position, source="gui"
)
except grpc.RpcError as e: except grpc.RpcError as e:
show_grpc_error(e) show_grpc_error(e)

View file

@ -27,6 +27,7 @@ class MobilityPlayer:
self.dialog = MobilityPlayerDialog( self.dialog = MobilityPlayerDialog(
self.master, self.app, self.canvas_node, self.config self.master, self.app, self.canvas_node, self.config
) )
self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close)
if self.state == MobilityAction.START: if self.state == MobilityAction.START:
self.set_play() self.set_play()
elif self.state == MobilityAction.PAUSE: elif self.state == MobilityAction.PAUSE:
@ -35,17 +36,24 @@ class MobilityPlayer:
self.set_stop() self.set_stop()
self.dialog.show() self.dialog.show()
def handle_close(self):
self.dialog.destroy()
self.dialog = None
def set_play(self): def set_play(self):
self.dialog.set_play()
self.state = MobilityAction.START self.state = MobilityAction.START
if self.dialog:
self.dialog.set_play()
def set_pause(self): def set_pause(self):
self.dialog.set_pause()
self.state = MobilityAction.PAUSE self.state = MobilityAction.PAUSE
if self.dialog:
self.dialog.set_pause()
def set_stop(self): def set_stop(self):
self.dialog.set_stop()
self.state = MobilityAction.STOP self.state = MobilityAction.STOP
if self.dialog:
self.dialog.set_stop()
class MobilityPlayerDialog(Dialog): class MobilityPlayerDialog(Dialog):
@ -92,7 +100,6 @@ class MobilityPlayerDialog(Dialog):
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD)
self.stop_button.state(["pressed"])
loop = tk.IntVar(value=int(self.config["loop"].value == "1")) loop = tk.IntVar(value=int(self.config["loop"].value == "1"))
checkbutton = ttk.Checkbutton( checkbutton = ttk.Checkbutton(

View file

@ -15,14 +15,21 @@ from coretk.graph.shapeutils import is_draw_shape
from coretk.images import Images from coretk.images import Images
from coretk.nodeutils import NodeUtils from coretk.nodeutils import NodeUtils
SCROLL_BUFFER = 25
ZOOM_IN = 1.1
ZOOM_OUT = 0.9
class CanvasGraph(tk.Canvas): class CanvasGraph(tk.Canvas):
def __init__(self, master, core, width, height, cnf=None, **kwargs): def __init__(self, master, core, width, height):
if cnf is None: super().__init__(
cnf = {} master,
kwargs["highlightthickness"] = 0 highlightthickness=0,
super().__init__(master, cnf, **kwargs) background="#cccccc",
scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER),
)
self.app = master self.app = master
self.core = core
self.mode = GraphMode.SELECT self.mode = GraphMode.SELECT
self.annotation_type = None self.annotation_type = None
self.selection = {} self.selection = {}
@ -35,12 +42,13 @@ class CanvasGraph(tk.Canvas):
self.wireless_edges = {} self.wireless_edges = {}
self.drawing_edge = None self.drawing_edge = None
self.grid = None self.grid = None
self.setup_bindings()
self.core = core
self.throughput_draw = Throughput(self, core) self.throughput_draw = Throughput(self, core)
self.shape_drawing = False self.shape_drawing = False
self.default_width = width self.default_width = width
self.default_height = height self.default_height = height
self.ratio = 1.0
self.offset = (0, 0)
self.cursor = (0, 0)
# background related # background related
self.wallpaper_id = None self.wallpaper_id = None
@ -51,6 +59,9 @@ class CanvasGraph(tk.Canvas):
self.show_grid = tk.BooleanVar(value=True) self.show_grid = tk.BooleanVar(value=True)
self.adjust_to_dim = tk.BooleanVar(value=False) self.adjust_to_dim = tk.BooleanVar(value=False)
# bindings
self.setup_bindings()
# draw base canvas # draw base canvas
self.draw_canvas() self.draw_canvas()
self.draw_grid() self.draw_grid()
@ -99,17 +110,19 @@ class CanvasGraph(tk.Canvas):
self.bind("<ButtonPress-1>", self.click_press) self.bind("<ButtonPress-1>", self.click_press)
self.bind("<ButtonRelease-1>", self.click_release) self.bind("<ButtonRelease-1>", self.click_release)
self.bind("<B1-Motion>", self.click_motion) self.bind("<B1-Motion>", self.click_motion)
self.bind("<Button-3>", self.click_context) self.bind("<ButtonRelease-3>", self.click_context)
self.bind("<Delete>", self.press_delete) self.bind("<Delete>", self.press_delete)
self.bind("<Control-1>", self.ctrl_click) self.bind("<Control-1>", self.ctrl_click)
self.bind("<Double-Button-1>", self.double_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 draw_grid(self): def draw_grid(self):
""" """
Create grid Create grid.
:param int width: the width
:param int height: the height
:return: nothing :return: nothing
""" """
@ -213,7 +226,8 @@ class CanvasGraph(tk.Canvas):
:rtype: int :rtype: int
:return: the item that the mouse point to :return: the item that the mouse point to
""" """
overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) x, y = self.canvas_xy(event)
overlapping = self.find_overlapping(x, y, x, y)
selected = None selected = None
for _id in overlapping: for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id: if self.drawing_edge and self.drawing_edge.id == _id:
@ -330,10 +344,10 @@ class CanvasGraph(tk.Canvas):
self.delete(_id) self.delete(_id)
self.selection.clear() self.selection.clear()
def object_drag(self, object_id, offset_x, offset_y): def move_selection(self, object_id, x_offset, y_offset):
select_id = self.selection.get(object_id) select_id = self.selection.get(object_id)
if select_id is not None: if select_id is not None:
self.move(select_id, offset_x, offset_y) self.move(select_id, x_offset, y_offset)
def delete_selection_objects(self): def delete_selection_objects(self):
edges = set() edges = set()
@ -382,6 +396,20 @@ class CanvasGraph(tk.Canvas):
self.selection.clear() self.selection.clear()
return nodes return nodes
def zoom(self, event, factor=None):
if not factor:
factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
self.scale("all", event.x, event.y, factor, factor)
self.configure(scrollregion=self.bbox("all"))
self.ratio *= float(factor)
self.offset = (
self.offset[0] * factor + event.x * (1 - factor),
self.offset[1] * factor + event.y * (1 - factor),
)
logging.info("ratio: %s", self.ratio)
logging.info("offset: %s", self.offset)
def click_press(self, event): def click_press(self, event):
""" """
Start drawing an edge if mouse click is on a node Start drawing an edge if mouse click is on a node
@ -389,40 +417,46 @@ class CanvasGraph(tk.Canvas):
:param event: mouse event :param event: mouse event
:return: nothing :return: nothing
""" """
logging.debug(f"click press: {event}") x, y = self.canvas_xy(event)
self.cursor = x, y
selected = self.get_selected(event) selected = self.get_selected(event)
logging.debug(f"click press: %s", selected)
is_node = selected in self.nodes is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node: if self.mode == GraphMode.EDGE and is_node:
x, y = self.coords(selected) x, y = self.coords(selected)
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
if self.mode == GraphMode.ANNOTATION and selected is None: if self.mode == GraphMode.ANNOTATION and selected is None:
x, y = self.canvas_xy(event)
shape = Shape(self.app, self, self.annotation_type, x, y) shape = Shape(self.app, self, self.annotation_type, x, y)
self.selected = shape.id self.selected = shape.id
self.shape_drawing = True self.shape_drawing = True
self.shapes[shape.id] = shape self.shapes[shape.id] = shape
if self.mode == GraphMode.SELECT: if selected is not None:
if selected is not None: if selected not in self.selection:
if selected in self.shapes: if selected in self.shapes:
x, y = self.canvas_xy(event)
shape = self.shapes[selected] shape = self.shapes[selected]
shape.cursor_x = x self.select_object(shape.id)
shape.cursor_y = y
if selected not in self.selection:
self.select_object(shape.id)
self.selected = selected self.selected = selected
else: elif selected in self.nodes:
self.clear_selection() node = self.nodes[selected]
self.select_object(node.id)
self.selected = selected
else:
self.clear_selection()
def ctrl_click(self, event): def ctrl_click(self, event):
# update cursor location
x, y = self.canvas_xy(event)
self.cursor = x, y
# handle multiple selections
logging.debug("control left click: %s", event) logging.debug("control left click: %s", event)
selected = self.get_selected(event) selected = self.get_selected(event)
if ( if (
self.mode == GraphMode.SELECT selected not in self.selection
and selected is not None
and selected in self.shapes and selected in self.shapes
or selected in self.nodes
): ):
self.select_object(selected, choose_multiple=True) self.select_object(selected, choose_multiple=True)
@ -433,36 +467,31 @@ class CanvasGraph(tk.Canvas):
:param event: mouse event :param event: mouse event
:return: nothing :return: nothing
""" """
x, y = self.canvas_xy(event)
x_offset = x - self.cursor[0]
y_offset = y - self.cursor[1]
self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None: if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
x2, y2 = self.canvas_xy(event)
x1, y1, _, _ = self.coords(self.drawing_edge.id) x1, y1, _, _ = self.coords(self.drawing_edge.id)
self.coords(self.drawing_edge.id, x1, y1, x2, y2) self.coords(self.drawing_edge.id, x1, y1, x, y)
if self.mode == GraphMode.ANNOTATION: if self.mode == GraphMode.ANNOTATION:
if is_draw_shape(self.annotation_type) and self.shape_drawing: if is_draw_shape(self.annotation_type) and self.shape_drawing:
x, y = self.canvas_xy(event)
shape = self.shapes[self.selected] shape = self.shapes[self.selected]
shape.shape_motion(x, y) shape.shape_motion(x, y)
if (
self.mode == GraphMode.SELECT
and self.selected is not None
and self.selected in self.shapes
):
x, y = self.canvas_xy(event)
shape = self.shapes[self.selected]
delta_x = x - shape.cursor_x
delta_y = y - shape.cursor_y
shape.motion(event)
# move other selected components if self.mode == GraphMode.EDGE:
for _id in self.selection: return
if _id != self.selected and _id in self.shapes:
shape = self.shapes[_id] # move selected objects
shape.motion(None, delta_x, delta_y) for selected_id in self.selection:
if _id != self.selected and _id in self.nodes: if selected_id in self.shapes:
node = self.nodes[_id] shape = self.shapes[selected_id]
node_x = node.core_node.position.x shape.motion(x_offset, y_offset)
node_y = node.core_node.position.y
node.move(node_x + delta_x, node_y + delta_y) if selected_id in self.nodes:
node = self.nodes[selected_id]
node.motion(x_offset, y_offset, update=self.core.is_runtime())
def click_context(self, event): def click_context(self, event):
logging.info("context event: %s", self.context) logging.info("context event: %s", self.context)
@ -592,7 +621,7 @@ class CanvasGraph(tk.Canvas):
:return: nothing :return: nothing
""" """
# resize canvas and scrollregion # resize canvas and scrollregion
self.config(scrollregion=(0, 0, width + 200, height + 200)) self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER))
self.coords(self.grid, 0, 0, width, height) self.coords(self.grid, 0, 0, width, height)
# redraw gridlines to new canvas size # redraw gridlines to new canvas size

View file

@ -1,4 +1,3 @@
import logging
import tkinter as tk import tkinter as tk
from tkinter import font from tkinter import font
@ -11,7 +10,6 @@ from coretk.dialogs.nodeconfig import NodeConfigDialog
from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog
from coretk.errors import show_grpc_error from coretk.errors import show_grpc_error
from coretk.graph import tags from coretk.graph import tags
from coretk.graph.enums import GraphMode
from coretk.graph.tooltip import CanvasTooltip from coretk.graph.tooltip import CanvasTooltip
from coretk.nodeutils import NodeUtils from coretk.nodeutils import NodeUtils
@ -29,12 +27,11 @@ class CanvasNode:
self.id = self.canvas.create_image( self.id = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
) )
image_box = self.canvas.bbox(self.id)
y = image_box[3] + NODE_TEXT_OFFSET
text_font = font.Font(family="TkIconFont", size=12) text_font = font.Font(family="TkIconFont", size=12)
label_y = self._get_label_y()
self.text_id = self.canvas.create_text( self.text_id = self.canvas.create_text(
x, x,
y, label_y,
text=self.core_node.name, text=self.core_node.name,
tags=tags.NODE_NAME, tags=tags.NODE_NAME,
font=text_font, font=text_font,
@ -44,17 +41,11 @@ class CanvasNode:
self.edges = set() self.edges = set()
self.interfaces = [] self.interfaces = []
self.wireless_edges = set() self.wireless_edges = set()
self.moving = None
self.antennae = [] self.antennae = []
self.setup_bindings() self.setup_bindings()
def setup_bindings(self): def setup_bindings(self):
# self.canvas.bind("<Button-3>", self.click_context)
self.canvas.tag_bind(self.id, "<ButtonPress-1>", self.click_press)
self.canvas.tag_bind(self.id, "<ButtonRelease-1>", self.click_release)
self.canvas.tag_bind(self.id, "<B1-Motion>", self.motion)
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click) self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click)
self.canvas.tag_bind(self.id, "<Control-1>", self.select_multiple)
self.canvas.tag_bind(self.id, "<Enter>", self.on_enter) self.canvas.tag_bind(self.id, "<Enter>", self.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.on_leave) self.canvas.tag_bind(self.id, "<Leave>", self.on_leave)
@ -95,30 +86,32 @@ class CanvasNode:
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antennae.clear() self.antennae.clear()
def move_antennae(self, x_offset, y_offset):
"""
redraw antennas of a node according to the new node position
:return: nothing
"""
for antenna_id in self.antennae:
self.canvas.move(antenna_id, x_offset, y_offset)
def redraw(self): def redraw(self):
self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.id, image=self.image)
self.canvas.itemconfig(self.text_id, text=self.core_node.name) self.canvas.itemconfig(self.text_id, text=self.core_node.name)
def move(self, x, y, update=True): def _get_label_y(self):
old_x = self.core_node.position.x image_box = self.canvas.bbox(self.id)
old_y = self.core_node.position.y return image_box[3] + NODE_TEXT_OFFSET
x_offset = x - old_x
y_offset = y - old_y def move(self, x, y):
self.core_node.position.x = int(x) x_offset = x - self.core_node.position.x
self.core_node.position.y = int(y) y_offset = y - self.core_node.position.y
self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset, y_offset, update=True):
self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.id, x_offset, y_offset)
self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset)
self.move_antennae(x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset)
self.canvas.object_drag(self.id, x_offset, y_offset) x, y = self.canvas.coords(self.id)
self.core_node.position.x = int(x)
self.core_node.position.y = int(y)
# move antennae
for antenna_id in self.antennae:
self.canvas.move(antenna_id, x_offset, y_offset)
# move edges
for edge in self.edges: for edge in self.edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id) x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id: if edge.src == self.id:
@ -126,16 +119,18 @@ class CanvasNode:
else: else:
self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.coords(edge.id, x1, y1, x, y)
self.canvas.throughput_draw.move(edge) self.canvas.throughput_draw.move(edge)
edge.link_info.recalculate_info() edge.link_info.recalculate_info()
for edge in self.wireless_edges: for edge in self.wireless_edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id) x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id: if edge.src == self.id:
self.canvas.coords(edge.id, x, y, x2, y2) self.canvas.coords(edge.id, x, y, x2, y2)
else: else:
self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.coords(edge.id, x1, y1, x, y)
# update core with new location
if self.app.core.is_runtime() and update: if self.app.core.is_runtime() and update:
self.app.core.edit_node(self.core_node.id, int(x), int(y)) self.app.core.edit_node(self.core_node)
def on_enter(self, event): def on_enter(self, event):
if self.app.core.is_runtime() and self.app.core.observer: if self.app.core.is_runtime() and self.app.core.observer:
@ -164,39 +159,6 @@ class CanvasNode:
self.core_node.position.x = int(x) self.core_node.position.x = int(x)
self.core_node.position.y = int(y) self.core_node.position.y = int(y)
def click_press(self, event):
logging.debug(f"node click press {self.core_node.name}: {event}")
self.moving = self.canvas.canvas_xy(event)
if self.id not in self.canvas.selection:
self.canvas.select_object(self.id)
self.canvas.selected = self.id
def click_release(self, event):
logging.debug(f"node click release {self.core_node.name}: {event}")
self.update_coords()
self.moving = None
def motion(self, event):
if self.canvas.mode == GraphMode.EDGE:
return
x, y = self.canvas.canvas_xy(event)
my_x = self.core_node.position.x
my_y = self.core_node.position.y
self.move(x, y)
# move other selected components
for object_id, selection_id in self.canvas.selection.items():
if object_id != self.id and object_id in self.canvas.nodes:
canvas_node = self.canvas.nodes[object_id]
other_old_x = canvas_node.core_node.position.x
other_old_y = canvas_node.core_node.position.y
other_new_x = x + other_old_x - my_x
other_new_y = y + other_old_y - my_y
self.canvas.nodes[object_id].move(other_new_x, other_new_y)
elif object_id in self.canvas.shapes:
shape = self.canvas.shapes[object_id]
shape.motion(None, x - my_x, y - my_y)
def create_context(self): def create_context(self):
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_emane = self.core_node.type == NodeType.EMANE is_emane = self.core_node.type == NodeType.EMANE
@ -244,9 +206,6 @@ class CanvasNode:
context.add_command(label="Hide", state=tk.DISABLED) context.add_command(label="Hide", state=tk.DISABLED)
return context return context
def select_multiple(self, event):
self.canvas.select_object(self.id, choose_multiple=True)
def show_config(self): def show_config(self):
self.canvas.context = None self.canvas.context = None
dialog = NodeConfigDialog(self.app, self.app, self) dialog = NodeConfigDialog(self.app, self.app, self)

View file

@ -49,13 +49,9 @@ class Shape:
if data is None: if data is None:
self.created = False self.created = False
self.shape_data = AnnotationData() self.shape_data = AnnotationData()
self.cursor_x = x1
self.cursor_y = y1
else: else:
self.created = True self.created = True
self.shape_data = data self.shape_data = data
self.cursor_x = None
self.cursor_y = None
self.draw() self.draw()
def draw(self): def draw(self):
@ -136,16 +132,11 @@ class Shape:
s = ShapeDialog(self.app, self.app, self) s = ShapeDialog(self.app, self.app, self)
s.show() s.show()
def motion(self, event, delta_x=None, delta_y=None): def motion(self, x_offset, y_offset):
if event is not None: self.canvas.move(self.id, x_offset, y_offset)
delta_x = event.x - self.cursor_x self.canvas.move_selection(self.id, x_offset, y_offset)
delta_y = event.y - self.cursor_y
self.cursor_x = event.x
self.cursor_y = event.y
self.canvas.move(self.id, delta_x, delta_y)
self.canvas.object_drag(self.id, delta_x, delta_y)
if self.text_id is not None: if self.text_id is not None:
self.canvas.move(self.text_id, delta_x, delta_y) self.canvas.move(self.text_id, x_offset, y_offset)
def delete(self): def delete(self):
self.canvas.delete(self.id) self.canvas.delete(self.id)