Merge pull request #549 from coreemu/feature/pygui-multi-canvas

Feature/pygui multi canvas
This commit is contained in:
bharnden 2021-01-12 20:42:11 -08:00 committed by GitHub
commit 6ef458fc74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1336 additions and 780 deletions

View file

@ -67,6 +67,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
image=node_proto.image,
services=node_proto.services,
config_services=node_proto.config_services,
canvas=node_proto.canvas,
)
if node_proto.emane:
options.emane = node_proto.emane
@ -290,6 +291,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
config_services=config_services,
dir=node_dir,
channel=channel,
canvas=node.canvas,
)

View file

@ -649,6 +649,7 @@ class Node:
geo: Geo = None
dir: str = None
channel: str = None
canvas: int = None
# configurations
emane_model_configs: Dict[
@ -683,6 +684,7 @@ class Node:
geo=Geo.from_proto(proto.geo),
dir=proto.dir,
channel=proto.channel,
canvas=proto.canvas,
)
def to_proto(self) -> core_pb2.Node:
@ -700,6 +702,7 @@ class Node:
server=self.server,
dir=self.dir,
channel=self.channel,
canvas=self.canvas,
)

View file

@ -591,7 +591,6 @@ class Session:
:raises core.CoreError: when node to update does not exist
"""
node = self.get_node(node_id, NodeBase)
node.canvas = options.canvas
node.icon = options.icon
self.set_node_position(node, options)
self.sdt.edit_node(node, options.lon, options.lat, options.alt)

View file

@ -13,7 +13,7 @@ from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
@ -35,7 +35,7 @@ class Application(ttk.Frame):
self.menubar: Optional[Menubar] = None
self.toolbar: Optional[Toolbar] = None
self.right_frame: Optional[ttk.Frame] = None
self.canvas: Optional[CanvasGraph] = None
self.manager: Optional[CanvasManager] = None
self.statusbar: Optional[StatusBar] = None
self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None
@ -136,20 +136,8 @@ class Application(ttk.Frame):
label.grid(sticky=tk.EW, pady=PADY)
def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky=tk.NSEW)
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
)
scroll_x.grid(row=1, column=0, sticky=tk.EW)
self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set)
self.manager = CanvasManager(self.right_frame, self, self.core)
self.manager.notebook.grid(sticky=tk.NSEW)
def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self)
@ -201,8 +189,10 @@ class Application(ttk.Frame):
def joined_session_update(self) -> None:
if self.core.is_runtime():
self.menubar.set_state(is_runtime=True)
self.toolbar.set_runtime()
else:
self.menubar.set_state(is_runtime=False)
self.toolbar.set_design()
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:

View file

@ -185,7 +185,8 @@ class GuiConfig(yaml.YAMLObject):
def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file)
if not new_file.exists():
shutil.copy(current_file, new_file)
def find_terminal() -> Optional[str]:
@ -197,16 +198,14 @@ def find_terminal() -> Optional[str]:
def check_directory() -> None:
if HOME_PATH.exists():
return
HOME_PATH.mkdir()
BACKGROUNDS_PATH.mkdir()
CUSTOM_EMANE_PATH.mkdir()
CUSTOM_SERVICE_PATH.mkdir()
ICONS_PATH.mkdir()
MOBILITY_PATH.mkdir()
XMLS_PATH.mkdir()
SCRIPT_PATH.mkdir()
HOME_PATH.mkdir(exist_ok=True)
BACKGROUNDS_PATH.mkdir(exist_ok=True)
CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
ICONS_PATH.mkdir(exist_ok=True)
MOBILITY_PATH.mkdir(exist_ok=True)
XMLS_PATH.mkdir(exist_ok=True)
SCRIPT_PATH.mkdir(exist_ok=True)
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)

View file

@ -6,7 +6,6 @@ import json
import logging
import os
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
@ -40,16 +39,14 @@ from core.api.grpc.wrappers import (
SessionState,
ThroughputsEvent,
)
from core.gui import appconfig
from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer
from core.gui.appconfig import XMLS_PATH, CoreServer, Observer
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import AnnotationData, Shape
from core.gui.graph.shapeutils import ShapeType
from core.gui.graph.shape import Shape
from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw, NodeUtils
@ -207,27 +204,26 @@ class CoreClient:
canvas_node2 = self.canvas_nodes[node2_id]
if event.link.type == LinkType.WIRELESS:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wireless_edge(
self.app.manager.add_wireless_edge(
canvas_node1, canvas_node2, event.link
)
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wireless_edge(
self.app.manager.delete_wireless_edge(
canvas_node1, canvas_node2, event.link
)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wireless_edge(
self.app.manager.update_wireless_edge(
canvas_node1, canvas_node2, event.link
)
else:
logging.warning("unknown link event: %s", event)
else:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
self.app.canvas.organize()
self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wired_edge(event.link)
self.app.manager.delete_wired_edge(event.link)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wired_edge(event.link)
self.app.manager.update_wired_edge(event.link)
else:
logging.warning("unknown link event: %s", event)
@ -243,13 +239,13 @@ class CoreClient:
canvas_node.update_icon(node.icon)
elif event.message_type == MessageType.DELETE:
canvas_node = self.canvas_nodes[node.id]
self.app.canvas.clear_selection()
self.app.canvas.select_object(canvas_node.id)
self.app.canvas.delete_selected_objects()
canvas_node.canvas.clear_selection()
canvas_node.canvas.select_object(canvas_node.id)
canvas_node.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD:
if node.id in self.session.nodes:
logging.error("core node already exists: %s", node)
self.app.canvas.add_core_node(node)
self.app.manager.add_core_node(node)
else:
logging.warning("unknown node event: %s", event)
@ -262,7 +258,7 @@ class CoreClient:
if self.handling_throughputs:
self.handling_throughputs.cancel()
self.handling_throughputs = None
self.app.canvas.clear_throughputs()
self.app.manager.clear_throughputs()
def cancel_events(self) -> None:
if self.handling_events:
@ -293,7 +289,7 @@ class CoreClient:
)
return
logging.debug("handling throughputs event: %s", event)
self.app.after(0, self.app.canvas.set_throughputs, event)
self.app.after(0, self.app.manager.set_throughputs, event)
def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
self.app.after(0, self.app.statusbar.set_cpu, event.usage)
@ -315,9 +311,7 @@ class CoreClient:
self.session.id, self.handle_events
)
self.ifaces_manager.joined(self.session.links)
self.app.canvas.reset_and_redraw(self.session)
self.parse_metadata()
self.app.canvas.organize()
self.app.manager.join(self.session)
if self.is_runtime():
self.show_mobility_players()
self.app.after(0, self.app.joined_session_update)
@ -334,23 +328,7 @@ class CoreClient:
logging.debug("canvas metadata: %s", canvas_config)
if canvas_config:
canvas_config = json.loads(canvas_config)
gridlines = canvas_config.get("gridlines", True)
self.app.canvas.show_grid.set(gridlines)
fit_image = canvas_config.get("fit_image", False)
self.app.canvas.adjust_to_dim.set(fit_image)
wallpaper_style = canvas_config.get("wallpaper-style", 1)
self.app.canvas.scale_option.set(wallpaper_style)
width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height
dimensions = canvas_config.get("dimensions", [width, height])
self.app.canvas.redraw_canvas(dimensions)
wallpaper = canvas_config.get("wallpaper")
if wallpaper:
wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper))
self.app.canvas.set_wallpaper(wallpaper)
else:
self.app.canvas.redraw_canvas()
self.app.canvas.set_wallpaper(None)
self.app.manager.parse_metadata(canvas_config)
# load saved shapes
shapes_config = config.get("shapes")
@ -358,28 +336,7 @@ class CoreClient:
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logging.debug("loading shape: %s", shape_config)
shape_type = shape_config["type"]
try:
shape_type = ShapeType(shape_type)
coords = shape_config["iconcoords"]
data = AnnotationData(
shape_config["label"],
shape_config["fontfamily"],
shape_config["fontsize"],
shape_config["labelcolor"],
shape_config["color"],
shape_config["border"],
shape_config["width"],
shape_config["bold"],
shape_config["italic"],
shape_config["underline"],
)
shape = Shape(
self.app, self.app.canvas, shape_type, *coords, data=data
)
self.app.canvas.shapes[shape.id] = shape
except ValueError:
logging.exception("unknown shape: %s", shape_type)
Shape.from_metadata(self.app, shape_config)
# load edges config
edges_config = config.get("edges")
@ -392,6 +349,17 @@ class CoreClient:
edge.color = edge_config["color"]
edge.redraw()
# read hidden nodes
hidden = config.get("hidden")
if hidden:
hidden = json.loads(hidden)
for _id in hidden:
canvas_node = self.canvas_nodes.get(_id)
if canvas_node:
canvas_node.hide()
else:
logging.warning("invalid node to hide: %s", _id)
def create_new_session(self) -> None:
"""
Create a new session
@ -557,26 +525,14 @@ class CoreClient:
def set_metadata(self) -> None:
# create canvas data
wallpaper_path = None
if self.app.canvas.wallpaper_file:
wallpaper = Path(self.app.canvas.wallpaper_file)
if BACKGROUNDS_PATH == wallpaper.parent:
wallpaper_path = wallpaper.name
else:
wallpaper_path = str(wallpaper)
canvas_config = {
"wallpaper": wallpaper_path,
"wallpaper-style": self.app.canvas.scale_option.get(),
"gridlines": self.app.canvas.show_grid.get(),
"fit_image": self.app.canvas.adjust_to_dim.get(),
"dimensions": self.app.canvas.current_dimensions,
}
canvas_config = self.app.manager.get_metadata()
canvas_config = json.dumps(canvas_config)
# create shapes data
shapes = []
for shape in self.app.canvas.shapes.values():
shapes.append(shape.metadata())
for canvas in self.app.manager.all():
for shape in canvas.shapes.values():
shapes.append(shape.metadata())
shapes = json.dumps(shapes)
# create edges config
@ -588,8 +544,14 @@ class CoreClient:
edges_config.append(edge_config)
edges_config = json.dumps(edges_config)
# create hidden metadata
hidden = [x.core_node.id for x in self.canvas_nodes.values() if x.hidden]
hidden = json.dumps(hidden)
# save metadata
metadata = dict(canvas=canvas_config, shapes=shapes, edges=edges_config)
metadata = dict(
canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden
)
response = self.client.set_session_metadata(self.session.id, metadata)
logging.debug("set session metadata %s, result: %s", metadata, response)

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
from core.gui import validation
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -22,9 +22,9 @@ class SizeAndScaleDialog(Dialog):
create an instance for size and scale object
"""
super().__init__(app, "Canvas Size and Scale")
self.canvas: CanvasGraph = self.app.canvas
self.manager: CanvasManager = self.app.manager
self.section_font: font.Font = font.Font(weight=font.BOLD)
width, height = self.canvas.current_dimensions
width, height = self.manager.current_dimensions
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.session.location
@ -189,9 +189,7 @@ class SizeAndScaleDialog(Dialog):
def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:
self.canvas.redraw_wallpaper()
self.manager.redraw_canvases((width, height))
location = self.app.core.session.location
location.x = self.x.get()
location.y = self.y.get()

View file

@ -23,7 +23,7 @@ class CanvasWallpaperDialog(Dialog):
create an instance of CanvasWallpaper object
"""
super().__init__(app, "Canvas Background")
self.canvas: CanvasGraph = self.app.canvas
self.canvas: CanvasGraph = self.app.manager.current()
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
value=self.canvas.adjust_to_dim.get()
@ -161,7 +161,6 @@ class CanvasWallpaperDialog(Dialog):
def click_apply(self) -> None:
self.canvas.scale_option.set(self.scale_option.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.show_grid.click_handler()
filename = self.filename.get()
if not filename:
filename = None

View file

@ -105,9 +105,13 @@ class FindDialog(Dialog):
self.tree.selection_set(results[0])
def close_dialog(self) -> None:
self.app.canvas.delete("find")
self.clear_find()
self.destroy()
def clear_find(self):
for canvas in self.app.manager.all():
canvas.delete("find")
def click_select(self, _event: tk.Event = None) -> None:
"""
find the node that matches search criteria, circle around that node
@ -116,13 +120,13 @@ class FindDialog(Dialog):
"""
item = self.tree.selection()
if item:
self.app.canvas.delete("find")
self.clear_find()
node_id = int(self.tree.item(item, "text"))
canvas_node = self.app.core.get_canvas_node(node_id)
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
self.app.manager.select(canvas_node.canvas.id)
x0, y0, x1, y1 = canvas_node.canvas.bbox(canvas_node.id)
dist = 5 * self.app.guiconfig.scale
self.app.canvas.create_oval(
canvas_node.canvas.create_oval(
x0 - dist,
y0 - dist,
x1 + dist,
@ -132,9 +136,9 @@ class FindDialog(Dialog):
width=3.0 * self.app.guiconfig.scale,
)
_x, _y, _, _ = self.app.canvas.bbox(canvas_node.id)
oid = self.app.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = self.app.canvas.bbox(oid[0])
_x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id)
oid = canvas_node.canvas.find_withtag("rectangle")
x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0])
logging.debug("Dist to most left: %s", abs(x0 - _x))
logging.debug("White canvas width: %s", abs(x0 - x1))
@ -150,5 +154,5 @@ class FindDialog(Dialog):
xscroll_fraction = xscroll_fraction - 0.05
if yscroll_fraction > 0.05:
yscroll_fraction = yscroll_fraction - 0.05
self.app.canvas.xview_moveto(xscroll_fraction)
self.app.canvas.yview_moveto(yscroll_fraction)
canvas_node.canvas.xview_moveto(xscroll_fraction)
canvas_node.canvas.yview_moveto(yscroll_fraction)

View file

@ -70,10 +70,10 @@ class LinkConfigurationDialog(Dialog):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
src_label = self.app.canvas.nodes[self.edge.src].core_node.name
src_label = self.edge.src.core_node.name
if self.edge.link.iface1:
src_label += f":{self.edge.link.iface1.name}"
dst_label = self.app.canvas.nodes[self.edge.dst].core_node.name
dst_label = self.edge.dst.core_node.name
if self.edge.link.iface2:
dst_label += f":{self.edge.link.iface2.name}"
label = ttk.Label(
@ -316,10 +316,8 @@ class LinkConfigurationDialog(Dialog):
"""
populate link config to the table
"""
width = self.app.canvas.itemcget(self.edge.id, "width")
self.width.set(width)
color = self.app.canvas.itemcget(self.edge.id, "fill")
self.color.set(color)
self.width.set(self.edge.width)
self.color.set(self.edge.color)
link = self.edge.link
if link.options:
self.bandwidth.set(str(link.options.bandwidth))

View file

@ -134,7 +134,8 @@ class PreferencesDialog(Dialog):
# scale toolbar and canvas items
self.app.toolbar.scale()
self.app.canvas.scale_graph()
for canvas in self.app.manager.all():
canvas.scale_graph()
def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get()

View file

@ -199,7 +199,10 @@ class SessionsDialog(Dialog):
logging.debug("delete session: %s", self.selected_session)
self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session)
if self.selected_session == self.app.core.session.id:
session_id = None
if self.app.core.session:
session_id = self.app.core.session.id
if self.selected_session == session_id:
self.click_new()
self.destroy()
self.click_select()

View file

@ -27,7 +27,7 @@ class ShapeDialog(Dialog):
else:
title = "Add Text"
super().__init__(app, title)
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = app.manager.current()
self.fill: Optional[ttk.Label] = None
self.border: Optional[ttk.Label] = None
self.shape: "Shape" = shape

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -17,16 +17,16 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config")
self.canvas: CanvasGraph = app.canvas
self.manager: CanvasManager = app.manager
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar(
value=self.canvas.throughput_threshold
value=self.manager.throughput_threshold
)
self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
self.color: str = self.canvas.throughput_color
self.width: tk.IntVar = tk.IntVar(value=self.manager.throughput_width)
self.color: str = self.manager.throughput_color
self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1)
self.draw()
@ -106,7 +106,7 @@ class ThroughputDialog(Dialog):
self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get()
self.canvas.throughput_color = self.color
self.manager.throughput_threshold = self.threshold.get()
self.manager.throughput_width = self.width.get()
self.manager.throughput_color = self.color
self.destroy()

View file

@ -21,7 +21,7 @@ RANGE_WIDTH: int = 3
class WlanConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = app.manager.current()
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None

View file

@ -79,15 +79,13 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
def draw(self) -> None:
link = self.edge.link
src_canvas_node = self.app.canvas.nodes[self.edge.src]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.canvas.nodes[self.edge.dst]
dst_node = dst_canvas_node.core_node
src_node = self.edge.src.core_node
dst_node = self.edge.dst.core_node
# find interface for each node connected to network
net_id = link.network_id
iface1 = get_iface(src_canvas_node, net_id)
iface2 = get_iface(dst_canvas_node, net_id)
iface1 = get_iface(self.edge.src, net_id)
iface2 = get_iface(self.edge.dst, net_id)
frame = DetailsFrame(self)
frame.grid(sticky=tk.EW)

View file

@ -1,18 +1,25 @@
import functools
import logging
import math
import tkinter as tk
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING, Optional, Tuple, Union
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
from core.gui.images import ImageEnum
from core.gui.nodeutils import ICON_SIZE
from core.gui.utils import bandwidth_text, delay_jitter_text
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.graph.node import CanvasNode
TEXT_DISTANCE: int = 60
EDGE_WIDTH: int = 3
@ -33,6 +40,19 @@ def create_edge_token(link: Link) -> str:
return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}"
def node_label_positions(
src_x: int, src_y: int, dst_x: int, dst_y: int
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x ** 2 + v_y ** 2)
if v_len == 0:
u_x, u_y = 0.0, 0.0
else:
u_x, u_y = v_x / v_len, v_y / v_len
offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
return (src_x + offset_x, src_y + offset_y), (dst_x - offset_x, dst_y - offset_y)
def arc_edges(edges) -> None:
if not edges:
return
@ -62,24 +82,114 @@ def arc_edges(edges) -> None:
edge.redraw()
class ShadowNode:
def __init__(
self, app: "Application", canvas: "CanvasGraph", node: "CanvasNode"
) -> None:
self.app: "Application" = app
self.canvas: "CanvasGraph" = canvas
self.node: "CanvasNode" = node
self.id: Optional[int] = None
self.text_id: Optional[int] = None
self.image: PhotoImage = self.app.get_icon(ImageEnum.SHADOW, ICON_SIZE)
self.draw()
def draw(self) -> None:
x, y = self.node.position()
self.id: int = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
)
self.text_id = self.canvas.create_text(
x,
y + 20,
text=f"{self.node.get_label()} [{self.node.canvas.id}]",
tags=tags.NODE_LABEL,
font=self.app.icon_text_font,
fill="#0000CD",
state=self.app.manager.show_node_labels.state(),
justify=tk.CENTER,
)
self.canvas.shadow_nodes[self.id] = self
self.canvas.shadow_core_nodes[self.node.core_node.id] = self
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def should_delete(self) -> bool:
for edge in self.node.edges:
other_node = edge.src
if self.node == edge.src:
other_node = edge.dst
if not other_node.is_wireless() and other_node.canvas == self.canvas:
return False
return True
def motion(self, x_offset, y_offset) -> None:
original_position = self.position()
self.canvas.move(self.id, x_offset, y_offset)
# check new position
bbox = self.canvas.bbox(self.id)
if not self.canvas.valid_position(*bbox):
self.canvas.coords(self.id, original_position)
return
# move text and selection box
self.canvas.move(self.text_id, x_offset, y_offset)
self.canvas.move_selection(self.id, x_offset, y_offset)
# move edges
for edge in self.node.edges:
edge.move_shadow(self)
for edge in self.node.wireless_edges:
edge.move_shadow(self)
def delete(self):
self.canvas.shadow_nodes.pop(self.id, None)
self.canvas.shadow_core_nodes.pop(self.node.core_node.id, None)
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)
def hide(self) -> None:
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.itemconfig(self.text_id, state=tk.HIDDEN)
def show(self) -> None:
self.canvas.itemconfig(self.id, state=tk.NORMAL)
self.canvas.itemconfig(self.text_id, state=tk.NORMAL)
class Edge:
tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
def __init__(
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
self.app: "Application" = app
self.manager: CanvasManager = app.manager
self.id: Optional[int] = None
self.src: int = src
self.dst: int = dst
self.id2: Optional[int] = None
self.src: "CanvasNode" = src
self.src_shadow: Optional[ShadowNode] = None
self.dst: Optional["CanvasNode"] = dst
self.dst_shadow: Optional[ShadowNode] = None
self.arc: int = 0
self.token: Optional[str] = None
self.src_label: Optional[int] = None
self.src_label2: Optional[int] = None
self.middle_label: Optional[int] = None
self.middle_label2: Optional[int] = None
self.dst_label: Optional[int] = None
self.dst_label2: Optional[int] = None
self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH
self.linked_wireless: bool = False
self.hidden: bool = False
if self.dst:
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale
return self.width * self.app.app_scale
def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
@ -110,11 +220,57 @@ class Edge:
arc_y = (perp_m * arc_x) + b
return arc_x, arc_y
def draw(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str
) -> None:
def arc_common_edges(self) -> None:
common_edges = list(self.src.edges & self.dst.edges)
common_edges += list(self.src.wireless_edges & self.dst.wireless_edges)
arc_edges(common_edges)
def has_shadows(self) -> bool:
# still drawing
if not self.dst:
return False
return self.src.canvas != self.dst.canvas
def draw(self, state: str) -> None:
if not self.has_shadows():
dst = self.dst if self.dst else self.src
self.id = self.draw_edge(self.src.canvas, self.src, dst, state)
elif self.linked_wireless:
if self.src.is_wireless():
self.src_shadow = self.dst.canvas.get_shadow(self.src)
self.id2 = self.draw_edge(
self.dst.canvas, self.src_shadow, self.dst, state
)
if self.dst.is_wireless():
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
self.id = self.draw_edge(
self.src.canvas, self.src, self.dst_shadow, state
)
else:
# draw shadow nodes and 2 lines
self.src_shadow = self.dst.canvas.get_shadow(self.src)
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
self.id = self.draw_edge(self.src.canvas, self.src, self.dst_shadow, state)
self.id2 = self.draw_edge(self.dst.canvas, self.src_shadow, self.dst, state)
self.src.canvas.organize()
self.dst.canvas.organize()
logging.info(
"drawed edge: src shadow(%s) dst shadow(%s)",
self.src_shadow,
self.dst_shadow,
)
def draw_edge(
self,
canvas: "CanvasGraph",
src: Union["CanvasNode", "ShadowNode"],
dst: Union["CanvasNode", "ShadowNode"],
state: str,
) -> int:
src_pos = src.position()
dst_pos = dst.position()
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line(
return canvas.create_line(
*src_pos,
*arc_pos,
*dst_pos,
@ -126,112 +282,243 @@ class Edge:
)
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
self.move_src(src_pos)
def middle_label_pos(self) -> Tuple[float, float]:
_, _, x, y, _, _ = self.canvas.coords(self.id)
return x, y
self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
self.move_src()
if self.id2:
self.dst.canvas.itemconfig(
self.id2, width=self.scaled_width(), fill=self.color
)
self.move_dst()
def middle_label_text(self, text: str) -> None:
if self.middle_label is None:
x, y = self.middle_label_pos()
self.middle_label = self.canvas.create_text(
_, _, x, y, _, _ = self.src.canvas.coords(self.id)
self.middle_label = self.src.canvas.create_text(
x,
y,
font=self.canvas.app.edge_font,
font=self.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.canvas.show_link_labels.state(),
state=self.manager.show_link_labels.state(),
)
if self.id2:
_, _, x, y, _, _ = self.dst.canvas.coords(self.id2)
self.middle_label2 = self.dst.canvas.create_text(
x,
y,
font=self.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.middle_label, text=text)
self.src.canvas.itemconfig(self.middle_label, text=text)
if self.middle_label2:
self.dst.canvas.itemconfig(self.middle_label2, text=text)
def clear_middle_label(self) -> None:
self.canvas.delete(self.middle_label)
self.src.canvas.delete(self.middle_label)
self.middle_label = None
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v_x, v_y = dst_x - src_x, dst_y - src_y
v_len = math.sqrt(v_x ** 2 + v_y ** 2)
if v_len == 0:
u_x, u_y = 0.0, 0.0
else:
u_x, u_y = v_x / v_len, v_y / v_len
offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
return (
(src_x + offset_x, src_y + offset_y),
(dst_x - offset_x, dst_y - offset_y),
)
if self.middle_label2:
self.dst.canvas.delete(self.middle_label2)
self.middle_label2 = None
def src_label_text(self, text: str) -> None:
if self.src_label is None:
src_pos, _ = self.node_label_positions()
self.src_label = self.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
if self.src_label is None and self.src_label2 is None:
if self.id:
logging.info("src label id")
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
self.src_label = self.src.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
if self.id2:
logging.info("src label id2")
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
self.src_label2 = self.dst.canvas.create_text(
*src_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.src_label, text=text)
if self.src_label:
self.src.canvas.itemconfig(self.src_label, text=text)
if self.src_label2:
self.dst.canvas.itemconfig(self.src_label2, text=text)
def dst_label_text(self, text: str) -> None:
if self.dst_label is None:
_, dst_pos = self.node_label_positions()
self.dst_label = self.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.canvas.app.edge_font,
tags=tags.LINK_LABEL,
state=self.canvas.show_link_labels.state(),
)
if self.dst_label is None and self.dst_label2 is None:
if self.id:
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
self.dst_label = self.src.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
if self.id2:
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
self.dst_label2 = self.dst.canvas.create_text(
*dst_pos,
text=text,
justify=tk.CENTER,
font=self.app.edge_font,
tags=tags.LINK_LABEL,
state=self.manager.show_link_labels.state(),
)
else:
self.canvas.itemconfig(self.dst_label, text=text)
if self.dst_label:
self.src.canvas.itemconfig(self.dst_label, text=text)
if self.dst_label2:
self.dst.canvas.itemconfig(self.dst_label2, text=text)
def move_node(self, node_id: int, pos: Tuple[float, float]) -> None:
if self.src == node_id:
self.move_src(pos)
else:
self.move_dst(pos)
def move_dst(self, dst_pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
def drawing(self, pos: Tuple[float, float]) -> None:
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
self.moved(src_pos, pos)
def move_src(self, src_pos: Tuple[float, float]) -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id)
def move_node(self, node: "CanvasNode") -> None:
if self.src == node:
self.move_src()
else:
self.move_dst()
def move_shadow(self, node: "ShadowNode") -> None:
if self.src_shadow == node:
self.move_src_shadow()
elif self.dst_shadow == node:
self.move_dst_shadow()
def move_src_shadow(self) -> None:
if not self.id2:
return
_, _, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
dst_pos = dst_x, dst_y
self.moved(src_pos, dst_pos)
self.moved2(self.src_shadow.position(), dst_pos)
def move_dst_shadow(self) -> None:
if not self.id:
return
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, self.dst_shadow.position())
def move_dst(self) -> None:
if self.dst.is_wireless() and self.has_shadows():
return
dst_pos = self.dst.position()
if self.id2:
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id2)
src_pos = src_x, src_y
self.moved2(src_pos, dst_pos)
elif self.id:
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id)
src_pos = src_x, src_y
self.moved(src_pos, dst_pos)
def move_src(self) -> None:
if not self.id:
return
_, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
dst_pos = dst_x, dst_y
self.moved(self.src.position(), dst_pos)
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions()
self.src.canvas.coords(self.middle_label, *arc_pos)
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
if self.src_label:
self.canvas.coords(self.src_label, *src_pos)
self.src.canvas.coords(self.src_label, *src_pos)
if self.dst_label:
self.canvas.coords(self.dst_label, *dst_pos)
self.src.canvas.coords(self.dst_label, *dst_pos)
def moved2(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos)
if self.middle_label2:
self.dst.canvas.coords(self.middle_label2, *arc_pos)
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
if self.src_label2:
self.dst.canvas.coords(self.src_label2, *src_pos)
if self.dst_label2:
self.dst.canvas.coords(self.dst_label2, *dst_pos)
def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.src_label)
self.canvas.delete(self.dst_label)
self.src.canvas.delete(self.id)
self.src.canvas.delete(self.src_label)
self.src.canvas.delete(self.dst_label)
if self.dst:
self.dst.canvas.delete(self.id2)
self.dst.canvas.delete(self.src_label2)
self.dst.canvas.delete(self.dst_label2)
if self.src_shadow and self.src_shadow.should_delete():
self.src_shadow.delete()
self.src_shadow = None
if self.dst_shadow and self.dst_shadow.should_delete():
self.dst_shadow.delete()
self.dst_shadow = None
self.clear_middle_label()
self.id = None
self.id2 = None
self.src_label = None
self.src_label2 = None
self.dst_label = None
self.dst_label2 = None
self.manager.edges.pop(self.token, None)
def hide(self) -> None:
self.hidden = True
if self.src_shadow:
self.src_shadow.hide()
if self.dst_shadow:
self.dst_shadow.hide()
self.src.canvas.itemconfigure(self.id, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.src_label, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.dst_label, state=tk.HIDDEN)
self.src.canvas.itemconfigure(self.middle_label, state=tk.HIDDEN)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.src_label2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.dst_label2, state=tk.HIDDEN)
self.dst.canvas.itemconfigure(self.middle_label2, state=tk.HIDDEN)
def show(self) -> None:
self.hidden = False
if self.src_shadow:
self.src_shadow.show()
if self.dst_shadow:
self.dst_shadow.show()
self.src.canvas.itemconfigure(self.id, state=tk.NORMAL)
self.src.canvas.itemconfigure(self.src_label, state=tk.NORMAL)
self.src.canvas.itemconfigure(self.dst_label, state=tk.NORMAL)
self.src.canvas.itemconfigure(self.middle_label, state=tk.NORMAL)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=tk.NORMAL)
self.dst.canvas.itemconfigure(self.src_label2, state=tk.NORMAL)
self.dst.canvas.itemconfigure(self.dst_label2, state=tk.NORMAL)
self.dst.canvas.itemconfigure(self.middle_label2, state=tk.NORMAL)
class CanvasWirelessEdge(Edge):
@ -239,35 +526,38 @@ class CanvasWirelessEdge(Edge):
def __init__(
self,
canvas: "CanvasGraph",
src: int,
dst: int,
app: "Application",
src: "CanvasNode",
dst: "CanvasNode",
network_id: int,
token: str,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
link: Link,
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
super().__init__(app, src, dst)
self.network_id: int = network_id
self.link: Link = link
self.token: str = token
self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR
self.color: str = color
self.draw(src_pos, dst_pos, self.canvas.show_wireless.state())
self.draw(self.manager.show_wireless.state())
if link.label:
self.middle_label_text(link.label)
self.set_binding()
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
if self.id2 is not None:
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
)
self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)
def delete(self) -> None:
self.src.wireless_edges.discard(self)
self.dst.wireless_edges.remove(self)
super().delete()
class CanvasEdge(Edge):
@ -276,47 +566,40 @@ class CanvasEdge(Edge):
"""
def __init__(
self,
canvas: "CanvasGraph",
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
"""
Create an instance of canvas edge object
"""
super().__init__(canvas, src)
super().__init__(app, src, dst)
self.text_src: Optional[int] = None
self.text_dst: Optional[int] = None
self.link: Optional[Link] = None
self.linked_wireless: bool = False
self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos, tk.NORMAL)
self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
self.draw(tk.NORMAL)
def is_customized(self) -> bool:
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
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)
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def set_bindings(self) -> None:
if self.id:
show_context = functools.partial(self.show_context, self.src.canvas)
self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
if self.id2:
show_context = functools.partial(self.show_context, self.dst.canvas)
self.dst.canvas.tag_bind(self.id2, "<ButtonRelease-3>", show_context)
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
def iface_label(self, iface: Interface) -> str:
label = ""
if iface.name and self.canvas.show_iface_names.get():
if iface.name and self.manager.show_iface_names.get():
label = f"{iface.name}"
if iface.ip4 and self.canvas.show_ip4s.get():
if iface.ip4 and self.manager.show_ip4s.get():
label = f"{label}\n" if label else ""
label += f"{iface.ip4}/{iface.ip4_mask}"
if iface.ip6 and self.canvas.show_ip6s.get():
if iface.ip6 and self.manager.show_ip6s.get():
label = f"{label}\n" if label else ""
label += f"{iface.ip6}/{iface.ip6_mask}"
return label
@ -346,77 +629,98 @@ class CanvasEdge(Edge):
return
if self.link.options.loss == EDGE_LOSS:
state = tk.HIDDEN
self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id)
if self.id:
self.src.canvas.addtag_withtag(tags.LOSS_EDGES, self.id)
if self.id2:
self.dst.canvas.addtag_withtag(tags.LOSS_EDGES, self.id2)
else:
state = tk.NORMAL
self.canvas.dtag(self.id, tags.LOSS_EDGES)
if self.canvas.show_loss_links.state() == tk.HIDDEN:
self.canvas.itemconfigure(self.id, state=state)
if self.id:
self.src.canvas.dtag(self.id, tags.LOSS_EDGES)
if self.id2:
self.dst.canvas.dtag(self.id2, tags.LOSS_EDGES)
if self.manager.show_loss_links.state() == tk.HIDDEN:
if self.id:
self.src.canvas.itemconfigure(self.id, state=state)
if self.id2:
self.dst.canvas.itemconfigure(self.id2, state=state)
def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput
text = f"{throughput:.3f} kbps"
self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold:
color = self.canvas.throughput_color
width = self.canvas.throughput_width
if throughput > self.manager.throughput_threshold:
color = self.manager.throughput_color
width = self.manager.throughput_width
else:
color = self.color
width = self.scaled_width()
self.canvas.itemconfig(self.id, fill=color, width=width)
self.src.canvas.itemconfig(self.id, fill=color, width=width)
if self.id2:
self.dst.canvas.itemconfig(self.id2, fill=color, width=width)
def clear_throughput(self) -> None:
self.clear_middle_label()
if not self.linked_wireless:
self.draw_link_options()
def complete(self, dst: int, linked_wireless: bool) -> None:
def complete(self, dst: "CanvasNode") -> None:
self.dst = dst
self.linked_wireless = linked_wireless
dst_pos = self.canvas.coords(self.dst)
self.move_dst(dst_pos)
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
self.set_bindings()
self.check_wireless()
logging.debug("draw wired link from node %s to node %s", self.src, dst)
def check_wireless(self) -> None:
if self.linked_wireless:
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.dtag(self.id, tags.EDGE)
self._check_antenna()
def _check_antenna(self) -> None:
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
if is_src_wireless or is_dst_wireless:
if is_src_wireless and not is_dst_wireless:
dst_node.add_antenna()
elif not is_src_wireless and is_dst_wireless:
src_node.add_antenna()
else:
src_node.add_antenna()
if not self.linked_wireless:
return
if self.id:
self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.src.canvas.dtag(self.id, tags.EDGE)
if self.id2:
self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
self.dst.canvas.dtag(self.id2, tags.EDGE)
# add antenna to node
if self.src.is_wireless() and not self.dst.is_wireless():
self.dst.add_antenna()
elif not self.src.is_wireless() and self.dst.is_wireless():
self.src.add_antenna()
else:
self.src.add_antenna()
def reset(self) -> None:
self.canvas.delete(self.middle_label)
self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
if self.middle_label:
self.src.canvas.delete(self.middle_label)
self.middle_label = None
if self.middle_label2:
self.dst.canvas.delete(self.middle_label2)
self.middle_label2 = None
if self.id:
self.src.canvas.itemconfig(
self.id, fill=self.color, width=self.scaled_width()
)
if self.id2:
self.dst.canvas.itemconfig(
self.id2, fill=self.color, width=self.scaled_width()
)
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self)
self.app.display_info(EdgeInfoFrame, app=self.app, edge=self)
def show_context(self, event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root)
def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
context: tk.Menu = tk.Menu(canvas)
themes.style_menu(context)
context.add_command(label="Configure", command=self.click_configure)
context.add_command(label="Delete", command=self.click_delete)
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
context.entryconfigure(1, state=state)
context.tk_popup(event.x_root, event.y_root)
def click_delete(self) -> None:
self.canvas.delete_edge(self)
self.delete()
def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self)
dialog = LinkConfigurationDialog(self.app, self)
dialog.show()
def draw_link_options(self):
@ -455,3 +759,20 @@ class CanvasEdge(Edge):
lines.append(dup_line)
label = "\n".join(lines)
self.middle_label_text(label)
def delete(self) -> None:
self.src.edges.discard(self)
if self.dst:
self.dst.edges.discard(self)
if self.link.iface1:
del self.src.ifaces[self.link.iface1.id]
if self.link.iface2:
del self.dst.ifaces[self.link.iface2.id]
if self.src.is_wireless():
self.dst.delete_antenna()
if self.dst.is_wireless():
self.src.delete_antenna()
self.app.core.deleted_canvas_edges([self])
super().delete()
if self.dst:
self.arc_common_edges()

View file

@ -1,79 +1,59 @@
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import (
Interface,
Link,
LinkType,
Node,
Session,
ThroughputsEvent,
)
from core.api.grpc.wrappers import Interface, Link
from core.gui import appconfig
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.edges import (
EDGE_WIDTH,
CanvasEdge,
CanvasWirelessEdge,
Edge,
arc_edges,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, ShadowNode
from core.gui.graph.enums import GraphMode, ScaleOption
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 NodeDraw, NodeUtils
from core.gui.images import TypeToImage
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.manager import CanvasManager
from core.gui.coreclient import CoreClient
ZOOM_IN = 1.1
ZOOM_OUT = 0.9
ICON_SIZE = 48
MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT}
class ShowVar(BooleanVar):
def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None:
super().__init__(value=value)
self.canvas = canvas
self.tag = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None:
self.canvas.itemconfigure(self.tag, state=self.state())
ZOOM_IN: float = 1.1
ZOOM_OUT: float = 0.9
ICON_SIZE: int = 48
MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
BACKGROUND_COLOR: str = "#cccccc"
class CanvasGraph(tk.Canvas):
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
self,
master: tk.BaseWidget,
app: "Application",
manager: "CanvasManager",
core: "CoreClient",
_id: int,
dimensions: Tuple[int, int],
) -> None:
super().__init__(master, highlightthickness=0, background="#cccccc")
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.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[str, CanvasEdge] = {}
self.shadow_nodes: Dict[int, ShadowNode] = {}
self.shapes: Dict[int, Shape] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
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]] = {}
@ -81,10 +61,7 @@ class CanvasGraph(tk.Canvas):
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: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
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)
@ -98,23 +75,6 @@ class CanvasGraph(tk.Canvas):
self.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)
# throughput related
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# drawing related
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_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, 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()
@ -126,7 +86,7 @@ class CanvasGraph(tk.Canvas):
if self.rect is not None:
self.delete(self.rect)
if not dimensions:
dimensions = self.default_dimensions
dimensions = self.manager.default_dimensions
self.current_dimensions = dimensions
self.rect = self.create_rectangle(
0,
@ -139,34 +99,6 @@ class CanvasGraph(tk.Canvas):
)
self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: Session) -> None:
# 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_iface_names.set(False)
self.show_ip4s.set(True)
self.show_ip6s.set(True)
self.show_loss_links.set(True)
# delete any existing drawn items
for tag in tags.RESET_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) -> None:
"""
Bind any mouse events or hot keys to the matching action
@ -183,6 +115,12 @@ 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_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
@ -204,16 +142,6 @@ class CanvasGraph(tk.Canvas):
valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright
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
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
edge = self.core.iface_to_edge.get(iface_to_edge_id)
if edge:
edge.set_throughput(throughput)
def draw_grid(self) -> None:
"""
Create grid.
@ -228,123 +156,6 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect)
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 delete_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
self.delete_edge(edge)
def update_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.link.options = deepcopy(link.options)
edge.draw_link_options()
edge.check_options()
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_wireless_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, network_id, token, src_pos, dst_pos, link
)
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)
self.arc_common_edges(edge)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_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)
self.arc_common_edges(edge)
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_wireless_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 add_core_node(self, core_node: Node) -> None:
logging.debug("adding node: %s", core_node)
# if the gui can't find node's image, default to the "edit-node" image
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
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.set_canvas_node(core_node, node)
def draw_session(self, session: Session) -> None:
"""
Draw existing session.
"""
# draw existing nodes
for core_node in session.nodes.values():
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
self.add_core_node(core_node)
# draw existing links
for link in session.links:
logging.debug("drawing link: %s", link)
canvas_node1 = self.core.get_canvas_node(link.node1_id)
canvas_node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link)
else:
self.add_wired_edge(canvas_node1, canvas_node2, link)
def stopped_session(self) -> None:
# 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 throughputs
self.clear_throughputs()
def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
"""
Convert window coordinate to canvas coordinate
@ -363,14 +174,12 @@ class CanvasGraph(tk.Canvas):
for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id:
continue
if _id in self.nodes:
elif _id in self.nodes:
selected = _id
break
if _id in self.shapes:
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:
@ -381,13 +190,13 @@ class CanvasGraph(tk.Canvas):
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
if self.mode == GraphMode.ANNOTATION:
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.mode == GraphMode.SELECT:
elif self.manager.mode == GraphMode.SELECT:
self.focus_set()
if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id)
@ -403,61 +212,35 @@ class CanvasGraph(tk.Canvas):
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:
logging.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.mode == GraphMode.NODE:
elif self.manager.mode == GraphMode.NODE:
self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE
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
# not drawing edge return
if edge is None:
return
# edge dst must be a node
logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected)
if not dst_node or not src_node:
if not dst_node:
edge.delete()
return
# edge dst is same as src, delete edge
if edge.src == self.selected:
# check if node can be linked
if not edge.src.is_linkable(dst_node):
edge.delete()
return
# rj45 nodes can only support one link
if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges:
edge.delete()
return
if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges:
edge.delete()
return
# only 1 link between bridge based nodes
is_src_bridge = NodeUtils.is_bridge_node(src_node.core_node)
is_dst_bridge = NodeUtils.is_bridge_node(dst_node.core_node)
common_links = src_node.edges & dst_node.edges
if all([is_src_bridge, is_dst_bridge, common_links]):
edge.delete()
return
# finalize edge creation
self.complete_edge(src_node, dst_node, edge)
def arc_common_edges(self, edge: Edge) -> None:
src_node = self.nodes[edge.src]
dst_node = self.nodes[edge.dst]
common_edges = list(src_node.edges & dst_node.edges)
common_edges += list(src_node.wireless_edges & dst_node.wireless_edges)
arc_edges(common_edges)
self.manager.complete_edge(edge, dst_node)
def select_object(self, object_id: int, choose_multiple: bool = False) -> None:
"""
@ -504,28 +287,16 @@ class CanvasGraph(tk.Canvas):
# 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:
while canvas_node.edges:
edge = canvas_node.edges.pop()
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_iface = edge.link.iface1
if edge.src == object_id:
other_id = edge.dst
other_iface = edge.link.iface2
other_node = self.nodes[other_id]
other_node.edges.remove(edge)
if other_iface:
del other_node.ifaces[other_iface.id]
if is_wireless:
other_node.delete_antenna()
# delete node
canvas_node.delete()
nodes.append(canvas_node)
# delete shape
if object_id in self.shapes:
@ -534,27 +305,21 @@ class CanvasGraph(tk.Canvas):
self.selection.clear()
self.core.deleted_canvas_nodes(nodes)
self.core.deleted_canvas_edges(edges)
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.link.iface1:
del src_node.ifaces[edge.link.iface1.id]
dst_node = self.nodes[edge.dst]
dst_node.edges.discard(edge)
if edge.link.iface2:
del dst_node.ifaces[edge.link.iface2.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])
self.arc_common_edges(edge)
def hide_selected_objects(self) -> 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:
@ -588,13 +353,13 @@ class CanvasGraph(tk.Canvas):
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.manager.mode == GraphMode.EDGE and is_node:
node = self.nodes[selected]
self.drawing_edge = CanvasEdge(self.app, node)
self.organize()
if self.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type):
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,
@ -604,11 +369,11 @@ class CanvasGraph(tk.Canvas):
fill=self.app.toolbar.marker_frame.color,
outline="",
tags=(tags.MARKER, tags.ANNOTATION),
state=self.show_annotations.state(),
state=self.manager.show_annotations.state(),
)
return
if selected is None:
shape = Shape(self.app, self, self.annotation_type, x, y)
shape = Shape(self.app, self, self.manager.annotation_type, x, y)
self.selected = shape.id
self.shape_drawing = True
self.shapes[shape.id] = shape
@ -629,8 +394,18 @@ class CanvasGraph(tk.Canvas):
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
logging.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.mode == GraphMode.SELECT:
if self.manager.mode == GraphMode.SELECT:
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
self.select_box = shape
self.clear_selection()
@ -659,7 +434,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box:
self.select_box.delete()
self.select_box = None
if is_draw_shape(self.annotation_type) and self.shape_drawing:
if is_draw_shape(self.manager.annotation_type) and self.shape_drawing:
shape = self.shapes.pop(self.selected)
shape.delete()
self.shape_drawing = False
@ -669,14 +444,14 @@ class CanvasGraph(tk.Canvas):
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:
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.annotation_type):
elif is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get()
self.create_oval(
x - r,
@ -689,21 +464,26 @@ class CanvasGraph(tk.Canvas):
)
return
if self.mode == GraphMode.EDGE:
if self.manager.mode == GraphMode.EDGE:
return
# move selected objects
if self.selection:
for selected_id in self.selection:
if self.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
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)
if self.mode in MOVE_NODE_MODES and selected_id in self.nodes:
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.mode == GraphMode.SELECT:
if self.select_box and self.manager.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event) -> None:
@ -729,17 +509,23 @@ class CanvasGraph(tk.Canvas):
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
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
try:
image_enum = self.node_draw.image_enum
self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE)
image_enum = self.manager.node_draw.image_enum
self.manager.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE)
except AttributeError:
image_file = self.node_draw.image_file
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE)
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
image_file = self.manager.node_draw.image_file
self.manager.node_draw.image = self.app.get_custom_icon(
image_file, ICON_SIZE
)
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)
@ -847,7 +633,7 @@ class CanvasGraph(tk.Canvas):
# redraw gridlines to new canvas size
self.delete(tags.GRIDLINE)
self.draw_grid()
self.app.canvas.show_grid.click_handler()
self.app.manager.show_grid.click_handler()
def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get():
@ -871,7 +657,7 @@ class CanvasGraph(tk.Canvas):
self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]) -> None:
logging.debug("setting wallpaper: %s", filename)
logging.info("setting canvas(%s) background: %s", self.id, filename)
if filename:
img = Image.open(filename)
self.wallpaper = img
@ -884,44 +670,16 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_file = None
def is_selection_mode(self) -> bool:
return self.mode == GraphMode.SELECT
return self.manager.mode == GraphMode.SELECT
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)
edge = CanvasEdge(self.app, src)
self.manager.complete_edge(edge, dst)
return edge
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 copy(self) -> None:
if self.core.is_runtime():
logging.debug("copy is disabled during runtime state")
@ -952,7 +710,9 @@ class CanvasGraph(tk.Canvas):
)
if not copy:
continue
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
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()
@ -1039,30 +799,6 @@ class CanvasGraph(tk.Canvas):
)
self.tag_raise(tags.NODE)
def is_linked_wireless(self, src: int, dst: int) -> bool:
src_node = self.nodes[src]
dst_node = self.nodes[dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
# update the wlan/EMANE network
wlan_network = self.wireless_network
if is_src_wireless and not is_dst_wireless:
if src not in wlan_network:
wlan_network[src] = set()
wlan_network[src].add(dst)
elif not is_src_wireless and is_dst_wireless:
if dst not in wlan_network:
wlan_network[dst] = set()
wlan_network[dst].add(src)
return is_src_wireless or is_dst_wireless
def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_throughput()
def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items():
img = None
@ -1085,3 +821,36 @@ class CanvasGraph(tk.Canvas):
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(),
)
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)
wallpaper = config.get("wallpaper")
if wallpaper:
wallpaper = Path(wallpaper)
if not wallpaper.is_file():
wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)
logging.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}"
)

View file

@ -0,0 +1,392 @@
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar, messagebox, ttk
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView
from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent
from core.gui.graph import tags
from core.gui.graph.edges import (
CanvasEdge,
CanvasWirelessEdge,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.enums import GraphMode
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.node import CanvasNode
from core.gui.graph.shapeutils import ShapeType
from core.gui.images import ImageEnum
from core.gui.nodeutils import ICON_SIZE, NodeDraw, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.coreclient import CoreClient
class ShowVar(BooleanVar):
def __init__(self, manager: "CanvasManager", tag: str, value: bool) -> None:
super().__init__(value=value)
self.manager: "CanvasManager" = manager
self.tag: str = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None:
for canvas in self.manager.all():
canvas.itemconfigure(self.tag, state=self.state())
class CanvasManager:
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
) -> None:
self.master: tk.BaseWidget = master
self.app: "Application" = app
self.core: "CoreClient" = core
# canvas interactions
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.node_draw: Optional[NodeDraw] = None
self.canvases: Dict[int, CanvasGraph] = {}
# global edge management
self.edges: Dict[str, CanvasEdge] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
# global canvas settings
self.default_dimensions: Tuple[int, int] = (
self.app.guiconfig.preferences.width,
self.app.guiconfig.preferences.height,
)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
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_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# throughput settings
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# widget
self.notebook: Optional[ttk.Notebook] = None
self.canvas_ids: Dict[str, int] = {}
self.unique_ids: Dict[int, str] = {}
self.draw()
self.setup_bindings()
# start with a single tab by default
self.add_canvas()
def setup_bindings(self) -> None:
self.notebook.bind("<<NotebookTabChanged>>", self.tab_change)
def tab_change(self, _event: tk.Event) -> None:
# ignore tab change events before tab data has been setup
unique_id = self.notebook.select()
if not unique_id or unique_id not in self.canvas_ids:
return
canvas = self.current()
self.app.statusbar.set_zoom(canvas.ratio)
def select(self, tab_id: int):
unique_id = self.unique_ids.get(tab_id)
self.notebook.select(unique_id)
def draw(self) -> None:
self.notebook = ttk.Notebook(self.master)
self.notebook.grid(sticky=tk.NSEW, pady=1)
def _next_id(self) -> int:
_id = 1
canvas_ids = set(self.canvas_ids.values())
while _id in canvas_ids:
_id += 1
return _id
def current(self) -> CanvasGraph:
unique_id = self.notebook.select()
canvas_id = self.canvas_ids[unique_id]
return self.get(canvas_id)
def all(self) -> ValuesView[CanvasGraph]:
return self.canvases.values()
def get(self, canvas_id: int) -> CanvasGraph:
canvas = self.canvases.get(canvas_id)
if not canvas:
canvas = self.add_canvas(canvas_id)
return canvas
def add_canvas(self, canvas_id: int = None) -> CanvasGraph:
# create tab frame
tab = ttk.Frame(self.notebook, padding=0)
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
tab.rowconfigure(0, weight=1)
if canvas_id is None:
canvas_id = self._next_id()
self.notebook.add(tab, text=f"Canvas {canvas_id}")
unique_id = self.notebook.tabs()[-1]
logging.info("creating canvas(%s) unique(%s)", canvas_id, unique_id)
self.canvas_ids[unique_id] = canvas_id
self.unique_ids[canvas_id] = unique_id
# create canvas
canvas = CanvasGraph(
tab, self.app, self, self.core, canvas_id, self.default_dimensions
)
canvas.grid(sticky=tk.NSEW)
self.canvases[canvas_id] = canvas
# add scrollbars
scroll_y = ttk.Scrollbar(tab, command=canvas.yview)
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(tab, orient=tk.HORIZONTAL, command=canvas.xview)
scroll_x.grid(row=1, column=0, sticky=tk.EW)
canvas.configure(xscrollcommand=scroll_x.set)
canvas.configure(yscrollcommand=scroll_y.set)
return canvas
def delete_canvas(self) -> None:
if len(self.notebook.tabs()) == 1:
messagebox.showinfo("Canvas", "Cannot delete last canvas", parent=self.app)
return
unique_id = self.notebook.select()
self.notebook.forget(unique_id)
canvas_id = self.canvas_ids.pop(unique_id)
canvas = self.canvases.pop(canvas_id)
edges = set()
for node in canvas.nodes.values():
node.delete()
while node.edges:
edge = node.edges.pop()
if edge in edges:
continue
edges.add(edge)
edge.delete()
def join(self, session: Session) -> None:
# clear out all canvas
for canvas_id in self.notebook.tabs():
self.notebook.forget(canvas_id)
self.canvases.clear()
self.canvas_ids.clear()
self.unique_ids.clear()
self.edges.clear()
self.wireless_edges.clear()
logging.info("cleared canvases")
# reset settings
self.show_node_labels.set(True)
self.show_link_labels.set(True)
self.show_grid.set(True)
self.show_annotations.set(True)
self.show_iface_names.set(False)
self.show_ip4s.set(True)
self.show_ip6s.set(True)
self.show_loss_links.set(True)
self.mode = GraphMode.SELECT
self.annotation_type = None
self.node_draw = None
# draw session
self.draw_session(session)
def draw_session(self, session: Session) -> None:
# create session nodes
for core_node in session.nodes.values():
# add node, avoiding ignored nodes
if NodeUtils.is_ignore_node(core_node.type):
continue
self.add_core_node(core_node)
# organize canvas tabs
canvas_ids = sorted(self.canvases)
for index, canvas_id in enumerate(canvas_ids):
canvas = self.canvases[canvas_id]
logging.info("sorting canvas index(%s) canvas(%s)", index, canvas_id)
self.notebook.insert(index, canvas.master)
# draw existing links
for link in session.links:
node1 = self.core.get_canvas_node(link.node1_id)
node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(node1, node2, link)
else:
self.add_wired_edge(node1, node2, link)
# parse metadata and organize canvases
self.core.parse_metadata()
for canvas in self.canvases.values():
canvas.organize()
# create a default canvas if none were created prior
if not self.canvases:
self.add_canvas()
def redraw_canvases(self, dimensions: Tuple[int, int]) -> None:
for canvas in self.canvases.values():
canvas.redraw_canvas(dimensions)
if canvas.wallpaper:
canvas.redraw_wallpaper()
def get_metadata(self) -> Dict[str, Any]:
canvases = [x.get_metadata() for x in self.all()]
return dict(
gridlines=self.app.manager.show_grid.get(),
dimensions=self.app.manager.current_dimensions,
canvases=canvases,
)
def parse_metadata(self, config: Dict[str, Any]) -> None:
# get configured dimensions and gridlines option
dimensions = self.default_dimensions
dimensions = config.get("dimensions", dimensions)
gridlines = config.get("gridlines", True)
self.show_grid.set(gridlines)
self.redraw_canvases(dimensions)
# get background configurations
for canvas_config in config.get("canvases", []):
canvas_id = canvas_config.get("id")
if canvas_id is None:
logging.error("canvas config id not provided")
continue
canvas = self.get(canvas_id)
canvas.parse_metadata(canvas_config)
def add_core_node(self, core_node: Node) -> None:
# get canvas tab for node
canvas_id = core_node.canvas if core_node.canvas > 0 else 1
logging.info("adding core node canvas(%s): %s", core_node.name, canvas_id)
canvas = self.get(canvas_id)
# if the gui can't find node's image, default to the "edit-node" image
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, canvas, x, y, core_node, image)
canvas.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def set_throughputs(self, throughputs_event: ThroughputsEvent):
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
edge = self.core.iface_to_edge.get(iface_to_edge_id)
if edge:
edge.set_throughput(throughput)
def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_throughput()
def stopped_session(self) -> None:
# clear wireless edges
for edge in self.wireless_edges.values():
edge.delete()
self.wireless_edges.clear()
self.clear_throughputs()
def update_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.link.options = deepcopy(link.options)
edge.draw_link_options()
edge.check_options()
def delete_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.delete()
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)
self.complete_edge(edge, dst, link)
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_wireless_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
logging.warning("ignoring link that already exists: %s", link)
return
edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link)
self.wireless_edges[token] = edge
src.wireless_edges.add(edge)
dst.wireless_edges.add(edge)
src.canvas.tag_raise(src.id)
dst.canvas.tag_raise(dst.id)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
return
edge = self.wireless_edges.pop(token)
edge.delete()
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_wireless_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)
# TODO: look into properly moving this into the edge itself and complete when
# the destination is already provided
def complete_edge(
self, edge: CanvasEdge, dst: CanvasNode, link: Optional[Link] = None
) -> None:
src = edge.src
edge.complete(dst)
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)
if not edge.linked_wireless:
edge.arc_common_edges()
edge.draw_labels()
edge.check_options()
self.edges[edge.token] = edge
self.core.save_edge(edge, src, dst)
edge.src.canvas.organize()
if edge.has_shadows():
edge.dst.canvas.organize()

View file

@ -2,7 +2,7 @@ import functools
import logging
import tkinter as tk
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
import grpc
from PIL.ImageTk import PhotoImage
@ -31,10 +31,16 @@ NODE_TEXT_OFFSET: int = 5
class CanvasNode:
def __init__(
self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage
self,
app: "Application",
canvas: "CanvasGraph",
x: float,
y: float,
core_node: Node,
image: PhotoImage,
):
self.app: "Application" = app
self.canvas: "CanvasGraph" = app.canvas
self.canvas: "CanvasGraph" = canvas
self.image: PhotoImage = image
self.core_node: Node = core_node
self.id: int = self.canvas.create_image(
@ -49,7 +55,7 @@ class CanvasNode:
tags=tags.NODE_LABEL,
font=self.app.icon_text_font,
fill="#0000CD",
state=self.canvas.show_node_labels.state(),
state=self.app.manager.show_node_labels.state(),
)
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set()
@ -57,10 +63,14 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {}
self.hidden: bool = False
self.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context)
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def next_iface_id(self) -> int:
i = 0
while i in self.ifaces:
@ -81,7 +91,7 @@ class CanvasNode:
self.delete_antennas()
def add_antenna(self) -> None:
x, y = self.canvas.coords(self.id)
x, y = self.position()
offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
antenna_id = self.canvas.create_image(
@ -139,15 +149,14 @@ class CanvasNode:
def move(self, x: float, y: float) -> None:
x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id)
current_x, current_y = self.position()
x_offset = x - current_x
y_offset = y - current_y
self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None:
original_position = self.canvas.coords(self.id)
original_position = self.position()
self.canvas.move(self.id, x_offset, y_offset)
pos = self.canvas.coords(self.id)
# check new position
bbox = self.canvas.bbox(self.id)
@ -165,11 +174,12 @@ class CanvasNode:
# move edges
for edge in self.edges:
edge.move_node(self.id, pos)
edge.move_node(self)
for edge in self.wireless_edges:
edge.move_node(self.id, pos)
edge.move_node(self)
# set actual coords for node and update core is running
pos = self.position()
real_x, real_y = self.canvas.get_actual_coords(*pos)
self.core_node.position.x = real_x
self.core_node.position.y = real_y
@ -245,27 +255,44 @@ class CanvasNode:
self.context.add_command(
label="Link To Selected", command=self.wireless_link_selected
)
link_menu = tk.Menu(self.context)
for canvas in self.app.manager.all():
canvas_menu = tk.Menu(link_menu)
themes.style_menu(canvas_menu)
for node in canvas.nodes.values():
if not self.is_linkable(node):
continue
func_link = functools.partial(self.click_link, node)
canvas_menu.add_command(
label=node.core_node.name, command=func_link
)
link_menu.add_cascade(label=f"Canvas {canvas.id}", menu=canvas_menu)
themes.style_menu(link_menu)
self.context.add_cascade(label="Link", menu=link_menu)
unlink_menu = tk.Menu(self.context)
for edge in self.edges:
link = edge.link
if self.id == edge.src:
other_id = edge.dst
if self.id == edge.src.id:
other_node = edge.dst
other_iface = link.iface2.name if link.iface2 else None
else:
other_id = edge.src
other_node = edge.src
other_iface = link.iface1.name if link.iface1 else None
other_node = self.canvas.nodes[other_id]
other_name = other_node.core_node.name
label = f"{other_name}:{other_iface}" if other_iface else other_name
func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu)
self.context.add_cascade(label="Unlink", menu=unlink_menu)
edit_menu = tk.Menu(self.context)
themes.style_menu(edit_menu)
edit_menu.add_command(label="Cut", command=self.click_cut)
edit_menu.add_command(label="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete)
edit_menu.add_command(label="Hide", command=self.hide)
self.context.add_cascade(label="Edit", menu=edit_menu)
self.context.tk_popup(event.x_root, event.y_root)
@ -274,9 +301,13 @@ class CanvasNode:
self.canvas_delete()
def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge)
edge.delete()
self.app.default_info()
def click_link(self, node: "CanvasNode") -> None:
edge = CanvasEdge(self.app, self, node)
self.app.manager.complete_edge(edge, node)
def canvas_delete(self) -> None:
self.canvas.clear_selection()
self.canvas.select_object(self.id)
@ -320,15 +351,14 @@ class CanvasNode:
def has_emane_link(self, iface_id: int) -> Node:
result = None
for edge in self.edges:
if self.id == edge.src:
other_id = edge.dst
if self.id == edge.src.id:
other_node = edge.dst
edge_iface_id = edge.link.iface1.id
else:
other_id = edge.src
other_node = edge.src
edge_iface_id = edge.link.iface2.id
if edge_iface_id != iface_id:
continue
other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE:
result = other_node.core_node
break
@ -360,3 +390,43 @@ class CanvasNode:
self.core_node.icon = icon_path
self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
self.canvas.itemconfig(self.id, image=self.image)
def is_linkable(self, node: "CanvasNode") -> bool:
# cannot link to self
if self == node:
return False
# rj45 nodes can only support one link
if NodeUtils.is_rj45_node(self.core_node.type) and self.edges:
return False
if NodeUtils.is_rj45_node(node.core_node.type) and node.edges:
return False
# only 1 link between bridge based nodes
is_src_bridge = NodeUtils.is_bridge_node(self.core_node)
is_dst_bridge = NodeUtils.is_bridge_node(node.core_node)
common_links = self.edges & node.edges
if all([is_src_bridge, is_dst_bridge, common_links]):
return False
# valid link
return True
def is_wireless(self) -> bool:
return NodeUtils.is_wireless_node(self.core_node.type)
def hide(self) -> None:
self.hidden = True
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.itemconfig(self.text_id, state=tk.HIDDEN)
for edge in self.edges:
if not edge.hidden:
edge.hide()
def show(self) -> None:
self.hidden = False
self.canvas.itemconfig(self.id, state=tk.NORMAL)
self.canvas.itemconfig(self.text_id, state=tk.NORMAL)
for edge in self.edges:
other_node = edge.src
if edge.src == self:
other_node = edge.dst
if edge.hidden and not other_node.hidden:
edge.show()

View file

@ -1,5 +1,5 @@
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
@ -69,6 +69,31 @@ class Shape:
self.shape_data = data
self.draw()
@classmethod
def from_metadata(cls, app: "Application", config: Dict[str, Any]) -> None:
shape_type = config["type"]
try:
shape_type = ShapeType(shape_type)
coords = config["iconcoords"]
data = AnnotationData(
config["label"],
config["fontfamily"],
config["fontsize"],
config["labelcolor"],
config["color"],
config["border"],
config["width"],
config["bold"],
config["italic"],
config["underline"],
)
canvas_id = config.get("canvas", 1)
canvas = app.manager.get(canvas_id)
shape = Shape(app, canvas, shape_type, *coords, data=data)
canvas.shapes[shape.id] = shape
except ValueError:
logging.exception("unknown shape: %s", shape_type)
def draw(self) -> None:
if self.created:
dash = None
@ -85,7 +110,7 @@ class Shape:
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE:
@ -99,7 +124,7 @@ class Shape:
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT:
@ -111,7 +136,7 @@ class Shape:
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
else:
logging.error("unknown shape type: %s", self.shape_type)
@ -139,7 +164,7 @@ class Shape:
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
state=self.canvas.show_annotations.state(),
state=self.app.manager.show_annotations.state(),
)
def shape_motion(self, x1: float, y1: float) -> None:
@ -184,6 +209,7 @@ class Shape:
x1, y1 = self.canvas.get_actual_coords(x1, y1)
coords = (x1, y1)
return {
"canvas": self.canvas.id,
"type": self.shape_type.value,
"iconcoords": coords,
"label": self.shape_data.text,

View file

@ -14,6 +14,7 @@ NODE: str = "node"
WALLPAPER: str = "wallpaper"
SELECTION: str = "selectednodes"
MARKER: str = "marker"
HIDDEN: str = "hidden"
ORGANIZE_TAGS: List[str] = [
WALLPAPER,
GRIDLINE,

View file

@ -90,6 +90,7 @@ class ImageEnum(Enum):
SHUTDOWN = "shutdown"
CANCEL = "cancel"
ERROR = "error"
SHADOW = "shadow"
class TypeToImage:

View file

@ -188,19 +188,16 @@ class InterfaceManager:
self, canvas_node: CanvasNode, visited: Set[int] = None
) -> Optional[IPNetwork]:
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas
subnets = None
if not visited:
visited = set()
visited.add(canvas_node.core_node.id)
for edge in canvas_node.edges:
src_node = canvas.nodes[edge.src]
dst_node = canvas.nodes[edge.dst]
iface = edge.link.iface1
check_node = src_node
if src_node == canvas_node:
check_node = edge.src
if edge.src == canvas_node:
iface = edge.link.iface2
check_node = dst_node
check_node = edge.dst
if check_node.core_node.id in visited:
continue
visited.add(check_node.core_node.id)

View file

@ -22,7 +22,7 @@ from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.manager import CanvasManager
from core.gui.nodeutils import ICON_SIZE
from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask
@ -45,9 +45,10 @@ class Menubar(tk.Menu):
super().__init__(app)
self.app: "Application" = app
self.core: CoreClient = app.core
self.canvas: CanvasGraph = app.canvas
self.manager: CanvasManager = app.manager
self.recent_menu: Optional[tk.Menu] = None
self.edit_menu: Optional[tk.Menu] = None
self.canvas_menu: Optional[tk.Menu] = None
self.observers_menu: Optional[ObserversMenu] = None
self.draw()
@ -106,6 +107,7 @@ class Menubar(tk.Menu):
menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes)
menu.add_command(label="Show Hidden Nodes", command=self.click_show_hidden)
menu.add_separator()
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
@ -116,11 +118,13 @@ class Menubar(tk.Menu):
menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.click_delete
)
menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide)
self.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut)
self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.click_delete)
self.app.master.bind_all("<Control-h>", self.click_hide)
self.edit_menu = menu
def draw_canvas_menu(self) -> None:
@ -128,9 +132,13 @@ class Menubar(tk.Menu):
Create canvas menu
"""
menu = tk.Menu(self)
menu.add_command(label="New", command=self.click_canvas_add)
menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
menu.add_separator()
menu.add_command(label="Delete", command=self.click_canvas_delete)
menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu)
self.canvas_menu = menu
def draw_view_menu(self) -> None:
"""
@ -145,52 +153,52 @@ class Menubar(tk.Menu):
menu.add_checkbutton(
label="Interface Names",
command=self.click_edge_label_change,
variable=self.canvas.show_iface_names,
variable=self.manager.show_iface_names,
)
menu.add_checkbutton(
label="IPv4 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip4s,
variable=self.manager.show_ip4s,
)
menu.add_checkbutton(
label="IPv6 Addresses",
command=self.click_edge_label_change,
variable=self.canvas.show_ip6s,
variable=self.manager.show_ip6s,
)
menu.add_checkbutton(
label="Node Labels",
command=self.canvas.show_node_labels.click_handler,
variable=self.canvas.show_node_labels,
command=self.manager.show_node_labels.click_handler,
variable=self.manager.show_node_labels,
)
menu.add_checkbutton(
label="Link Labels",
command=self.canvas.show_link_labels.click_handler,
variable=self.canvas.show_link_labels,
command=self.manager.show_link_labels.click_handler,
variable=self.manager.show_link_labels,
)
menu.add_checkbutton(
label="Links",
command=self.canvas.show_links.click_handler,
variable=self.canvas.show_links,
command=self.manager.show_links.click_handler,
variable=self.manager.show_links,
)
menu.add_checkbutton(
label="Loss Links",
command=self.canvas.show_loss_links.click_handler,
variable=self.canvas.show_loss_links,
command=self.manager.show_loss_links.click_handler,
variable=self.manager.show_loss_links,
)
menu.add_checkbutton(
label="Wireless Links",
command=self.canvas.show_wireless.click_handler,
variable=self.canvas.show_wireless,
command=self.manager.show_wireless.click_handler,
variable=self.manager.show_wireless,
)
menu.add_checkbutton(
label="Annotations",
command=self.canvas.show_annotations.click_handler,
variable=self.canvas.show_annotations,
command=self.manager.show_annotations.click_handler,
variable=self.manager.show_annotations,
)
menu.add_checkbutton(
label="Canvas Grid",
command=self.canvas.show_grid.click_handler,
variable=self.canvas.show_grid,
command=self.manager.show_grid.click_handler,
variable=self.manager.show_grid,
)
self.add_cascade(label="View", menu=menu)
@ -334,17 +342,12 @@ class Menubar(tk.Menu):
self.app.save_config()
self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None:
labels = {"Copy", "Paste", "Delete", "Cut"}
for i in range(self.edit_menu.index(tk.END) + 1):
try:
label = self.edit_menu.entrycget(i, "label")
if label not in labels:
continue
state = tk.DISABLED if is_runtime else tk.NORMAL
self.edit_menu.entryconfig(i, state=state)
except tk.TclError:
pass
def set_state(self, is_runtime: bool) -> None:
state = tk.DISABLED if is_runtime else tk.NORMAL
for entry in {"Copy", "Paste", "Delete", "Cut"}:
self.edit_menu.entryconfigure(entry, state=state)
for entry in {"Delete"}:
self.canvas_menu.entryconfigure(entry, state=state)
def prompt_save_running_session(self, quit_app: bool = False) -> None:
"""
@ -372,6 +375,12 @@ class Menubar(tk.Menu):
dialog = PreferencesDialog(self.app)
dialog.show()
def click_canvas_add(self) -> None:
self.manager.add_canvas()
def click_canvas_delete(self) -> None:
self.manager.delete_canvas()
def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app)
dialog.show()
@ -401,17 +410,29 @@ class Menubar(tk.Menu):
dialog.show()
def click_copy(self, _event: tk.Event = None) -> None:
self.canvas.copy()
canvas = self.manager.current()
canvas.copy()
def click_paste(self, _event: tk.Event = None) -> None:
self.canvas.paste()
canvas = self.manager.current()
canvas.paste()
def click_delete(self, _event: tk.Event = None) -> None:
self.canvas.delete_selected_objects()
canvas = self.manager.current()
canvas.delete_selected_objects()
def click_hide(self, _event: tk.Event = None) -> None:
canvas = self.manager.current()
canvas.hide_selected_objects()
def click_cut(self, _event: tk.Event = None) -> None:
self.canvas.copy()
self.canvas.delete_selected_objects()
canvas = self.manager.current()
canvas.copy()
canvas.delete_selected_objects()
def click_show_hidden(self, _event: tk.Event = None) -> None:
for canvas in self.manager.all():
canvas.show_hidden()
def click_session_options(self) -> None:
logging.debug("Click options")
@ -439,14 +460,15 @@ class Menubar(tk.Menu):
dialog.show()
def click_autogrid(self) -> None:
width, height = self.canvas.current_dimensions
width, height = self.manager.current_dimensions
padding = (ICON_SIZE / 2) + 10
layout_size = padding + ICON_SIZE
col_count = width // layout_size
logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
)
for i, node in enumerate(self.canvas.nodes.values()):
canvas = self.manager.current()
for i, node in enumerate(canvas.nodes.values()):
col = i % col_count
row = i // col_count
x = (col * layout_size) + padding
@ -460,7 +482,7 @@ class Menubar(tk.Menu):
self.app.hide_info()
def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values():
for edge in self.manager.edges.values():
edge.draw_labels()
def click_mac_config(self) -> None:

View file

@ -48,7 +48,6 @@ class StatusBar(ttk.Frame):
self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
self.zoom.grid(row=0, column=1, sticky=tk.EW)
self.set_zoom(self.app.canvas.ratio)
self.cpu_label = ttk.Label(
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE

View file

@ -144,7 +144,8 @@ class MarkerFrame(ttk.Frame):
Tooltip(self.color_frame, "Marker Color")
def click_clear(self) -> None:
self.app.canvas.delete(tags.MARKER)
canvas = self.app.manager.current()
canvas.delete(tags.MARKER)
def click_color(self, _event: tk.Event) -> None:
dialog = ColorPickerDialog(self.app, self.app, self.color)
@ -257,8 +258,8 @@ class Toolbar(ttk.Frame):
def draw_node_picker(self) -> None:
self.hide_marker()
self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_node
self.app.manager.mode = GraphMode.NODE
self.app.manager.node_draw = self.current_node
self.design_frame.select_radio(self.node_button)
self.picker = PickerFrame(self.app, self.node_button)
# draw default nodes
@ -278,12 +279,12 @@ class Toolbar(ttk.Frame):
def click_selection(self) -> None:
self.design_frame.select_radio(self.select_button)
self.app.canvas.mode = GraphMode.SELECT
self.app.manager.mode = GraphMode.SELECT
self.hide_marker()
def click_runtime_selection(self) -> None:
self.runtime_frame.select_radio(self.runtime_select_button)
self.app.canvas.mode = GraphMode.SELECT
self.app.manager.mode = GraphMode.SELECT
self.hide_marker()
def click_start(self) -> None:
@ -291,8 +292,8 @@ class Toolbar(ttk.Frame):
Start session handler redraw buttons, send node and link messages to grpc
server.
"""
self.app.menubar.change_menubar_item_state(is_runtime=True)
self.app.canvas.mode = GraphMode.SELECT
self.app.menubar.set_state(is_runtime=True)
self.app.manager.mode = GraphMode.SELECT
enable_buttons(self.design_frame, enabled=False)
task = ProgressTask(
self.app, "Start", self.app.core.start_session, self.start_callback
@ -324,7 +325,7 @@ class Toolbar(ttk.Frame):
def click_link(self) -> None:
self.design_frame.select_radio(self.link_button)
self.app.canvas.mode = GraphMode.EDGE
self.app.manager.mode = GraphMode.EDGE
self.hide_marker()
def update_button(
@ -337,7 +338,7 @@ class Toolbar(ttk.Frame):
logging.debug("update button(%s): %s", button, node_draw)
button.configure(image=image)
button.image = image
self.app.canvas.node_draw = node_draw
self.app.manager.node_draw = node_draw
if type_enum == NodeTypeEnum.NODE:
self.current_node = node_draw
elif type_enum == NodeTypeEnum.NETWORK:
@ -348,8 +349,8 @@ class Toolbar(ttk.Frame):
Draw the options for link-layer button.
"""
self.hide_marker()
self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_network
self.app.manager.mode = GraphMode.NODE
self.app.manager.node_draw = self.current_network
self.design_frame.select_radio(self.network_button)
self.picker = PickerFrame(self.app, self.network_button)
for node_draw in NodeUtils.NETWORK_NODES:
@ -364,8 +365,8 @@ class Toolbar(ttk.Frame):
Draw the options for marker button.
"""
self.design_frame.select_radio(self.annotation_button)
self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = self.current_annotation
self.app.manager.mode = GraphMode.ANNOTATION
self.app.manager.annotation_type = self.current_annotation
if is_marker(self.current_annotation):
self.show_marker()
self.picker = PickerFrame(self.app, self.annotation_button)
@ -396,7 +397,7 @@ class Toolbar(ttk.Frame):
redraw buttons on the toolbar, send node and link messages to grpc server
"""
logging.info("clicked stop button")
self.app.menubar.change_menubar_item_state(is_runtime=False)
self.app.menubar.set_state(is_runtime=False)
self.app.core.close_mobility_players()
enable_buttons(self.runtime_frame, enabled=False)
task = ProgressTask(
@ -406,7 +407,7 @@ class Toolbar(ttk.Frame):
def stop_callback(self, result: bool) -> None:
self.set_design()
self.app.canvas.stopped_session()
self.app.manager.stopped_session()
def update_annotation(
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
@ -414,7 +415,7 @@ class Toolbar(ttk.Frame):
logging.debug("clicked annotation")
self.annotation_button.configure(image=image)
self.annotation_button.image = image
self.app.canvas.annotation_type = shape_type
self.app.manager.annotation_type = shape_type
self.current_annotation = shape_type
self.annotation_enum = image_enum
if is_marker(shape_type):
@ -435,8 +436,8 @@ class Toolbar(ttk.Frame):
def click_marker_button(self) -> None:
self.runtime_frame.select_radio(self.runtime_marker_button)
self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = ShapeType.MARKER
self.app.manager.mode = GraphMode.ANNOTATION
self.app.manager.annotation_type = ShapeType.MARKER
self.show_marker()
def scale_button(

View file

@ -752,6 +752,7 @@ message Node {
Geo geo = 12;
string dir = 13;
string channel = 14;
int32 canvas = 15;
}
message Link {