pygui: added class variable type hinting to core.gui.graph

This commit is contained in:
Blake Harnden 2020-06-19 23:24:07 -07:00
parent 0356f3b19c
commit 11be40bc90
8 changed files with 256 additions and 249 deletions

View file

@ -79,7 +79,7 @@ class CoreClient:
self.read_config()
# helpers
self.iface_to_edge: Dict[Tuple[int, int], Tuple[int, int]] = {}
self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {}
self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
# session data

View file

@ -1,9 +1,10 @@
import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Any, Tuple
from typing import TYPE_CHECKING, Optional, Tuple
from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.graph import tags
@ -12,12 +13,12 @@ from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
TEXT_DISTANCE = 0.30
EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933"
ARC_DISTANCE = 50
TEXT_DISTANCE: float = 0.30
EDGE_WIDTH: int = 3
EDGE_COLOR: str = "#ff0000"
WIRELESS_WIDTH: float = 1.5
WIRELESS_COLOR: str = "#009933"
ARC_DISTANCE: int = 50
def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
@ -57,20 +58,20 @@ def arc_edges(edges) -> None:
class Edge:
tag = tags.EDGE
tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
self.id = None
self.src = src
self.dst = dst
self.arc = 0
self.token = None
self.src_label = None
self.middle_label = None
self.dst_label = None
self.color = EDGE_COLOR
self.width = EDGE_WIDTH
self.id: Optional[int] = None
self.src: int = src
self.dst: int = dst
self.arc: int = 0
self.token: Optional[Tuple[int, ...]] = None
self.src_label: Optional[int] = None
self.middle_label: Optional[int] = None
self.dst_label: Optional[int] = None
self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH
@classmethod
def create_token(cls, src: int, dst: int) -> Tuple[int, ...]:
@ -120,7 +121,7 @@ class Edge:
fill=self.color,
)
def redraw(self):
def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
@ -233,13 +234,13 @@ class CanvasWirelessEdge(Edge):
dst: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
token: Tuple[Any, ...],
token: Tuple[int, ...],
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
self.token = token
self.width = WIRELESS_WIDTH
self.color = WIRELESS_COLOR
self.token: Tuple[int, ...] = token
self.width: float = WIRELESS_WIDTH
self.color: str = WIRELESS_COLOR
self.draw(src_pos, dst_pos)
@ -259,19 +260,19 @@ class CanvasEdge(Edge):
Create an instance of canvas edge object
"""
super().__init__(canvas, src)
self.src_iface = None
self.dst_iface = None
self.text_src = None
self.text_dst = None
self.link = None
self.asymmetric_link = None
self.throughput = None
self.src_iface: Optional[Interface] = None
self.dst_iface: Optional[Interface] = None
self.text_src: Optional[int] = None
self.text_dst: Optional[int] = None
self.link: Optional[Link] = None
self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos)
self.set_binding()
self.context = tk.Menu(self.canvas)
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
def create_context(self):
def create_context(self) -> None:
themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure)
self.context.add_command(label="Delete", command=self.click_delete)
@ -279,7 +280,7 @@ class CanvasEdge(Edge):
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
def set_link(self, link) -> None:
def set_link(self, link: Link) -> None:
self.link = link
self.draw_labels()
@ -383,7 +384,7 @@ class CanvasEdge(Edge):
self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root)
def click_delete(self):
def click_delete(self) -> None:
self.canvas.delete_edge(self)
def click_configure(self) -> None:

View file

@ -2,11 +2,12 @@ import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from PIL import Image, ImageTk
from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import Interface, Link, LinkType, Session, ThroughputsEvent
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.edges import (
@ -21,7 +22,7 @@ from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, TypeToImage
from core.gui.nodeutils import NodeUtils
from core.gui.nodeutils import NodeDraw, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
@ -48,58 +49,59 @@ class ShowVar(BooleanVar):
class CanvasGraph(tk.Canvas):
def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"):
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
) -> None:
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 = {}
self.app: "Application" = app
self.core: "CoreClient" = core
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {}
self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None
self.node_draw: Optional[NodeDraw] = None
self.nodes: Dict[int, CanvasNode] = {}
self.edges: Dict[int, CanvasEdge] = {}
self.shapes: Dict[int, Shape] = {}
self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {}
# map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network = {}
self.wireless_network: Dict[int, Set[int]] = {}
self.drawing_edge = None
self.rect = None
self.shape_drawing = False
self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None
self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height
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 = []
self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_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 = 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)
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)
# throughput related
self.throughput_threshold = 250.0
self.throughput_width = 10
self.throughput_color = "#FF0000"
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#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_iface_names = BooleanVar(value=False)
self.show_ip4s = BooleanVar(value=True)
self.show_ip6s = BooleanVar(value=True)
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# bindings
self.setup_bindings()
@ -108,7 +110,7 @@ class CanvasGraph(tk.Canvas):
self.draw_canvas()
self.draw_grid()
def draw_canvas(self, dimensions: Tuple[int, int] = None):
def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None:
if self.rect is not None:
self.delete(self.rect)
if not dimensions:
@ -125,7 +127,7 @@ class CanvasGraph(tk.Canvas):
)
self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: core_pb2.Session):
def reset_and_redraw(self, session: Session) -> None:
"""
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
client.
@ -157,7 +159,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge = None
self.draw_session(session)
def setup_bindings(self):
def setup_bindings(self) -> None:
"""
Bind any mouse events or hot keys to the matching action
"""
@ -173,28 +175,28 @@ class CanvasGraph(tk.Canvas):
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]:
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) -> [float, float]:
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) -> [bool, bool]:
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) -> [bool, bool]:
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 set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent):
def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None:
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
@ -209,7 +211,7 @@ class CanvasGraph(tk.Canvas):
else:
del self.core.iface_to_edge[iface_to_edge_id]
def draw_grid(self):
def draw_grid(self) -> None:
"""
Create grid.
"""
@ -223,9 +225,7 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect)
def add_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
) -> None:
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: 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:
@ -248,7 +248,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id)
@ -263,7 +263,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges)
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
@ -275,7 +275,7 @@ class CanvasGraph(tk.Canvas):
edge = self.wireless_edges[token]
edge.middle_label_text(link.label)
def draw_session(self, session: core_pb2.Session):
def draw_session(self, session: Session) -> None:
"""
Draw existing session.
"""
@ -306,7 +306,7 @@ class CanvasGraph(tk.Canvas):
node2 = canvas_node2.core_node
token = create_edge_token(canvas_node1.id, canvas_node2.id)
if link.type == core_pb2.LinkType.WIRELESS:
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link)
else:
if token not in self.edges:
@ -337,7 +337,7 @@ class CanvasGraph(tk.Canvas):
else:
logging.error("duplicate link received: %s", link)
def stopped_session(self):
def stopped_session(self) -> None:
# clear wireless edges
for edge in self.wireless_edges.values():
edge.delete()
@ -351,7 +351,7 @@ class CanvasGraph(tk.Canvas):
for edge in self.edges.values():
edge.reset()
def canvas_xy(self, event: tk.Event) -> [float, float]:
def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
"""
Convert window coordinate to canvas coordinate
"""
@ -379,7 +379,7 @@ class CanvasGraph(tk.Canvas):
return selected
def click_release(self, event: tk.Event):
def click_release(self, event: tk.Event) -> None:
"""
Draw a node or finish drawing an edge according to the current graph mode
"""
@ -418,7 +418,7 @@ class CanvasGraph(tk.Canvas):
self.mode = GraphMode.NODE
self.selected = None
def handle_edge_release(self, _event: tk.Event):
def handle_edge_release(self, _event: tk.Event) -> None:
edge = self.drawing_edge
self.drawing_edge = None
@ -454,7 +454,7 @@ class CanvasGraph(tk.Canvas):
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):
def select_object(self, object_id: int, choose_multiple: bool = False) -> None:
"""
create a bounding box when a node is selected
"""
@ -475,7 +475,7 @@ class CanvasGraph(tk.Canvas):
selection_id = self.selection.pop(object_id)
self.delete(selection_id)
def clear_selection(self):
def clear_selection(self) -> None:
"""
Clear current selection boxes.
"""
@ -483,7 +483,7 @@ class CanvasGraph(tk.Canvas):
self.delete(_id)
self.selection.clear()
def move_selection(self, object_id: int, x_offset: float, y_offset: float):
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)
@ -531,7 +531,7 @@ class CanvasGraph(tk.Canvas):
self.core.deleted_graph_nodes(nodes)
self.core.deleted_graph_edges(edges)
def delete_edge(self, edge: CanvasEdge):
def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete()
del self.edges[edge.token]
src_node = self.nodes[edge.src]
@ -550,7 +550,7 @@ class CanvasGraph(tk.Canvas):
src_node.delete_antenna()
self.core.deleted_graph_edges([edge])
def zoom(self, event: tk.Event, factor: float = None):
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)
@ -568,7 +568,7 @@ class CanvasGraph(tk.Canvas):
if self.wallpaper:
self.redraw_wallpaper()
def click_press(self, event: tk.Event):
def click_press(self, event: tk.Event) -> None:
"""
Start drawing an edge if mouse click is on a node
"""
@ -630,7 +630,7 @@ class CanvasGraph(tk.Canvas):
self.select_box = shape
self.clear_selection()
def ctrl_click(self, event: tk.Event):
def ctrl_click(self, event: tk.Event) -> None:
# update cursor location
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
@ -648,7 +648,7 @@ class CanvasGraph(tk.Canvas):
):
self.select_object(selected, choose_multiple=True)
def click_motion(self, event: tk.Event):
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:
@ -701,7 +701,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box and self.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event):
def press_delete(self, _event: tk.Event) -> None:
"""
delete selected nodes and any data that relates to it
"""
@ -711,7 +711,7 @@ class CanvasGraph(tk.Canvas):
else:
logging.debug("node deletion is disabled during runtime state")
def double_click(self, event: tk.Event):
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]
@ -737,7 +737,7 @@ class CanvasGraph(tk.Canvas):
self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node
def width_and_height(self):
def width_and_height(self) -> Tuple[int, int]:
"""
retrieve canvas width and height in pixels
"""
@ -753,8 +753,8 @@ class CanvasGraph(tk.Canvas):
return image
def draw_wallpaper(
self, image: ImageTk.PhotoImage, x: float = None, y: float = None
):
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
@ -762,7 +762,7 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER)
self.wallpaper_drawn = image
def wallpaper_upper_left(self):
def wallpaper_upper_left(self) -> None:
self.delete(self.wallpaper_id)
# create new scaled image, cropped if needed
@ -775,7 +775,7 @@ class CanvasGraph(tk.Canvas):
if image.height > height:
cropy = image.height
cropped = image.crop((0, 0, cropx, cropy))
image = ImageTk.PhotoImage(cropped)
image = PhotoImage(cropped)
# draw on canvas
x1, y1, _, _ = self.bbox(self.rect)
@ -783,7 +783,7 @@ class CanvasGraph(tk.Canvas):
y = (cropy / 2) + y1
self.draw_wallpaper(image, x, y)
def wallpaper_center(self):
def wallpaper_center(self) -> None:
"""
place the image at the center of canvas
"""
@ -803,26 +803,26 @@ class CanvasGraph(tk.Canvas):
x2 = image.width - cropx
y2 = image.height - cropy
cropped = image.crop((x1, y1, x2, y2))
image = ImageTk.PhotoImage(cropped)
image = PhotoImage(cropped)
self.draw_wallpaper(image)
def wallpaper_scaled(self):
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 = ImageTk.PhotoImage(image)
image = PhotoImage(image)
self.draw_wallpaper(image)
def resize_to_wallpaper(self):
def resize_to_wallpaper(self) -> None:
self.delete(self.wallpaper_id)
image = ImageTk.PhotoImage(self.wallpaper)
image = PhotoImage(self.wallpaper)
self.redraw_canvas((image.width(), image.height()))
self.draw_wallpaper(image)
def redraw_canvas(self, dimensions: Tuple[int, int] = None):
def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None:
logging.debug("redrawing canvas to dimensions: %s", dimensions)
# reset scale and move back to original position
@ -843,7 +843,7 @@ class CanvasGraph(tk.Canvas):
self.draw_grid()
self.app.canvas.show_grid.click_handler()
def redraw_wallpaper(self):
def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get():
logging.debug("drawing wallpaper to canvas dimensions")
self.resize_to_wallpaper()
@ -864,7 +864,7 @@ class CanvasGraph(tk.Canvas):
for tag in tags.ORGANIZE_TAGS:
self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]):
def set_wallpaper(self, filename: Optional[str]) -> None:
logging.debug("setting wallpaper: %s", filename)
if filename:
img = Image.open(filename)
@ -880,7 +880,7 @@ class CanvasGraph(tk.Canvas):
def is_selection_mode(self) -> bool:
return self.mode == GraphMode.SELECT
def create_edge(self, source: CanvasNode, dest: CanvasNode):
def create_edge(self, source: CanvasNode, dest: CanvasNode) -> None:
"""
create an edge between source node and destination node
"""
@ -894,7 +894,7 @@ class CanvasGraph(tk.Canvas):
self.nodes[dest.id].edges.add(edge)
self.core.create_link(edge, source, dest)
def copy(self):
def copy(self) -> None:
if self.core.is_runtime():
logging.debug("copy is disabled during runtime state")
return
@ -905,7 +905,7 @@ class CanvasGraph(tk.Canvas):
canvas_node = self.nodes[node_id]
self.to_copy.append(canvas_node)
def paste(self):
def paste(self) -> None:
if self.core.is_runtime():
logging.debug("paste is disabled during runtime state")
return
@ -972,11 +972,11 @@ class CanvasGraph(tk.Canvas):
else:
asym_iface1 = None
if iface1_id:
asym_iface1 = core_pb2.Interface(id=iface1_id)
asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None
if iface2_id:
asym_iface2 = core_pb2.Interface(id=iface2_id)
copy_edge.asymmetric_link = core_pb2.Link(
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_iface1,
@ -990,7 +990,7 @@ class CanvasGraph(tk.Canvas):
)
self.tag_raise(tags.NODE)
def scale_graph(self):
def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items():
img = None
if NodeUtils.is_custom(

View file

@ -1,12 +1,14 @@
import functools
import logging
import tkinter as tk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import NodeType
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Interface, Node, NodeType
from core.api.grpc.services_pb2 import NodeServiceData
from core.gui import themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
@ -15,36 +17,31 @@ from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from PIL.ImageTk import PhotoImage
from core.gui.graph.graph import CanvasGraph
NODE_TEXT_OFFSET = 5
NODE_TEXT_OFFSET: int = 5
class CanvasNode:
def __init__(
self,
app: "Application",
x: float,
y: float,
core_node: core_pb2.Node,
image: "PhotoImage",
self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage
):
self.app = app
self.canvas = app.canvas
self.image = image
self.core_node = core_node
self.id = self.canvas.create_image(
self.app: "Application" = app
self.canvas: "CanvasGraph" = app.canvas
self.image: PhotoImage = image
self.core_node: Node = core_node
self.id: int = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
)
label_y = self._get_label_y()
self.text_id = self.canvas.create_text(
self.text_id: int = self.canvas.create_text(
x,
label_y,
text=self.core_node.name,
@ -53,21 +50,21 @@ class CanvasNode:
fill="#0000CD",
state=self.canvas.show_node_labels.state(),
)
self.tooltip = CanvasTooltip(self.canvas)
self.edges = set()
self.ifaces = {}
self.wireless_edges = set()
self.antennas = []
self.antenna_images = {}
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set()
self.ifaces: Dict[int, Interface] = {}
self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {}
# possible configurations
self.emane_model_configs = {}
self.wlan_config = {}
self.mobility_config = {}
self.service_configs = {}
self.service_file_configs = {}
self.config_service_configs = {}
self.emane_model_configs: Dict[Tuple[str, Optional[int]], ConfigOption] = {}
self.wlan_config: Dict[str, ConfigOption] = {}
self.mobility_config: Dict[str, ConfigOption] = {}
self.service_configs: Dict[str, NodeServiceData] = {}
self.service_file_configs: Dict[str, Dict[str, str]] = {}
self.config_service_configs: Dict[str, Any] = {}
self.setup_bindings()
self.context = tk.Menu(self.canvas)
self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context)
def next_iface_id(self) -> int:
@ -76,19 +73,19 @@ class CanvasNode:
i += 1
return i
def setup_bindings(self):
def setup_bindings(self) -> None:
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click)
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, "<ButtonRelease-3>", self.show_context)
def delete(self):
def delete(self) -> None:
logging.debug("Delete canvas node for %s", self.core_node)
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)
self.delete_antennas()
def add_antenna(self):
def add_antenna(self) -> None:
x, y = self.canvas.coords(self.id)
offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
@ -102,7 +99,7 @@ class CanvasNode:
self.antennas.append(antenna_id)
self.antenna_images[antenna_id] = img
def delete_antenna(self):
def delete_antenna(self) -> None:
"""
delete one antenna
"""
@ -112,7 +109,7 @@ class CanvasNode:
self.canvas.delete(antenna_id)
self.antenna_images.pop(antenna_id, None)
def delete_antennas(self):
def delete_antennas(self) -> None:
"""
delete all antennas
"""
@ -122,30 +119,30 @@ class CanvasNode:
self.antennas.clear()
self.antenna_images.clear()
def redraw(self):
def redraw(self) -> None:
self.canvas.itemconfig(self.id, image=self.image)
self.canvas.itemconfig(self.text_id, text=self.core_node.name)
for edge in self.edges:
edge.redraw()
def _get_label_y(self):
def _get_label_y(self) -> int:
image_box = self.canvas.bbox(self.id)
return image_box[3] + NODE_TEXT_OFFSET
def scale_text(self):
def scale_text(self) -> None:
text_bound = self.canvas.bbox(self.text_id)
prev_y = (text_bound[3] + text_bound[1]) / 2
new_y = self._get_label_y()
self.canvas.move(self.text_id, 0, new_y - prev_y)
def move(self, x: int, y: int):
def move(self, x: int, y: int) -> None:
x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id)
x_offset = x - current_x
y_offset = y - current_y
self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: int, y_offset: int, update: bool = True):
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None:
original_position = self.canvas.coords(self.id)
self.canvas.move(self.id, x_offset, y_offset)
pos = self.canvas.coords(self.id)
@ -177,7 +174,7 @@ class CanvasNode:
if self.app.core.is_runtime() and update:
self.app.core.edit_node(self.core_node)
def on_enter(self, event: tk.Event):
def on_enter(self, event: tk.Event) -> None:
if self.app.core.is_runtime() and self.app.core.observer:
self.tooltip.text.set("waiting...")
self.tooltip.on_enter(event)
@ -187,10 +184,10 @@ class CanvasNode:
except grpc.RpcError as e:
self.app.show_grpc_exception("Observer Error", e)
def on_leave(self, event: tk.Event):
def on_leave(self, event: tk.Event) -> None:
self.tooltip.on_leave(event)
def double_click(self, event: tk.Event):
def double_click(self, event: tk.Event) -> None:
if self.app.core.is_runtime():
self.canvas.core.launch_terminal(self.core_node.id)
else:
@ -270,37 +267,37 @@ class CanvasNode:
self.canvas.selection[self.id] = self
self.canvas.copy()
def show_config(self):
def show_config(self) -> None:
dialog = NodeConfigDialog(self.app, self)
dialog.show()
def show_wlan_config(self):
def show_wlan_config(self) -> None:
dialog = WlanConfigDialog(self.app, self)
if not dialog.has_error:
dialog.show()
def show_mobility_config(self):
def show_mobility_config(self) -> None:
dialog = MobilityConfigDialog(self.app, self)
if not dialog.has_error:
dialog.show()
def show_mobility_player(self):
def show_mobility_player(self) -> None:
mobility_player = self.app.core.mobility_players[self.core_node.id]
mobility_player.show()
def show_emane_config(self):
def show_emane_config(self) -> None:
dialog = EmaneConfigDialog(self.app, self)
dialog.show()
def show_services(self):
def show_services(self) -> None:
dialog = NodeServiceDialog(self.app, self)
dialog.show()
def show_config_services(self):
def show_config_services(self) -> None:
dialog = NodeConfigServiceDialog(self.app, self)
dialog.show()
def has_emane_link(self, iface_id: int) -> core_pb2.Node:
def has_emane_link(self, iface_id: int) -> Node:
result = None
for edge in self.edges:
if self.id == edge.src:
@ -317,14 +314,14 @@ class CanvasNode:
break
return result
def wireless_link_selected(self):
def wireless_link_selected(self) -> None:
nodes = [x for x in self.canvas.selection if x in self.canvas.nodes]
for node_id in nodes:
canvas_node = self.canvas.nodes[node_id]
self.canvas.create_edge(self, canvas_node)
self.canvas.clear_selection()
def scale_antennas(self):
def scale_antennas(self) -> None:
for i in range(len(self.antennas)):
antenna_id = self.antennas[i]
image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)

View file

@ -1,5 +1,5 @@
import logging
from typing import TYPE_CHECKING, Dict, List, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
@ -23,17 +23,17 @@ class AnnotationData:
bold: bool = False,
italic: bool = False,
underline: bool = False,
):
self.text = text
self.font = font
self.font_size = font_size
self.text_color = text_color
self.fill_color = fill_color
self.border_color = border_color
self.border_width = border_width
self.bold = bold
self.italic = italic
self.underline = underline
) -> None:
self.text: str = text
self.font: str = font
self.font_size: int = font_size
self.text_color: str = text_color
self.fill_color: str = fill_color
self.border_color: str = border_color
self.border_width: int = border_width
self.bold: bool = bold
self.italic: bool = italic
self.underline: bool = underline
class Shape:
@ -47,29 +47,29 @@ class Shape:
x2: float = None,
y2: float = None,
data: AnnotationData = None,
):
self.app = app
self.canvas = canvas
self.shape_type = shape_type
self.id = None
self.text_id = None
self.x1 = x1
self.y1 = y1
) -> None:
self.app: "Application" = app
self.canvas: "CanvasGraph" = canvas
self.shape_type: ShapeType = shape_type
self.id: Optional[int] = None
self.text_id: Optional[int] = None
self.x1: float = x1
self.y1: float = y1
if x2 is None:
x2 = x1
self.x2 = x2
self.x2: float = x2
if y2 is None:
y2 = y1
self.y2 = y2
self.y2: float = y2
if data is None:
self.created = False
self.shape_data = AnnotationData()
self.created: bool = False
self.shape_data: AnnotationData = AnnotationData()
else:
self.created = True
self.created: bool = True
self.shape_data = data
self.draw()
def draw(self):
def draw(self) -> None:
if self.created:
dash = None
else:
@ -127,7 +127,7 @@ class Shape:
font.append("underline")
return font
def draw_shape_text(self):
def draw_shape_text(self) -> None:
if self.shape_data.text:
x = (self.x1 + self.x2) / 2
y = self.y1 + 1.5 * self.shape_data.font_size
@ -142,18 +142,18 @@ class Shape:
state=self.canvas.show_annotations.state(),
)
def shape_motion(self, x1: float, y1: float):
def shape_motion(self, x1: float, y1: float) -> None:
self.canvas.coords(self.id, self.x1, self.y1, x1, y1)
def shape_complete(self, x: float, y: float):
def shape_complete(self, x: float, y: float) -> None:
self.canvas.organize()
s = ShapeDialog(self.app, self)
s.show()
def disappear(self):
def disappear(self) -> None:
self.canvas.delete(self.id)
def motion(self, x_offset: float, y_offset: float):
def motion(self, x_offset: float, y_offset: float) -> None:
original_position = self.canvas.coords(self.id)
self.canvas.move(self.id, x_offset, y_offset)
coords = self.canvas.coords(self.id)
@ -166,7 +166,7 @@ class Shape:
if self.text_id is not None:
self.canvas.move(self.text_id, x_offset, y_offset)
def delete(self):
def delete(self) -> None:
logging.debug("Delete shape, id(%s)", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)

View file

@ -1,4 +1,5 @@
import enum
from typing import Set
class ShapeType(enum.Enum):
@ -8,7 +9,7 @@ class ShapeType(enum.Enum):
TEXT = "text"
SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE}
SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE}
def is_draw_shape(shape_type: ShapeType) -> bool:

View file

@ -1,17 +1,19 @@
ANNOTATION = "annotation"
GRIDLINE = "gridline"
SHAPE = "shape"
SHAPE_TEXT = "shapetext"
EDGE = "edge"
LINK_LABEL = "linklabel"
WIRELESS_EDGE = "wireless"
ANTENNA = "antenna"
NODE_LABEL = "nodename"
NODE = "node"
WALLPAPER = "wallpaper"
SELECTION = "selectednodes"
MARKER = "marker"
ORGANIZE_TAGS = [
from typing import List
ANNOTATION: str = "annotation"
GRIDLINE: str = "gridline"
SHAPE: str = "shape"
SHAPE_TEXT: str = "shapetext"
EDGE: str = "edge"
LINK_LABEL: str = "linklabel"
WIRELESS_EDGE: str = "wireless"
ANTENNA: str = "antenna"
NODE_LABEL: str = "nodename"
NODE: str = "node"
WALLPAPER: str = "wallpaper"
SELECTION: str = "selectednodes"
MARKER: str = "marker"
ORGANIZE_TAGS: List[str] = [
WALLPAPER,
GRIDLINE,
SHAPE,
@ -25,7 +27,7 @@ ORGANIZE_TAGS = [
SELECTION,
MARKER,
]
RESET_TAGS = [
RESET_TAGS: List[str] = [
EDGE,
NODE,
NODE_LABEL,

View file

@ -1,6 +1,6 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional, Tuple
from core.gui.themes import Styles
@ -27,39 +27,45 @@ class CanvasTooltip:
self,
canvas: "CanvasGraph",
*,
pad=(5, 3, 5, 3),
pad: Tuple[int, int, int, int] = (5, 3, 5, 3),
waittime: int = 400,
wraplength: int = 600
):
) -> None:
# in miliseconds, originally 500
self.waittime = waittime
self.waittime: int = waittime
# in pixels, originally 180
self.wraplength = wraplength
self.canvas = canvas
self.text = tk.StringVar()
self.pad = pad
self.id = None
self.tw = None
self.wraplength: int = wraplength
self.canvas: "CanvasGraph" = canvas
self.text: tk.StringVar = tk.StringVar()
self.pad: Tuple[int, int, int, int] = pad
self.id: Optional[str] = None
self.tw: Optional[tk.Toplevel] = None
def on_enter(self, event: tk.Event = None):
def on_enter(self, event: tk.Event = None) -> None:
self.schedule()
def on_leave(self, event: tk.Event = None):
def on_leave(self, event: tk.Event = None) -> None:
self.unschedule()
self.hide()
def schedule(self):
def schedule(self) -> None:
self.unschedule()
self.id = self.canvas.after(self.waittime, self.show)
def unschedule(self):
def unschedule(self) -> None:
id_ = self.id
self.id = None
if id_:
self.canvas.after_cancel(id_)
def show(self, event: tk.Event = None):
def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)):
def show(self, event: tk.Event = None) -> None:
def tip_pos_calculator(
canvas: "CanvasGraph",
label: ttk.Label,
*,
tip_delta: Tuple[int, int] = (10, 5),
pad: Tuple[int, int, int, int] = (5, 3, 5, 3)
):
c = canvas
s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight()
width, height = (
@ -108,7 +114,7 @@ class CanvasTooltip:
x, y = tip_pos_calculator(canvas, label, pad=pad)
self.tw.wm_geometry("+%d+%d" % (x, y))
def hide(self):
def hide(self) -> None:
if self.tw:
self.tw.destroy()
self.tw = None