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, image=node_proto.image,
services=node_proto.services, services=node_proto.services,
config_services=node_proto.config_services, config_services=node_proto.config_services,
canvas=node_proto.canvas,
) )
if node_proto.emane: if node_proto.emane:
options.emane = 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, config_services=config_services,
dir=node_dir, dir=node_dir,
channel=channel, channel=channel,
canvas=node.canvas,
) )

View file

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

View file

@ -591,7 +591,6 @@ class Session:
:raises core.CoreError: when node to update does not exist :raises core.CoreError: when node to update does not exist
""" """
node = self.get_node(node_id, NodeBase) node = self.get_node(node_id, NodeBase)
node.canvas = options.canvas
node.icon = options.icon node.icon = options.icon
self.set_node_position(node, options) self.set_node_position(node, options)
self.sdt.edit_node(node, options.lon, options.lat, options.alt) 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.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame 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.images import ImageEnum, Images
from core.gui.menubar import Menubar from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
@ -35,7 +35,7 @@ class Application(ttk.Frame):
self.menubar: Optional[Menubar] = None self.menubar: Optional[Menubar] = None
self.toolbar: Optional[Toolbar] = None self.toolbar: Optional[Toolbar] = None
self.right_frame: Optional[ttk.Frame] = None self.right_frame: Optional[ttk.Frame] = None
self.canvas: Optional[CanvasGraph] = None self.manager: Optional[CanvasManager] = None
self.statusbar: Optional[StatusBar] = None self.statusbar: Optional[StatusBar] = None
self.progress: Optional[Progressbar] = None self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None self.infobar: Optional[ttk.Frame] = None
@ -136,20 +136,8 @@ class Application(ttk.Frame):
label.grid(sticky=tk.EW, pady=PADY) label.grid(sticky=tk.EW, pady=PADY)
def draw_canvas(self) -> None: def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame) self.manager = CanvasManager(self.right_frame, self, self.core)
canvas_frame.rowconfigure(0, weight=1) self.manager.notebook.grid(sticky=tk.NSEW)
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)
def draw_status(self) -> None: def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
@ -201,8 +189,10 @@ class Application(ttk.Frame):
def joined_session_update(self) -> None: def joined_session_update(self) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
self.menubar.set_state(is_runtime=True)
self.toolbar.set_runtime() self.toolbar.set_runtime()
else: else:
self.menubar.set_state(is_runtime=False)
self.toolbar.set_design() self.toolbar.set_design()
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: 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: def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"): for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name) 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]: def find_terminal() -> Optional[str]:
@ -197,16 +198,14 @@ def find_terminal() -> Optional[str]:
def check_directory() -> None: def check_directory() -> None:
if HOME_PATH.exists(): HOME_PATH.mkdir(exist_ok=True)
return BACKGROUNDS_PATH.mkdir(exist_ok=True)
HOME_PATH.mkdir() CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
BACKGROUNDS_PATH.mkdir() CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
CUSTOM_EMANE_PATH.mkdir() ICONS_PATH.mkdir(exist_ok=True)
CUSTOM_SERVICE_PATH.mkdir() MOBILITY_PATH.mkdir(exist_ok=True)
ICONS_PATH.mkdir() XMLS_PATH.mkdir(exist_ok=True)
MOBILITY_PATH.mkdir() SCRIPT_PATH.mkdir(exist_ok=True)
XMLS_PATH.mkdir()
SCRIPT_PATH.mkdir()
copy_files(LOCAL_ICONS_PATH, ICONS_PATH) copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)

View file

@ -6,7 +6,6 @@ import json
import logging import logging
import os import os
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import messagebox from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
@ -40,16 +39,14 @@ from core.api.grpc.wrappers import (
SessionState, SessionState,
ThroughputsEvent, ThroughputsEvent,
) )
from core.gui import appconfig from core.gui.appconfig import XMLS_PATH, CoreServer, Observer
from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer
from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.graph.edges import CanvasEdge from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import AnnotationData, Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType
from core.gui.interface import InterfaceManager from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.nodeutils import NodeDraw, NodeUtils
@ -207,27 +204,26 @@ class CoreClient:
canvas_node2 = self.canvas_nodes[node2_id] canvas_node2 = self.canvas_nodes[node2_id]
if event.link.type == LinkType.WIRELESS: if event.link.type == LinkType.WIRELESS:
if event.message_type == MessageType.ADD: if event.message_type == MessageType.ADD:
self.app.canvas.add_wireless_edge( self.app.manager.add_wireless_edge(
canvas_node1, canvas_node2, event.link canvas_node1, canvas_node2, event.link
) )
elif event.message_type == MessageType.DELETE: elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wireless_edge( self.app.manager.delete_wireless_edge(
canvas_node1, canvas_node2, event.link canvas_node1, canvas_node2, event.link
) )
elif event.message_type == MessageType.NONE: elif event.message_type == MessageType.NONE:
self.app.canvas.update_wireless_edge( self.app.manager.update_wireless_edge(
canvas_node1, canvas_node2, event.link canvas_node1, canvas_node2, event.link
) )
else: else:
logging.warning("unknown link event: %s", event) logging.warning("unknown link event: %s", event)
else: else:
if event.message_type == MessageType.ADD: if event.message_type == MessageType.ADD:
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link) self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
self.app.canvas.organize()
elif event.message_type == MessageType.DELETE: 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: elif event.message_type == MessageType.NONE:
self.app.canvas.update_wired_edge(event.link) self.app.manager.update_wired_edge(event.link)
else: else:
logging.warning("unknown link event: %s", event) logging.warning("unknown link event: %s", event)
@ -243,13 +239,13 @@ class CoreClient:
canvas_node.update_icon(node.icon) canvas_node.update_icon(node.icon)
elif event.message_type == MessageType.DELETE: elif event.message_type == MessageType.DELETE:
canvas_node = self.canvas_nodes[node.id] canvas_node = self.canvas_nodes[node.id]
self.app.canvas.clear_selection() canvas_node.canvas.clear_selection()
self.app.canvas.select_object(canvas_node.id) canvas_node.canvas.select_object(canvas_node.id)
self.app.canvas.delete_selected_objects() canvas_node.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD: elif event.message_type == MessageType.ADD:
if node.id in self.session.nodes: if node.id in self.session.nodes:
logging.error("core node already exists: %s", node) logging.error("core node already exists: %s", node)
self.app.canvas.add_core_node(node) self.app.manager.add_core_node(node)
else: else:
logging.warning("unknown node event: %s", event) logging.warning("unknown node event: %s", event)
@ -262,7 +258,7 @@ class CoreClient:
if self.handling_throughputs: if self.handling_throughputs:
self.handling_throughputs.cancel() self.handling_throughputs.cancel()
self.handling_throughputs = None self.handling_throughputs = None
self.app.canvas.clear_throughputs() self.app.manager.clear_throughputs()
def cancel_events(self) -> None: def cancel_events(self) -> None:
if self.handling_events: if self.handling_events:
@ -293,7 +289,7 @@ class CoreClient:
) )
return return
logging.debug("handling throughputs event: %s", event) 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: def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
self.app.after(0, self.app.statusbar.set_cpu, event.usage) self.app.after(0, self.app.statusbar.set_cpu, event.usage)
@ -315,9 +311,7 @@ class CoreClient:
self.session.id, self.handle_events self.session.id, self.handle_events
) )
self.ifaces_manager.joined(self.session.links) self.ifaces_manager.joined(self.session.links)
self.app.canvas.reset_and_redraw(self.session) self.app.manager.join(self.session)
self.parse_metadata()
self.app.canvas.organize()
if self.is_runtime(): if self.is_runtime():
self.show_mobility_players() self.show_mobility_players()
self.app.after(0, self.app.joined_session_update) self.app.after(0, self.app.joined_session_update)
@ -334,23 +328,7 @@ class CoreClient:
logging.debug("canvas metadata: %s", canvas_config) logging.debug("canvas metadata: %s", canvas_config)
if canvas_config: if canvas_config:
canvas_config = json.loads(canvas_config) canvas_config = json.loads(canvas_config)
gridlines = canvas_config.get("gridlines", True) self.app.manager.parse_metadata(canvas_config)
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)
# load saved shapes # load saved shapes
shapes_config = config.get("shapes") shapes_config = config.get("shapes")
@ -358,28 +336,7 @@ class CoreClient:
shapes_config = json.loads(shapes_config) shapes_config = json.loads(shapes_config)
for shape_config in shapes_config: for shape_config in shapes_config:
logging.debug("loading shape: %s", shape_config) logging.debug("loading shape: %s", shape_config)
shape_type = shape_config["type"] Shape.from_metadata(self.app, shape_config)
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)
# load edges config # load edges config
edges_config = config.get("edges") edges_config = config.get("edges")
@ -392,6 +349,17 @@ class CoreClient:
edge.color = edge_config["color"] edge.color = edge_config["color"]
edge.redraw() 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: def create_new_session(self) -> None:
""" """
Create a new session Create a new session
@ -557,26 +525,14 @@ class CoreClient:
def set_metadata(self) -> None: def set_metadata(self) -> None:
# create canvas data # create canvas data
wallpaper_path = None canvas_config = self.app.manager.get_metadata()
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 = json.dumps(canvas_config) canvas_config = json.dumps(canvas_config)
# create shapes data # create shapes data
shapes = [] shapes = []
for shape in self.app.canvas.shapes.values(): for canvas in self.app.manager.all():
shapes.append(shape.metadata()) for shape in canvas.shapes.values():
shapes.append(shape.metadata())
shapes = json.dumps(shapes) shapes = json.dumps(shapes)
# create edges config # create edges config
@ -588,8 +544,14 @@ class CoreClient:
edges_config.append(edge_config) edges_config.append(edge_config)
edges_config = json.dumps(edges_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 # 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) response = self.client.set_session_metadata(self.session.id, metadata)
logging.debug("set session metadata %s, result: %s", metadata, response) 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 import validation
from core.gui.dialogs.dialog import Dialog 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 from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -22,9 +22,9 @@ class SizeAndScaleDialog(Dialog):
create an instance for size and scale object create an instance for size and scale object
""" """
super().__init__(app, "Canvas Size and Scale") 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) 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_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height) self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.session.location location = self.app.core.session.location
@ -189,9 +189,7 @@ class SizeAndScaleDialog(Dialog):
def click_apply(self) -> None: def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get() width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height)) self.manager.redraw_canvases((width, height))
if self.canvas.wallpaper:
self.canvas.redraw_wallpaper()
location = self.app.core.session.location location = self.app.core.session.location
location.x = self.x.get() location.x = self.x.get()
location.y = self.y.get() location.y = self.y.get()

View file

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

View file

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

View file

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

View file

@ -134,7 +134,8 @@ class PreferencesDialog(Dialog):
# scale toolbar and canvas items # scale toolbar and canvas items
self.app.toolbar.scale() 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: def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get() scale_value = self.gui_scale.get()

View file

@ -199,7 +199,10 @@ class SessionsDialog(Dialog):
logging.debug("delete session: %s", self.selected_session) logging.debug("delete session: %s", self.selected_session)
self.tree.delete(self.selected_id) self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session) 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.click_new()
self.destroy() self.destroy()
self.click_select() self.click_select()

View file

@ -27,7 +27,7 @@ class ShapeDialog(Dialog):
else: else:
title = "Add Text" title = "Add Text"
super().__init__(app, title) super().__init__(app, title)
self.canvas: "CanvasGraph" = app.canvas self.canvas: "CanvasGraph" = app.manager.current()
self.fill: Optional[ttk.Label] = None self.fill: Optional[ttk.Label] = None
self.border: Optional[ttk.Label] = None self.border: Optional[ttk.Label] = None
self.shape: "Shape" = shape 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.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog 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 from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -17,16 +17,16 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog): class ThroughputDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config") 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.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1) self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1) self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1) self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar( 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.width: tk.IntVar = tk.IntVar(value=self.manager.throughput_width)
self.color: str = self.canvas.throughput_color self.color: str = self.manager.throughput_color
self.color_button: Optional[tk.Button] = None self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw() self.draw()
@ -106,7 +106,7 @@ class ThroughputDialog(Dialog):
self.color_button.config(bg=self.color, text=self.color, bd=0) self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self) -> None: def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get() self.manager.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get() self.manager.throughput_width = self.width.get()
self.canvas.throughput_color = self.color self.manager.throughput_color = self.color
self.destroy() self.destroy()

View file

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

View file

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

View file

@ -1,18 +1,25 @@
import functools
import logging import logging
import math import math
import tkinter as tk 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.api.grpc.wrappers import Interface, Link
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags 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 from core.gui.utils import bandwidth_text, delay_jitter_text
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph 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 TEXT_DISTANCE: int = 60
EDGE_WIDTH: int = 3 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}" 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: def arc_edges(edges) -> None:
if not edges: if not edges:
return return
@ -62,24 +82,114 @@ def arc_edges(edges) -> None:
edge.redraw() 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: class Edge:
tag: str = tags.EDGE tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: def __init__(
self.canvas = canvas self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
) -> None:
self.app: "Application" = app
self.manager: CanvasManager = app.manager
self.id: Optional[int] = None self.id: Optional[int] = None
self.src: int = src self.id2: Optional[int] = None
self.dst: int = dst 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.arc: int = 0
self.token: Optional[str] = None self.token: Optional[str] = None
self.src_label: Optional[int] = None self.src_label: Optional[int] = None
self.src_label2: Optional[int] = None
self.middle_label: Optional[int] = None self.middle_label: Optional[int] = None
self.middle_label2: Optional[int] = None
self.dst_label: Optional[int] = None self.dst_label: Optional[int] = None
self.dst_label2: Optional[int] = None
self.color: str = EDGE_COLOR self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH 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: def scaled_width(self) -> float:
return self.width * self.canvas.app.app_scale return self.width * self.app.app_scale
def _get_arcpoint( def _get_arcpoint(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
@ -110,11 +220,57 @@ class Edge:
arc_y = (perp_m * arc_x) + b arc_y = (perp_m * arc_x) + b
return arc_x, arc_y return arc_x, arc_y
def draw( def arc_common_edges(self) -> None:
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str common_edges = list(self.src.edges & self.dst.edges)
) -> None: 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) arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line( return canvas.create_line(
*src_pos, *src_pos,
*arc_pos, *arc_pos,
*dst_pos, *dst_pos,
@ -126,112 +282,243 @@ class Edge:
) )
def redraw(self) -> None: def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) self.move_src()
src_pos = src_x, src_y if self.id2:
self.move_src(src_pos) self.dst.canvas.itemconfig(
self.id2, width=self.scaled_width(), fill=self.color
def middle_label_pos(self) -> Tuple[float, float]: )
_, _, x, y, _, _ = self.canvas.coords(self.id) self.move_dst()
return x, y
def middle_label_text(self, text: str) -> None: def middle_label_text(self, text: str) -> None:
if self.middle_label is None: if self.middle_label is None:
x, y = self.middle_label_pos() _, _, x, y, _, _ = self.src.canvas.coords(self.id)
self.middle_label = self.canvas.create_text( self.middle_label = self.src.canvas.create_text(
x, x,
y, y,
font=self.canvas.app.edge_font, font=self.app.edge_font,
text=text, text=text,
tags=tags.LINK_LABEL, tags=tags.LINK_LABEL,
justify=tk.CENTER, 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: 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: def clear_middle_label(self) -> None:
self.canvas.delete(self.middle_label) self.src.canvas.delete(self.middle_label)
self.middle_label = None self.middle_label = None
if self.middle_label2:
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: self.dst.canvas.delete(self.middle_label2)
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id) self.middle_label2 = None
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 src_label_text(self, text: str) -> None: def src_label_text(self, text: str) -> None:
if self.src_label is None: if self.src_label is None and self.src_label2 is None:
src_pos, _ = self.node_label_positions() if self.id:
self.src_label = self.canvas.create_text( logging.info("src label id")
*src_pos, src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
text=text, src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
justify=tk.CENTER, self.src_label = self.src.canvas.create_text(
font=self.canvas.app.edge_font, *src_pos,
tags=tags.LINK_LABEL, text=text,
state=self.canvas.show_link_labels.state(), 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: 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: def dst_label_text(self, text: str) -> None:
if self.dst_label is None: if self.dst_label is None and self.dst_label2 is None:
_, dst_pos = self.node_label_positions() if self.id:
self.dst_label = self.canvas.create_text( src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
*dst_pos, _, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
text=text, self.dst_label = self.src.canvas.create_text(
justify=tk.CENTER, *dst_pos,
font=self.canvas.app.edge_font, text=text,
tags=tags.LINK_LABEL, justify=tk.CENTER,
state=self.canvas.show_link_labels.state(), 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: 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: def drawing(self, pos: Tuple[float, float]) -> None:
if self.src == node_id: src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.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)
src_pos = src_x, src_y 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: def move_node(self, node: "CanvasNode") -> None:
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) 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 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: def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos) 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: if self.middle_label:
self.canvas.coords(self.middle_label, *arc_pos) self.src.canvas.coords(self.middle_label, *arc_pos)
src_pos, dst_pos = self.node_label_positions() 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: 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: 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: def delete(self) -> None:
logging.debug("deleting canvas edge, id: %s", self.id) logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id) self.src.canvas.delete(self.id)
self.canvas.delete(self.src_label) self.src.canvas.delete(self.src_label)
self.canvas.delete(self.dst_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.clear_middle_label()
self.id = None self.id = None
self.id2 = None
self.src_label = None self.src_label = None
self.src_label2 = None
self.dst_label = 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): class CanvasWirelessEdge(Edge):
@ -239,35 +526,38 @@ class CanvasWirelessEdge(Edge):
def __init__( def __init__(
self, self,
canvas: "CanvasGraph", app: "Application",
src: int, src: "CanvasNode",
dst: int, dst: "CanvasNode",
network_id: int, network_id: int,
token: str, token: str,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
link: Link, link: Link,
) -> None: ) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst) 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.network_id: int = network_id
self.link: Link = link self.link: Link = link
self.token: str = token self.token: str = token
self.width: float = WIRELESS_WIDTH self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR color = link.color if link.color else WIRELESS_COLOR
self.color: str = 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: if link.label:
self.middle_label_text(link.label) self.middle_label_text(link.label)
self.set_binding() self.set_binding()
def set_binding(self) -> None: 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: def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info( self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
) def delete(self) -> None:
self.src.wireless_edges.discard(self)
self.dst.wireless_edges.remove(self)
super().delete()
class CanvasEdge(Edge): class CanvasEdge(Edge):
@ -276,47 +566,40 @@ class CanvasEdge(Edge):
""" """
def __init__( def __init__(
self, self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
canvas: "CanvasGraph",
src: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
) -> None: ) -> None:
""" """
Create an instance of canvas edge object Create an instance of canvas edge object
""" """
super().__init__(canvas, src) super().__init__(app, src, dst)
self.text_src: Optional[int] = None self.text_src: Optional[int] = None
self.text_dst: Optional[int] = None self.text_dst: Optional[int] = None
self.link: Optional[Link] = None self.link: Optional[Link] = None
self.linked_wireless: bool = False
self.asymmetric_link: Optional[Link] = None self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos, tk.NORMAL) self.draw(tk.NORMAL)
self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
def is_customized(self) -> bool: def is_customized(self) -> bool:
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
def create_context(self) -> None: def set_bindings(self) -> None:
themes.style_menu(self.context) if self.id:
self.context.add_command(label="Configure", command=self.click_configure) show_context = functools.partial(self.show_context, self.src.canvas)
self.context.add_command(label="Delete", command=self.click_delete) self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def set_binding(self) -> None: if self.id2:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context) show_context = functools.partial(self.show_context, self.dst.canvas)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info) 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: def iface_label(self, iface: Interface) -> str:
label = "" 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}" 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"{label}\n" if label else ""
label += f"{iface.ip4}/{iface.ip4_mask}" 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"{label}\n" if label else ""
label += f"{iface.ip6}/{iface.ip6_mask}" label += f"{iface.ip6}/{iface.ip6_mask}"
return label return label
@ -346,77 +629,98 @@ class CanvasEdge(Edge):
return return
if self.link.options.loss == EDGE_LOSS: if self.link.options.loss == EDGE_LOSS:
state = tk.HIDDEN 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: else:
state = tk.NORMAL state = tk.NORMAL
self.canvas.dtag(self.id, tags.LOSS_EDGES) if self.id:
if self.canvas.show_loss_links.state() == tk.HIDDEN: self.src.canvas.dtag(self.id, tags.LOSS_EDGES)
self.canvas.itemconfigure(self.id, state=state) 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: def set_throughput(self, throughput: float) -> None:
throughput = 0.001 * throughput throughput = 0.001 * throughput
text = f"{throughput:.3f} kbps" text = f"{throughput:.3f} kbps"
self.middle_label_text(text) self.middle_label_text(text)
if throughput > self.canvas.throughput_threshold: if throughput > self.manager.throughput_threshold:
color = self.canvas.throughput_color color = self.manager.throughput_color
width = self.canvas.throughput_width width = self.manager.throughput_width
else: else:
color = self.color color = self.color
width = self.scaled_width() 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: def clear_throughput(self) -> None:
self.clear_middle_label() self.clear_middle_label()
if not self.linked_wireless: if not self.linked_wireless:
self.draw_link_options() self.draw_link_options()
def complete(self, dst: int, linked_wireless: bool) -> None: def complete(self, dst: "CanvasNode") -> None:
self.dst = dst self.dst = dst
self.linked_wireless = linked_wireless self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
dst_pos = self.canvas.coords(self.dst) self.set_bindings()
self.move_dst(dst_pos)
self.check_wireless() self.check_wireless()
logging.debug("draw wired link from node %s to node %s", self.src, dst) logging.debug("draw wired link from node %s to node %s", self.src, dst)
def check_wireless(self) -> None: def check_wireless(self) -> None:
if self.linked_wireless: if not self.linked_wireless:
self.canvas.itemconfig(self.id, state=tk.HIDDEN) return
self.canvas.dtag(self.id, tags.EDGE) if self.id:
self._check_antenna() self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.src.canvas.dtag(self.id, tags.EDGE)
def _check_antenna(self) -> None: if self.id2:
src_node = self.canvas.nodes[self.src] self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
dst_node = self.canvas.nodes[self.dst] self.dst.canvas.dtag(self.id2, tags.EDGE)
src_node_type = src_node.core_node.type # add antenna to node
dst_node_type = dst_node.core_node.type if self.src.is_wireless() and not self.dst.is_wireless():
is_src_wireless = NodeUtils.is_wireless_node(src_node_type) self.dst.add_antenna()
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) elif not self.src.is_wireless() and self.dst.is_wireless():
if is_src_wireless or is_dst_wireless: self.src.add_antenna()
if is_src_wireless and not is_dst_wireless: else:
dst_node.add_antenna() self.src.add_antenna()
elif not is_src_wireless and is_dst_wireless:
src_node.add_antenna()
else:
src_node.add_antenna()
def reset(self) -> None: def reset(self) -> None:
self.canvas.delete(self.middle_label) if self.middle_label:
self.middle_label = None self.src.canvas.delete(self.middle_label)
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) 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: 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: def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL context: tk.Menu = tk.Menu(canvas)
self.context.entryconfigure(1, state=state) themes.style_menu(context)
self.context.tk_popup(event.x_root, event.y_root) 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: def click_delete(self) -> None:
self.canvas.delete_edge(self) self.delete()
def click_configure(self) -> None: def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self) dialog = LinkConfigurationDialog(self.app, self)
dialog.show() dialog.show()
def draw_link_options(self): def draw_link_options(self):
@ -455,3 +759,20 @@ class CanvasEdge(Edge):
lines.append(dup_line) lines.append(dup_line)
label = "\n".join(lines) label = "\n".join(lines)
self.middle_label_text(label) 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 logging
import tkinter as tk import tkinter as tk
from copy import deepcopy from copy import deepcopy
from tkinter import BooleanVar from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
from PIL import Image from PIL import Image
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import Interface, Link
Interface, from core.gui import appconfig
Link,
LinkType,
Node,
Session,
ThroughputsEvent,
)
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import ( from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, ShadowNode
EDGE_WIDTH,
CanvasEdge,
CanvasWirelessEdge,
Edge,
arc_edges,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, TypeToImage from core.gui.images import TypeToImage
from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.manager import CanvasManager
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
ZOOM_IN = 1.1 ZOOM_IN: float = 1.1
ZOOM_OUT = 0.9 ZOOM_OUT: float = 0.9
ICON_SIZE = 48 ICON_SIZE: int = 48
MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT} MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT} MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
BACKGROUND_COLOR: str = "#cccccc"
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())
class CanvasGraph(tk.Canvas): class CanvasGraph(tk.Canvas):
def __init__( 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: ) -> 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.app: "Application" = app
self.manager: "CanvasManager" = manager
self.core: "CoreClient" = core self.core: "CoreClient" = core
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {} self.selection: Dict[int, int] = {}
self.select_box: Optional[Shape] = None self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None self.selected: Optional[int] = None
self.node_draw: Optional[NodeDraw] = None
self.nodes: Dict[int, CanvasNode] = {} self.nodes: Dict[int, CanvasNode] = {}
self.edges: Dict[str, CanvasEdge] = {} self.shadow_nodes: Dict[int, ShadowNode] = {}
self.shapes: Dict[int, Shape] = {} 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 # map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network: Dict[int, Set[int]] = {} self.wireless_network: Dict[int, Set[int]] = {}
@ -81,10 +61,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge: Optional[CanvasEdge] = None self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None self.rect: Optional[int] = None
self.shape_drawing: bool = False self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width self.current_dimensions: Tuple[int, int] = dimensions
height = self.app.guiconfig.preferences.height
self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.ratio: float = 1.0 self.ratio: float = 1.0
self.offset: Tuple[int, int] = (0, 0) self.offset: Tuple[int, int] = (0, 0)
self.cursor: 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.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) 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 # bindings
self.setup_bindings() self.setup_bindings()
@ -126,7 +86,7 @@ class CanvasGraph(tk.Canvas):
if self.rect is not None: if self.rect is not None:
self.delete(self.rect) self.delete(self.rect)
if not dimensions: if not dimensions:
dimensions = self.default_dimensions dimensions = self.manager.default_dimensions
self.current_dimensions = dimensions self.current_dimensions = dimensions
self.rect = self.create_rectangle( self.rect = self.create_rectangle(
0, 0,
@ -139,34 +99,6 @@ class CanvasGraph(tk.Canvas):
) )
self.configure(scrollregion=self.bbox(tk.ALL)) 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: def setup_bindings(self) -> None:
""" """
Bind any mouse events or hot keys to the matching action 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("<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)) 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]: def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
actual_x = (x - self.offset[0]) / self.ratio actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / 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) valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright 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: def draw_grid(self) -> None:
""" """
Create grid. Create grid.
@ -228,123 +156,6 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE) self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect) 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]: def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
""" """
Convert window coordinate to canvas coordinate Convert window coordinate to canvas coordinate
@ -363,14 +174,12 @@ class CanvasGraph(tk.Canvas):
for _id in overlapping: for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id: if self.drawing_edge and self.drawing_edge.id == _id:
continue continue
elif _id in self.nodes:
if _id in self.nodes:
selected = _id selected = _id
break elif _id in self.shapes:
selected = _id
if _id in self.shapes: elif _id in self.shadow_nodes:
selected = _id selected = _id
return selected return selected
def click_release(self, event: tk.Event) -> None: def click_release(self, event: tk.Event) -> None:
@ -381,13 +190,13 @@ class CanvasGraph(tk.Canvas):
x, y = self.canvas_xy(event) x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y): if not self.inside_canvas(x, y):
return return
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
self.focus_set() self.focus_set()
if self.shape_drawing: if self.shape_drawing:
shape = self.shapes[self.selected] shape = self.shapes[self.selected]
shape.shape_complete(x, y) shape.shape_complete(x, y)
self.shape_drawing = False self.shape_drawing = False
elif self.mode == GraphMode.SELECT: elif self.manager.mode == GraphMode.SELECT:
self.focus_set() self.focus_set()
if self.select_box: if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id) x0, y0, x1, y1 = self.coords(self.select_box.id)
@ -403,61 +212,35 @@ class CanvasGraph(tk.Canvas):
else: else:
self.focus_set() self.focus_set()
self.selected = self.get_selected(event) self.selected = self.get_selected(event)
logging.debug(f"click release selected({self.selected}) mode({self.mode})") logging.debug(
if self.mode == GraphMode.EDGE: "click release selected(%s) mode(%s)", self.selected, self.manager.mode
)
if self.manager.mode == GraphMode.EDGE:
self.handle_edge_release(event) self.handle_edge_release(event)
elif self.mode == GraphMode.NODE: elif self.manager.mode == GraphMode.NODE:
self.add_node(x, y) self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE: elif self.manager.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE self.manager.mode = GraphMode.NODE
self.selected = None self.selected = None
def handle_edge_release(self, _event: tk.Event) -> None: def handle_edge_release(self, _event: tk.Event) -> None:
# not drawing edge return
if not self.drawing_edge:
return
edge = self.drawing_edge edge = self.drawing_edge
self.drawing_edge = None self.drawing_edge = None
# not drawing edge return
if edge is None:
return
# edge dst must be a node # edge dst must be a node
logging.debug("current selected: %s", self.selected) logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected) dst_node = self.nodes.get(self.selected)
if not dst_node or not src_node: if not dst_node:
edge.delete() edge.delete()
return return
# check if node can be linked
# edge dst is same as src, delete edge if not edge.src.is_linkable(dst_node):
if edge.src == self.selected:
edge.delete() edge.delete()
return 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 # finalize edge creation
self.complete_edge(src_node, dst_node, edge) self.manager.complete_edge(edge, dst_node)
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)
def select_object(self, object_id: int, choose_multiple: bool = False) -> None: 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 # delete node and related edges
if object_id in self.nodes: if object_id in self.nodes:
canvas_node = self.nodes.pop(object_id) 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 # delete related edges
for edge in canvas_node.edges: while canvas_node.edges:
edge = canvas_node.edges.pop()
if edge in edges: if edge in edges:
continue continue
edges.add(edge) edges.add(edge)
del self.edges[edge.token]
edge.delete() edge.delete()
# update node connected to edge being deleted # delete node
other_id = edge.src canvas_node.delete()
other_iface = edge.link.iface1 nodes.append(canvas_node)
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 shape # delete shape
if object_id in self.shapes: if object_id in self.shapes:
@ -534,27 +305,21 @@ class CanvasGraph(tk.Canvas):
self.selection.clear() self.selection.clear()
self.core.deleted_canvas_nodes(nodes) self.core.deleted_canvas_nodes(nodes)
self.core.deleted_canvas_edges(edges)
def delete_edge(self, edge: CanvasEdge) -> None: def hide_selected_objects(self) -> None:
edge.delete() for object_id in self.selection:
del self.edges[edge.token] # delete selection box
src_node = self.nodes[edge.src] selection_id = self.selection[object_id]
src_node.edges.discard(edge) self.delete(selection_id)
if edge.link.iface1: # hide node and related edges
del src_node.ifaces[edge.link.iface1.id] if object_id in self.nodes:
dst_node = self.nodes[edge.dst] canvas_node = self.nodes[object_id]
dst_node.edges.discard(edge) canvas_node.hide()
if edge.link.iface2:
del dst_node.ifaces[edge.link.iface2.id] def show_hidden(self) -> None:
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) for node in self.nodes.values():
if src_wireless: if node.hidden:
dst_node.delete_antenna() node.show()
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 zoom(self, event: tk.Event, factor: float = None) -> None: def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor: if not factor:
@ -588,13 +353,13 @@ class CanvasGraph(tk.Canvas):
y_check = self.cursor[1] - self.offset[1] y_check = self.cursor[1] - self.offset[1]
logging.debug("click press offset(%s, %s)", x_check, y_check) logging.debug("click press offset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node: if self.manager.mode == GraphMode.EDGE and is_node:
pos = self.coords(selected) node = self.nodes[selected]
self.drawing_edge = CanvasEdge(self, selected, pos, pos) self.drawing_edge = CanvasEdge(self.app, node)
self.organize() self.organize()
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type): if is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get() r = self.app.toolbar.marker_frame.size.get()
self.create_oval( self.create_oval(
x - r, x - r,
@ -604,11 +369,11 @@ class CanvasGraph(tk.Canvas):
fill=self.app.toolbar.marker_frame.color, fill=self.app.toolbar.marker_frame.color,
outline="", outline="",
tags=(tags.MARKER, tags.ANNOTATION), tags=(tags.MARKER, tags.ANNOTATION),
state=self.show_annotations.state(), state=self.manager.show_annotations.state(),
) )
return return
if selected is None: 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.selected = shape.id
self.shape_drawing = True self.shape_drawing = True
self.shapes[shape.id] = shape self.shapes[shape.id] = shape
@ -629,8 +394,18 @@ class CanvasGraph(tk.Canvas):
node.core_node.position.x, node.core_node.position.x,
node.core_node.position.y, 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: else:
if self.mode == GraphMode.SELECT: if self.manager.mode == GraphMode.SELECT:
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
self.select_box = shape self.select_box = shape
self.clear_selection() self.clear_selection()
@ -659,7 +434,7 @@ class CanvasGraph(tk.Canvas):
if self.select_box: if self.select_box:
self.select_box.delete() self.select_box.delete()
self.select_box = None 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 = self.shapes.pop(self.selected)
shape.delete() shape.delete()
self.shape_drawing = False self.shape_drawing = False
@ -669,14 +444,14 @@ class CanvasGraph(tk.Canvas):
y_offset = y - self.cursor[1] y_offset = y - self.cursor[1]
self.cursor = x, y self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None: if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None:
self.drawing_edge.move_dst(self.cursor) self.drawing_edge.drawing(self.cursor)
if self.mode == GraphMode.ANNOTATION: if self.manager.mode == GraphMode.ANNOTATION:
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[self.selected] shape = self.shapes[self.selected]
shape.shape_motion(x, y) shape.shape_motion(x, y)
return return
elif is_marker(self.annotation_type): elif is_marker(self.manager.annotation_type):
r = self.app.toolbar.marker_frame.size.get() r = self.app.toolbar.marker_frame.size.get()
self.create_oval( self.create_oval(
x - r, x - r,
@ -689,21 +464,26 @@ class CanvasGraph(tk.Canvas):
) )
return return
if self.mode == GraphMode.EDGE: if self.manager.mode == GraphMode.EDGE:
return return
# move selected objects # move selected objects
if self.selection: if self.selection:
for selected_id in 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 = self.shapes[selected_id]
shape.motion(x_offset, y_offset) shape.motion(x_offset, y_offset)
elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes:
if self.mode in MOVE_NODE_MODES and selected_id in self.nodes:
node = self.nodes[selected_id] node = self.nodes[selected_id]
node.motion(x_offset, y_offset, update=self.core.is_runtime()) 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: 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) self.select_box.shape_motion(x, y)
def press_delete(self, _event: tk.Event) -> None: def press_delete(self, _event: tk.Event) -> None:
@ -729,17 +509,23 @@ class CanvasGraph(tk.Canvas):
return return
actual_x, actual_y = self.get_actual_coords(x, y) actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node( 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: if not core_node:
return return
core_node.canvas = self.id
try: try:
image_enum = self.node_draw.image_enum image_enum = self.manager.node_draw.image_enum
self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE) self.manager.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE)
except AttributeError: except AttributeError:
image_file = self.node_draw.image_file image_file = self.manager.node_draw.image_file
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE) self.manager.node_draw.image = self.app.get_custom_icon(
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) image_file, ICON_SIZE
)
node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image)
self.nodes[node.id] = node self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node) self.core.set_canvas_node(core_node, node)
@ -847,7 +633,7 @@ class CanvasGraph(tk.Canvas):
# redraw gridlines to new canvas size # redraw gridlines to new canvas size
self.delete(tags.GRIDLINE) self.delete(tags.GRIDLINE)
self.draw_grid() self.draw_grid()
self.app.canvas.show_grid.click_handler() self.app.manager.show_grid.click_handler()
def redraw_wallpaper(self) -> None: def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
@ -871,7 +657,7 @@ class CanvasGraph(tk.Canvas):
self.tag_raise(tag) self.tag_raise(tag)
def set_wallpaper(self, filename: Optional[str]) -> None: 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: if filename:
img = Image.open(filename) img = Image.open(filename)
self.wallpaper = img self.wallpaper = img
@ -884,44 +670,16 @@ class CanvasGraph(tk.Canvas):
self.wallpaper_file = None self.wallpaper_file = None
def is_selection_mode(self) -> bool: 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: def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
""" """
create an edge between source node and destination node create an edge between source node and destination node
""" """
pos = (src.core_node.position.x, src.core_node.position.y) edge = CanvasEdge(self.app, src)
edge = CanvasEdge(self, src.id, pos, pos) self.manager.complete_edge(edge, dst)
self.complete_edge(src, dst, edge)
return edge 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: def copy(self) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("copy is disabled during runtime state") logging.debug("copy is disabled during runtime state")
@ -952,7 +710,9 @@ class CanvasGraph(tk.Canvas):
) )
if not copy: if not copy:
continue 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 # copy configurations and services
node.core_node.services = core_node.services.copy() node.core_node.services = core_node.services.copy()
node.core_node.config_services = core_node.config_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) 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: def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items(): for nid, canvas_node in self.nodes.items():
img = None img = None
@ -1085,3 +821,36 @@ class CanvasGraph(tk.Canvas):
for edge_id in self.find_withtag(tags.EDGE): for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) 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 logging
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set from typing import TYPE_CHECKING, Dict, List, Set, Tuple
import grpc import grpc
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
@ -31,10 +31,16 @@ NODE_TEXT_OFFSET: int = 5
class CanvasNode: class CanvasNode:
def __init__( 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.app: "Application" = app
self.canvas: "CanvasGraph" = app.canvas self.canvas: "CanvasGraph" = canvas
self.image: PhotoImage = image self.image: PhotoImage = image
self.core_node: Node = core_node self.core_node: Node = core_node
self.id: int = self.canvas.create_image( self.id: int = self.canvas.create_image(
@ -49,7 +55,7 @@ class CanvasNode:
tags=tags.NODE_LABEL, tags=tags.NODE_LABEL,
font=self.app.icon_text_font, font=self.app.icon_text_font,
fill="#0000CD", fill="#0000CD",
state=self.canvas.show_node_labels.state(), state=self.app.manager.show_node_labels.state(),
) )
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges: Set[CanvasEdge] = set() self.edges: Set[CanvasEdge] = set()
@ -57,10 +63,14 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set() self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = [] self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {} self.antenna_images: Dict[int, PhotoImage] = {}
self.hidden: bool = False
self.setup_bindings() self.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context) themes.style_menu(self.context)
def position(self) -> Tuple[int, int]:
return self.canvas.coords(self.id)
def next_iface_id(self) -> int: def next_iface_id(self) -> int:
i = 0 i = 0
while i in self.ifaces: while i in self.ifaces:
@ -81,7 +91,7 @@ class CanvasNode:
self.delete_antennas() self.delete_antennas()
def add_antenna(self) -> None: 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 offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
antenna_id = self.canvas.create_image( antenna_id = self.canvas.create_image(
@ -139,15 +149,14 @@ class CanvasNode:
def move(self, x: float, y: float) -> None: def move(self, x: float, y: float) -> None:
x, y = self.canvas.get_scaled_coords(x, y) 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 x_offset = x - current_x
y_offset = y - current_y y_offset = y - current_y
self.motion(x_offset, y_offset, update=False) self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None: 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) self.canvas.move(self.id, x_offset, y_offset)
pos = self.canvas.coords(self.id)
# check new position # check new position
bbox = self.canvas.bbox(self.id) bbox = self.canvas.bbox(self.id)
@ -165,11 +174,12 @@ class CanvasNode:
# move edges # move edges
for edge in self.edges: for edge in self.edges:
edge.move_node(self.id, pos) edge.move_node(self)
for edge in self.wireless_edges: 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 # set actual coords for node and update core is running
pos = self.position()
real_x, real_y = self.canvas.get_actual_coords(*pos) real_x, real_y = self.canvas.get_actual_coords(*pos)
self.core_node.position.x = real_x self.core_node.position.x = real_x
self.core_node.position.y = real_y self.core_node.position.y = real_y
@ -245,27 +255,44 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="Link To Selected", command=self.wireless_link_selected 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) unlink_menu = tk.Menu(self.context)
for edge in self.edges: for edge in self.edges:
link = edge.link link = edge.link
if self.id == edge.src: if self.id == edge.src.id:
other_id = edge.dst other_node = edge.dst
other_iface = link.iface2.name if link.iface2 else None other_iface = link.iface2.name if link.iface2 else None
else: else:
other_id = edge.src other_node = edge.src
other_iface = link.iface1.name if link.iface1 else None other_iface = link.iface1.name if link.iface1 else None
other_node = self.canvas.nodes[other_id]
other_name = other_node.core_node.name other_name = other_node.core_node.name
label = f"{other_name}:{other_iface}" if other_iface else other_name label = f"{other_name}:{other_iface}" if other_iface else other_name
func_unlink = functools.partial(self.click_unlink, edge) func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink) unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu) themes.style_menu(unlink_menu)
self.context.add_cascade(label="Unlink", menu=unlink_menu) self.context.add_cascade(label="Unlink", menu=unlink_menu)
edit_menu = tk.Menu(self.context) edit_menu = tk.Menu(self.context)
themes.style_menu(edit_menu) themes.style_menu(edit_menu)
edit_menu.add_command(label="Cut", command=self.click_cut) 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="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete) 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.add_cascade(label="Edit", menu=edit_menu)
self.context.tk_popup(event.x_root, event.y_root) self.context.tk_popup(event.x_root, event.y_root)
@ -274,9 +301,13 @@ class CanvasNode:
self.canvas_delete() self.canvas_delete()
def click_unlink(self, edge: CanvasEdge) -> None: def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge) edge.delete()
self.app.default_info() 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: def canvas_delete(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()
self.canvas.select_object(self.id) self.canvas.select_object(self.id)
@ -320,15 +351,14 @@ class CanvasNode:
def has_emane_link(self, iface_id: int) -> Node: def has_emane_link(self, iface_id: int) -> Node:
result = None result = None
for edge in self.edges: for edge in self.edges:
if self.id == edge.src: if self.id == edge.src.id:
other_id = edge.dst other_node = edge.dst
edge_iface_id = edge.link.iface1.id edge_iface_id = edge.link.iface1.id
else: else:
other_id = edge.src other_node = edge.src
edge_iface_id = edge.link.iface2.id edge_iface_id = edge.link.iface2.id
if edge_iface_id != iface_id: if edge_iface_id != iface_id:
continue continue
other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE: if other_node.core_node.type == NodeType.EMANE:
result = other_node.core_node result = other_node.core_node
break break
@ -360,3 +390,43 @@ class CanvasNode:
self.core_node.icon = icon_path self.core_node.icon = icon_path
self.image = Images.create(icon_path, nodeutils.ICON_SIZE) self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
self.canvas.itemconfig(self.id, image=self.image) 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 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.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
@ -69,6 +69,31 @@ class Shape:
self.shape_data = data self.shape_data = data
self.draw() 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: def draw(self) -> None:
if self.created: if self.created:
dash = None dash = None
@ -85,7 +110,7 @@ class Shape:
fill=self.shape_data.fill_color, fill=self.shape_data.fill_color,
outline=self.shape_data.border_color, outline=self.shape_data.border_color,
width=self.shape_data.border_width, width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
self.draw_shape_text() self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE: elif self.shape_type == ShapeType.RECTANGLE:
@ -99,7 +124,7 @@ class Shape:
fill=self.shape_data.fill_color, fill=self.shape_data.fill_color,
outline=self.shape_data.border_color, outline=self.shape_data.border_color,
width=self.shape_data.border_width, width=self.shape_data.border_width,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
self.draw_shape_text() self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT: elif self.shape_type == ShapeType.TEXT:
@ -111,7 +136,7 @@ class Shape:
text=self.shape_data.text, text=self.shape_data.text,
fill=self.shape_data.text_color, fill=self.shape_data.text_color,
font=font, font=font,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
else: else:
logging.error("unknown shape type: %s", self.shape_type) logging.error("unknown shape type: %s", self.shape_type)
@ -139,7 +164,7 @@ class Shape:
text=self.shape_data.text, text=self.shape_data.text,
fill=self.shape_data.text_color, fill=self.shape_data.text_color,
font=font, font=font,
state=self.canvas.show_annotations.state(), state=self.app.manager.show_annotations.state(),
) )
def shape_motion(self, x1: float, y1: float) -> None: def shape_motion(self, x1: float, y1: float) -> None:
@ -184,6 +209,7 @@ class Shape:
x1, y1 = self.canvas.get_actual_coords(x1, y1) x1, y1 = self.canvas.get_actual_coords(x1, y1)
coords = (x1, y1) coords = (x1, y1)
return { return {
"canvas": self.canvas.id,
"type": self.shape_type.value, "type": self.shape_type.value,
"iconcoords": coords, "iconcoords": coords,
"label": self.shape_data.text, "label": self.shape_data.text,

View file

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

View file

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

View file

@ -188,19 +188,16 @@ class InterfaceManager:
self, canvas_node: CanvasNode, visited: Set[int] = None self, canvas_node: CanvasNode, visited: Set[int] = None
) -> Optional[IPNetwork]: ) -> Optional[IPNetwork]:
logging.info("finding subnet for node: %s", canvas_node.core_node.name) logging.info("finding subnet for node: %s", canvas_node.core_node.name)
canvas = self.app.canvas
subnets = None subnets = None
if not visited: if not visited:
visited = set() visited = set()
visited.add(canvas_node.core_node.id) visited.add(canvas_node.core_node.id)
for edge in canvas_node.edges: for edge in canvas_node.edges:
src_node = canvas.nodes[edge.src]
dst_node = canvas.nodes[edge.dst]
iface = edge.link.iface1 iface = edge.link.iface1
check_node = src_node check_node = edge.src
if src_node == canvas_node: if edge.src == canvas_node:
iface = edge.link.iface2 iface = edge.link.iface2
check_node = dst_node check_node = edge.dst
if check_node.core_node.id in visited: if check_node.core_node.id in visited:
continue continue
visited.add(check_node.core_node.id) 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.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog 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.nodeutils import ICON_SIZE
from core.gui.observers import ObserversMenu from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask from core.gui.task import ProgressTask
@ -45,9 +45,10 @@ class Menubar(tk.Menu):
super().__init__(app) super().__init__(app)
self.app: "Application" = app self.app: "Application" = app
self.core: CoreClient = app.core self.core: CoreClient = app.core
self.canvas: CanvasGraph = app.canvas self.manager: CanvasManager = app.manager
self.recent_menu: Optional[tk.Menu] = None self.recent_menu: Optional[tk.Menu] = None
self.edit_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.observers_menu: Optional[ObserversMenu] = None
self.draw() self.draw()
@ -106,6 +107,7 @@ class Menubar(tk.Menu):
menu = tk.Menu(self) menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes) 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_separator()
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", 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( menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.click_delete 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.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-x>", self.click_cut) 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-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste) 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-d>", self.click_delete)
self.app.master.bind_all("<Control-h>", self.click_hide)
self.edit_menu = menu self.edit_menu = menu
def draw_canvas_menu(self) -> None: def draw_canvas_menu(self) -> None:
@ -128,9 +132,13 @@ class Menubar(tk.Menu):
Create canvas menu Create canvas menu
""" """
menu = tk.Menu(self) 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_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) menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu) self.add_cascade(label="Canvas", menu=menu)
self.canvas_menu = menu
def draw_view_menu(self) -> None: def draw_view_menu(self) -> None:
""" """
@ -145,52 +153,52 @@ class Menubar(tk.Menu):
menu.add_checkbutton( menu.add_checkbutton(
label="Interface Names", label="Interface Names",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_iface_names, variable=self.manager.show_iface_names,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="IPv4 Addresses", label="IPv4 Addresses",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_ip4s, variable=self.manager.show_ip4s,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="IPv6 Addresses", label="IPv6 Addresses",
command=self.click_edge_label_change, command=self.click_edge_label_change,
variable=self.canvas.show_ip6s, variable=self.manager.show_ip6s,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Node Labels", label="Node Labels",
command=self.canvas.show_node_labels.click_handler, command=self.manager.show_node_labels.click_handler,
variable=self.canvas.show_node_labels, variable=self.manager.show_node_labels,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Link Labels", label="Link Labels",
command=self.canvas.show_link_labels.click_handler, command=self.manager.show_link_labels.click_handler,
variable=self.canvas.show_link_labels, variable=self.manager.show_link_labels,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Links", label="Links",
command=self.canvas.show_links.click_handler, command=self.manager.show_links.click_handler,
variable=self.canvas.show_links, variable=self.manager.show_links,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Loss Links", label="Loss Links",
command=self.canvas.show_loss_links.click_handler, command=self.manager.show_loss_links.click_handler,
variable=self.canvas.show_loss_links, variable=self.manager.show_loss_links,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Wireless Links", label="Wireless Links",
command=self.canvas.show_wireless.click_handler, command=self.manager.show_wireless.click_handler,
variable=self.canvas.show_wireless, variable=self.manager.show_wireless,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Annotations", label="Annotations",
command=self.canvas.show_annotations.click_handler, command=self.manager.show_annotations.click_handler,
variable=self.canvas.show_annotations, variable=self.manager.show_annotations,
) )
menu.add_checkbutton( menu.add_checkbutton(
label="Canvas Grid", label="Canvas Grid",
command=self.canvas.show_grid.click_handler, command=self.manager.show_grid.click_handler,
variable=self.canvas.show_grid, variable=self.manager.show_grid,
) )
self.add_cascade(label="View", menu=menu) self.add_cascade(label="View", menu=menu)
@ -334,17 +342,12 @@ class Menubar(tk.Menu):
self.app.save_config() self.app.save_config()
self.app.menubar.update_recent_files() self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None: def set_state(self, is_runtime: bool) -> None:
labels = {"Copy", "Paste", "Delete", "Cut"} state = tk.DISABLED if is_runtime else tk.NORMAL
for i in range(self.edit_menu.index(tk.END) + 1): for entry in {"Copy", "Paste", "Delete", "Cut"}:
try: self.edit_menu.entryconfigure(entry, state=state)
label = self.edit_menu.entrycget(i, "label") for entry in {"Delete"}:
if label not in labels: self.canvas_menu.entryconfigure(entry, state=state)
continue
state = tk.DISABLED if is_runtime else tk.NORMAL
self.edit_menu.entryconfig(i, state=state)
except tk.TclError:
pass
def prompt_save_running_session(self, quit_app: bool = False) -> None: def prompt_save_running_session(self, quit_app: bool = False) -> None:
""" """
@ -372,6 +375,12 @@ class Menubar(tk.Menu):
dialog = PreferencesDialog(self.app) dialog = PreferencesDialog(self.app)
dialog.show() 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: def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app) dialog = SizeAndScaleDialog(self.app)
dialog.show() dialog.show()
@ -401,17 +410,29 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_copy(self, _event: tk.Event = None) -> None: 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: 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: 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: def click_cut(self, _event: tk.Event = None) -> None:
self.canvas.copy() canvas = self.manager.current()
self.canvas.delete_selected_objects() 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: def click_session_options(self) -> None:
logging.debug("Click options") logging.debug("Click options")
@ -439,14 +460,15 @@ class Menubar(tk.Menu):
dialog.show() dialog.show()
def click_autogrid(self) -> None: def click_autogrid(self) -> None:
width, height = self.canvas.current_dimensions width, height = self.manager.current_dimensions
padding = (ICON_SIZE / 2) + 10 padding = (ICON_SIZE / 2) + 10
layout_size = padding + ICON_SIZE layout_size = padding + ICON_SIZE
col_count = width // layout_size col_count = width // layout_size
logging.info( logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count "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 col = i % col_count
row = i // col_count row = i // col_count
x = (col * layout_size) + padding x = (col * layout_size) + padding
@ -460,7 +482,7 @@ class Menubar(tk.Menu):
self.app.hide_info() self.app.hide_info()
def click_edge_label_change(self) -> None: def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values(): for edge in self.manager.edges.values():
edge.draw_labels() edge.draw_labels()
def click_mac_config(self) -> None: 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 = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
self.zoom.grid(row=0, column=1, sticky=tk.EW) self.zoom.grid(row=0, column=1, sticky=tk.EW)
self.set_zoom(self.app.canvas.ratio)
self.cpu_label = ttk.Label( self.cpu_label = ttk.Label(
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE 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") Tooltip(self.color_frame, "Marker Color")
def click_clear(self) -> None: 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: def click_color(self, _event: tk.Event) -> None:
dialog = ColorPickerDialog(self.app, self.app, self.color) dialog = ColorPickerDialog(self.app, self.app, self.color)
@ -257,8 +258,8 @@ class Toolbar(ttk.Frame):
def draw_node_picker(self) -> None: def draw_node_picker(self) -> None:
self.hide_marker() self.hide_marker()
self.app.canvas.mode = GraphMode.NODE self.app.manager.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_node self.app.manager.node_draw = self.current_node
self.design_frame.select_radio(self.node_button) self.design_frame.select_radio(self.node_button)
self.picker = PickerFrame(self.app, self.node_button) self.picker = PickerFrame(self.app, self.node_button)
# draw default nodes # draw default nodes
@ -278,12 +279,12 @@ class Toolbar(ttk.Frame):
def click_selection(self) -> None: def click_selection(self) -> None:
self.design_frame.select_radio(self.select_button) self.design_frame.select_radio(self.select_button)
self.app.canvas.mode = GraphMode.SELECT self.app.manager.mode = GraphMode.SELECT
self.hide_marker() self.hide_marker()
def click_runtime_selection(self) -> None: def click_runtime_selection(self) -> None:
self.runtime_frame.select_radio(self.runtime_select_button) self.runtime_frame.select_radio(self.runtime_select_button)
self.app.canvas.mode = GraphMode.SELECT self.app.manager.mode = GraphMode.SELECT
self.hide_marker() self.hide_marker()
def click_start(self) -> None: 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 Start session handler redraw buttons, send node and link messages to grpc
server. server.
""" """
self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.menubar.set_state(is_runtime=True)
self.app.canvas.mode = GraphMode.SELECT self.app.manager.mode = GraphMode.SELECT
enable_buttons(self.design_frame, enabled=False) enable_buttons(self.design_frame, enabled=False)
task = ProgressTask( task = ProgressTask(
self.app, "Start", self.app.core.start_session, self.start_callback self.app, "Start", self.app.core.start_session, self.start_callback
@ -324,7 +325,7 @@ class Toolbar(ttk.Frame):
def click_link(self) -> None: def click_link(self) -> None:
self.design_frame.select_radio(self.link_button) self.design_frame.select_radio(self.link_button)
self.app.canvas.mode = GraphMode.EDGE self.app.manager.mode = GraphMode.EDGE
self.hide_marker() self.hide_marker()
def update_button( def update_button(
@ -337,7 +338,7 @@ class Toolbar(ttk.Frame):
logging.debug("update button(%s): %s", button, node_draw) logging.debug("update button(%s): %s", button, node_draw)
button.configure(image=image) button.configure(image=image)
button.image = image button.image = image
self.app.canvas.node_draw = node_draw self.app.manager.node_draw = node_draw
if type_enum == NodeTypeEnum.NODE: if type_enum == NodeTypeEnum.NODE:
self.current_node = node_draw self.current_node = node_draw
elif type_enum == NodeTypeEnum.NETWORK: elif type_enum == NodeTypeEnum.NETWORK:
@ -348,8 +349,8 @@ class Toolbar(ttk.Frame):
Draw the options for link-layer button. Draw the options for link-layer button.
""" """
self.hide_marker() self.hide_marker()
self.app.canvas.mode = GraphMode.NODE self.app.manager.mode = GraphMode.NODE
self.app.canvas.node_draw = self.current_network self.app.manager.node_draw = self.current_network
self.design_frame.select_radio(self.network_button) self.design_frame.select_radio(self.network_button)
self.picker = PickerFrame(self.app, self.network_button) self.picker = PickerFrame(self.app, self.network_button)
for node_draw in NodeUtils.NETWORK_NODES: for node_draw in NodeUtils.NETWORK_NODES:
@ -364,8 +365,8 @@ class Toolbar(ttk.Frame):
Draw the options for marker button. Draw the options for marker button.
""" """
self.design_frame.select_radio(self.annotation_button) self.design_frame.select_radio(self.annotation_button)
self.app.canvas.mode = GraphMode.ANNOTATION self.app.manager.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = self.current_annotation self.app.manager.annotation_type = self.current_annotation
if is_marker(self.current_annotation): if is_marker(self.current_annotation):
self.show_marker() self.show_marker()
self.picker = PickerFrame(self.app, self.annotation_button) 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 redraw buttons on the toolbar, send node and link messages to grpc server
""" """
logging.info("clicked stop button") 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() self.app.core.close_mobility_players()
enable_buttons(self.runtime_frame, enabled=False) enable_buttons(self.runtime_frame, enabled=False)
task = ProgressTask( task = ProgressTask(
@ -406,7 +407,7 @@ class Toolbar(ttk.Frame):
def stop_callback(self, result: bool) -> None: def stop_callback(self, result: bool) -> None:
self.set_design() self.set_design()
self.app.canvas.stopped_session() self.app.manager.stopped_session()
def update_annotation( def update_annotation(
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
@ -414,7 +415,7 @@ class Toolbar(ttk.Frame):
logging.debug("clicked annotation") logging.debug("clicked annotation")
self.annotation_button.configure(image=image) self.annotation_button.configure(image=image)
self.annotation_button.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.current_annotation = shape_type
self.annotation_enum = image_enum self.annotation_enum = image_enum
if is_marker(shape_type): if is_marker(shape_type):
@ -435,8 +436,8 @@ class Toolbar(ttk.Frame):
def click_marker_button(self) -> None: def click_marker_button(self) -> None:
self.runtime_frame.select_radio(self.runtime_marker_button) self.runtime_frame.select_radio(self.runtime_marker_button)
self.app.canvas.mode = GraphMode.ANNOTATION self.app.manager.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = ShapeType.MARKER self.app.manager.annotation_type = ShapeType.MARKER
self.show_marker() self.show_marker()
def scale_button( def scale_button(

View file

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