commit
b2726b627f
58 changed files with 2564 additions and 1805 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,3 +1,22 @@
|
|||
## 2021-03-11 CORE 7.5.0
|
||||
|
||||
* core-daemon
|
||||
* fixed issue setting mobility loop value properly
|
||||
* fixed issue that some states would not properly remove session directories
|
||||
* \#560 - fixed issues with sdt integration for mobility movement and layer creation
|
||||
* core-pygui
|
||||
* added multiple canvas support
|
||||
* added support to hide nodes and restore them visually
|
||||
* update to assign full netmasks to wireless connected nodes by default
|
||||
* update to display services and action controls for nodes during runtime
|
||||
* fixed issues with custom nodes
|
||||
* fixed issue auto assigning macs, avoiding duplication
|
||||
* fixed issue joining session with different netmasks
|
||||
* fixed issues when deleting a session from the sessions dialog
|
||||
* \#550 - fixed issue not sending all service customization data
|
||||
* core-cli
|
||||
* added delete session command
|
||||
|
||||
## 2021-01-11 CORE 7.4.0
|
||||
|
||||
* Installation
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 7.4.0)
|
||||
AC_INIT(core, 7.5.0)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
|
|
@ -25,7 +25,9 @@ from core.emulator.session import Session
|
|||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.lxd import LxcNode
|
||||
from core.nodes.network import WlanNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
|
@ -67,6 +69,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
|
|||
image=node_proto.image,
|
||||
services=node_proto.services,
|
||||
config_services=node_proto.config_services,
|
||||
canvas=node_proto.canvas,
|
||||
)
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
|
@ -263,19 +266,22 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
|||
geo = core_pb2.Geo(
|
||||
lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
|
||||
)
|
||||
services = getattr(node, "services", [])
|
||||
if services is None:
|
||||
services = []
|
||||
services = [x.name for x in services]
|
||||
config_services = getattr(node, "config_services", {})
|
||||
config_services = [x for x in config_services]
|
||||
services = [x.name for x in node.services]
|
||||
model = node.type
|
||||
node_dir = None
|
||||
config_services = []
|
||||
if isinstance(node, CoreNodeBase):
|
||||
node_dir = node.nodedir
|
||||
config_services = [x for x in node.config_services]
|
||||
channel = None
|
||||
if isinstance(node, CoreNode):
|
||||
channel = node.ctrlchnlname
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.model.name
|
||||
model = getattr(node, "type", None)
|
||||
node_dir = getattr(node, "nodedir", None)
|
||||
channel = getattr(node, "ctrlchnlname", None)
|
||||
image = getattr(node, "image", None)
|
||||
image = None
|
||||
if isinstance(node, (DockerNode, LxcNode)):
|
||||
image = node.image
|
||||
return core_pb2.Node(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
|
@ -290,6 +296,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node:
|
|||
config_services=config_services,
|
||||
dir=node_dir,
|
||||
channel=channel,
|
||||
canvas=node.canvas,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -649,6 +649,7 @@ class Node:
|
|||
geo: Geo = None
|
||||
dir: str = None
|
||||
channel: str = None
|
||||
canvas: int = None
|
||||
|
||||
# configurations
|
||||
emane_model_configs: Dict[
|
||||
|
@ -683,6 +684,7 @@ class Node:
|
|||
geo=Geo.from_proto(proto.geo),
|
||||
dir=proto.dir,
|
||||
channel=proto.channel,
|
||||
canvas=proto.canvas,
|
||||
)
|
||||
|
||||
def to_proto(self) -> core_pb2.Node:
|
||||
|
@ -700,6 +702,7 @@ class Node:
|
|||
server=self.server,
|
||||
dir=self.dir,
|
||||
channel=self.channel,
|
||||
canvas=self.canvas,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -103,8 +103,7 @@ class FRRZebra(ConfigService):
|
|||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
|
|
|
@ -104,8 +104,7 @@ class Zebra(ConfigService):
|
|||
ip4s.append(str(ip4.ip))
|
||||
for ip6 in iface.ip6s:
|
||||
ip6s.append(str(ip6.ip))
|
||||
is_control = getattr(iface, "control", False)
|
||||
ifaces.append((iface, ip4s, ip6s, is_control))
|
||||
ifaces.append((iface, ip4s, ip6s, iface.control))
|
||||
|
||||
return dict(
|
||||
quagga_bin_search=quagga_bin_search,
|
||||
|
|
|
@ -134,7 +134,6 @@ class Session:
|
|||
self.link_handlers: List[Callable[[LinkData], None]] = []
|
||||
self.file_handlers: List[Callable[[FileData], None]] = []
|
||||
self.config_handlers: List[Callable[[ConfigData], None]] = []
|
||||
self.shutdown_handlers: List[Callable[[Session], None]] = []
|
||||
|
||||
# session options/metadata
|
||||
self.options: SessionConfig = SessionConfig()
|
||||
|
@ -591,7 +590,6 @@ class Session:
|
|||
:raises core.CoreError: when node to update does not exist
|
||||
"""
|
||||
node = self.get_node(node_id, NodeBase)
|
||||
node.canvas = options.canvas
|
||||
node.icon = options.icon
|
||||
self.set_node_position(node, options)
|
||||
self.sdt.edit_node(node, options.lon, options.lat, options.alt)
|
||||
|
@ -772,20 +770,17 @@ class Session:
|
|||
"""
|
||||
if self.state == EventTypes.SHUTDOWN_STATE:
|
||||
logging.info("session(%s) state(%s) already shutdown", self.id, self.state)
|
||||
return
|
||||
logging.info("session(%s) state(%s) shutting down", self.id, self.state)
|
||||
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
|
||||
# clear out current core session
|
||||
self.clear()
|
||||
# shutdown sdt
|
||||
self.sdt.shutdown()
|
||||
else:
|
||||
logging.info("session(%s) state(%s) shutting down", self.id, self.state)
|
||||
self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
|
||||
# clear out current core session
|
||||
self.clear()
|
||||
# shutdown sdt
|
||||
self.sdt.shutdown()
|
||||
# remove this sessions working directory
|
||||
preserve = self.options.get_config("preservedir") == "1"
|
||||
if not preserve:
|
||||
shutil.rmtree(self.session_dir, ignore_errors=True)
|
||||
# call session shutdown handlers
|
||||
for handler in self.shutdown_handlers:
|
||||
handler(self)
|
||||
|
||||
def broadcast_event(self, event_data: EventData) -> None:
|
||||
"""
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from tkinter import PhotoImage, font, ttk
|
||||
from tkinter import PhotoImage, font, messagebox, ttk
|
||||
from tkinter.ttk import Progressbar
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
import grpc
|
||||
|
||||
from core.gui import appconfig, themes
|
||||
from core.gui import appconfig, images
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui import themes
|
||||
from core.gui.appconfig import GuiConfig
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.dialogs.error import ErrorDialog
|
||||
from core.gui.frames.base import InfoFrameBase
|
||||
from core.gui.frames.default import DefaultInfoFrame
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.menubar import Menubar
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.statusbar import StatusBar
|
||||
from core.gui.themes import PADY
|
||||
from core.gui.toolbar import Toolbar
|
||||
|
@ -29,13 +30,13 @@ class Application(ttk.Frame):
|
|||
def __init__(self, proxy: bool, session_id: int = None) -> None:
|
||||
super().__init__()
|
||||
# load node icons
|
||||
NodeUtils.setup()
|
||||
nutils.setup()
|
||||
|
||||
# widgets
|
||||
self.menubar: Optional[Menubar] = None
|
||||
self.toolbar: Optional[Toolbar] = None
|
||||
self.right_frame: Optional[ttk.Frame] = None
|
||||
self.canvas: Optional[CanvasGraph] = None
|
||||
self.manager: Optional[CanvasManager] = None
|
||||
self.statusbar: Optional[StatusBar] = None
|
||||
self.progress: Optional[Progressbar] = None
|
||||
self.infobar: Optional[ttk.Frame] = None
|
||||
|
@ -77,7 +78,7 @@ class Application(ttk.Frame):
|
|||
self.master.title("CORE")
|
||||
self.center()
|
||||
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
self.master.tk.call("wm", "iconphoto", self.master._w, image)
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.setup_file_dialogs()
|
||||
|
@ -136,20 +137,8 @@ class Application(ttk.Frame):
|
|||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
def draw_canvas(self) -> None:
|
||||
canvas_frame = ttk.Frame(self.right_frame)
|
||||
canvas_frame.rowconfigure(0, weight=1)
|
||||
canvas_frame.columnconfigure(0, weight=1)
|
||||
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
|
||||
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
||||
self.canvas.grid(sticky=tk.NSEW)
|
||||
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
||||
scroll_y.grid(row=0, column=1, sticky=tk.NS)
|
||||
scroll_x = ttk.Scrollbar(
|
||||
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.grid(row=1, column=0, sticky=tk.EW)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
self.manager = CanvasManager(self.right_frame, self, self.core)
|
||||
self.manager.notebook.grid(sticky=tk.NSEW)
|
||||
|
||||
def draw_status(self) -> None:
|
||||
self.statusbar = StatusBar(self.right_frame, self)
|
||||
|
@ -179,17 +168,30 @@ class Application(ttk.Frame):
|
|||
def hide_info(self) -> None:
|
||||
self.infobar.grid_forget()
|
||||
|
||||
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
|
||||
def show_grpc_exception(
|
||||
self, message: str, e: grpc.RpcError, blocking: bool = False
|
||||
) -> None:
|
||||
logging.exception("app grpc exception", exc_info=e)
|
||||
message = e.details()
|
||||
self.show_error(title, message)
|
||||
dialog = ErrorDialog(self, "GRPC Exception", message, e.details())
|
||||
if blocking:
|
||||
dialog.show()
|
||||
else:
|
||||
self.after(0, lambda: dialog.show())
|
||||
|
||||
def show_exception(self, title: str, e: Exception) -> None:
|
||||
def show_exception(self, message: str, e: Exception) -> None:
|
||||
logging.exception("app exception", exc_info=e)
|
||||
self.show_error(title, str(e))
|
||||
self.after(
|
||||
0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show()
|
||||
)
|
||||
|
||||
def show_error(self, title: str, message: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message).show())
|
||||
def show_exception_data(self, title: str, message: str, details: str) -> None:
|
||||
self.after(0, lambda: ErrorDialog(self, title, message, details).show())
|
||||
|
||||
def show_error(self, title: str, message: str, blocking: bool = False) -> None:
|
||||
if blocking:
|
||||
messagebox.showerror(title, message, parent=self)
|
||||
else:
|
||||
self.after(0, lambda: messagebox.showerror(title, message, parent=self))
|
||||
|
||||
def on_closing(self) -> None:
|
||||
if self.toolbar.picker:
|
||||
|
@ -201,15 +203,17 @@ class Application(ttk.Frame):
|
|||
|
||||
def joined_session_update(self) -> None:
|
||||
if self.core.is_runtime():
|
||||
self.menubar.set_state(is_runtime=True)
|
||||
self.toolbar.set_runtime()
|
||||
else:
|
||||
self.menubar.set_state(is_runtime=False)
|
||||
self.toolbar.set_design()
|
||||
|
||||
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
|
||||
return Images.get(image_enum, int(width * self.app_scale))
|
||||
def get_enum_icon(self, image_enum: ImageEnum, *, width: int) -> PhotoImage:
|
||||
return images.from_enum(image_enum, width=width, scale=self.app_scale)
|
||||
|
||||
def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
|
||||
return Images.get_custom(image_file, int(width * self.app_scale))
|
||||
def get_file_icon(self, file_path: str, *, width: int) -> PhotoImage:
|
||||
return images.from_file(file_path, width=width, scale=self.app_scale)
|
||||
|
||||
def close(self) -> None:
|
||||
self.master.destroy()
|
||||
|
|
|
@ -185,7 +185,8 @@ class GuiConfig(yaml.YAMLObject):
|
|||
def copy_files(current_path: Path, new_path: Path) -> None:
|
||||
for current_file in current_path.glob("*"):
|
||||
new_file = new_path.joinpath(current_file.name)
|
||||
shutil.copy(current_file, new_file)
|
||||
if not new_file.exists():
|
||||
shutil.copy(current_file, new_file)
|
||||
|
||||
|
||||
def find_terminal() -> Optional[str]:
|
||||
|
@ -197,30 +198,27 @@ def find_terminal() -> Optional[str]:
|
|||
|
||||
|
||||
def check_directory() -> None:
|
||||
if HOME_PATH.exists():
|
||||
return
|
||||
HOME_PATH.mkdir()
|
||||
BACKGROUNDS_PATH.mkdir()
|
||||
CUSTOM_EMANE_PATH.mkdir()
|
||||
CUSTOM_SERVICE_PATH.mkdir()
|
||||
ICONS_PATH.mkdir()
|
||||
MOBILITY_PATH.mkdir()
|
||||
XMLS_PATH.mkdir()
|
||||
SCRIPT_PATH.mkdir()
|
||||
|
||||
HOME_PATH.mkdir(exist_ok=True)
|
||||
BACKGROUNDS_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_EMANE_PATH.mkdir(exist_ok=True)
|
||||
CUSTOM_SERVICE_PATH.mkdir(exist_ok=True)
|
||||
ICONS_PATH.mkdir(exist_ok=True)
|
||||
MOBILITY_PATH.mkdir(exist_ok=True)
|
||||
XMLS_PATH.mkdir(exist_ok=True)
|
||||
SCRIPT_PATH.mkdir(exist_ok=True)
|
||||
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
|
||||
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
|
||||
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
|
||||
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
|
||||
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
if not CONFIG_PATH.exists():
|
||||
terminal = find_terminal()
|
||||
if "EDITOR" in os.environ:
|
||||
editor = EDITORS[0]
|
||||
else:
|
||||
editor = EDITORS[1]
|
||||
preferences = PreferencesConfig(editor, terminal)
|
||||
config = GuiConfig(preferences=preferences)
|
||||
save(config)
|
||||
|
||||
|
||||
def read() -> GuiConfig:
|
||||
|
|
|
@ -6,7 +6,6 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
||||
|
@ -25,7 +24,6 @@ from core.api.grpc.wrappers import (
|
|||
ConfigOption,
|
||||
ConfigService,
|
||||
ExceptionEvent,
|
||||
Interface,
|
||||
Link,
|
||||
LinkEvent,
|
||||
LinkType,
|
||||
|
@ -40,18 +38,16 @@ from core.api.grpc.wrappers import (
|
|||
SessionState,
|
||||
ThroughputsEvent,
|
||||
)
|
||||
from core.gui import appconfig
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.appconfig import XMLS_PATH, CoreServer, Observer
|
||||
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
|
||||
from core.gui.dialogs.error import ErrorDialog
|
||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
from core.gui.graph.edges import CanvasEdge
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.shape import AnnotationData, Shape
|
||||
from core.gui.graph.shapeutils import ShapeType
|
||||
from core.gui.graph.shape import Shape
|
||||
from core.gui.interface import InterfaceManager
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -155,7 +151,7 @@ class CoreClient:
|
|||
self.custom_observers[observer.name] = observer
|
||||
|
||||
def handle_events(self, event: core_pb2.Event) -> None:
|
||||
if event.source == GUI_SOURCE:
|
||||
if not self.session or event.source == GUI_SOURCE:
|
||||
return
|
||||
if event.session_id != self.session.id:
|
||||
logging.warning(
|
||||
|
@ -207,27 +203,26 @@ class CoreClient:
|
|||
canvas_node2 = self.canvas_nodes[node2_id]
|
||||
if event.link.type == LinkType.WIRELESS:
|
||||
if event.message_type == MessageType.ADD:
|
||||
self.app.canvas.add_wireless_edge(
|
||||
self.app.manager.add_wireless_edge(
|
||||
canvas_node1, canvas_node2, event.link
|
||||
)
|
||||
elif event.message_type == MessageType.DELETE:
|
||||
self.app.canvas.delete_wireless_edge(
|
||||
self.app.manager.delete_wireless_edge(
|
||||
canvas_node1, canvas_node2, event.link
|
||||
)
|
||||
elif event.message_type == MessageType.NONE:
|
||||
self.app.canvas.update_wireless_edge(
|
||||
self.app.manager.update_wireless_edge(
|
||||
canvas_node1, canvas_node2, event.link
|
||||
)
|
||||
else:
|
||||
logging.warning("unknown link event: %s", event)
|
||||
else:
|
||||
if event.message_type == MessageType.ADD:
|
||||
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
|
||||
self.app.canvas.organize()
|
||||
self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link)
|
||||
elif event.message_type == MessageType.DELETE:
|
||||
self.app.canvas.delete_wired_edge(event.link)
|
||||
self.app.manager.delete_wired_edge(event.link)
|
||||
elif event.message_type == MessageType.NONE:
|
||||
self.app.canvas.update_wired_edge(event.link)
|
||||
self.app.manager.update_wired_edge(event.link)
|
||||
else:
|
||||
logging.warning("unknown link event: %s", event)
|
||||
|
||||
|
@ -243,13 +238,13 @@ class CoreClient:
|
|||
canvas_node.update_icon(node.icon)
|
||||
elif event.message_type == MessageType.DELETE:
|
||||
canvas_node = self.canvas_nodes[node.id]
|
||||
self.app.canvas.clear_selection()
|
||||
self.app.canvas.select_object(canvas_node.id)
|
||||
self.app.canvas.delete_selected_objects()
|
||||
canvas_node.canvas.clear_selection()
|
||||
canvas_node.canvas.select_object(canvas_node.id)
|
||||
canvas_node.canvas.delete_selected_objects()
|
||||
elif event.message_type == MessageType.ADD:
|
||||
if node.id in self.session.nodes:
|
||||
logging.error("core node already exists: %s", node)
|
||||
self.app.canvas.add_core_node(node)
|
||||
self.app.manager.add_core_node(node)
|
||||
else:
|
||||
logging.warning("unknown node event: %s", event)
|
||||
|
||||
|
@ -262,7 +257,7 @@ class CoreClient:
|
|||
if self.handling_throughputs:
|
||||
self.handling_throughputs.cancel()
|
||||
self.handling_throughputs = None
|
||||
self.app.canvas.clear_throughputs()
|
||||
self.app.manager.clear_throughputs()
|
||||
|
||||
def cancel_events(self) -> None:
|
||||
if self.handling_events:
|
||||
|
@ -293,7 +288,7 @@ class CoreClient:
|
|||
)
|
||||
return
|
||||
logging.debug("handling throughputs event: %s", event)
|
||||
self.app.after(0, self.app.canvas.set_throughputs, event)
|
||||
self.app.after(0, self.app.manager.set_throughputs, event)
|
||||
|
||||
def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None:
|
||||
self.app.after(0, self.app.statusbar.set_cpu, event.usage)
|
||||
|
@ -315,9 +310,7 @@ class CoreClient:
|
|||
self.session.id, self.handle_events
|
||||
)
|
||||
self.ifaces_manager.joined(self.session.links)
|
||||
self.app.canvas.reset_and_redraw(self.session)
|
||||
self.parse_metadata()
|
||||
self.app.canvas.organize()
|
||||
self.app.manager.join(self.session)
|
||||
if self.is_runtime():
|
||||
self.show_mobility_players()
|
||||
self.app.after(0, self.app.joined_session_update)
|
||||
|
@ -334,23 +327,7 @@ class CoreClient:
|
|||
logging.debug("canvas metadata: %s", canvas_config)
|
||||
if canvas_config:
|
||||
canvas_config = json.loads(canvas_config)
|
||||
gridlines = canvas_config.get("gridlines", True)
|
||||
self.app.canvas.show_grid.set(gridlines)
|
||||
fit_image = canvas_config.get("fit_image", False)
|
||||
self.app.canvas.adjust_to_dim.set(fit_image)
|
||||
wallpaper_style = canvas_config.get("wallpaper-style", 1)
|
||||
self.app.canvas.scale_option.set(wallpaper_style)
|
||||
width = self.app.guiconfig.preferences.width
|
||||
height = self.app.guiconfig.preferences.height
|
||||
dimensions = canvas_config.get("dimensions", [width, height])
|
||||
self.app.canvas.redraw_canvas(dimensions)
|
||||
wallpaper = canvas_config.get("wallpaper")
|
||||
if wallpaper:
|
||||
wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper))
|
||||
self.app.canvas.set_wallpaper(wallpaper)
|
||||
else:
|
||||
self.app.canvas.redraw_canvas()
|
||||
self.app.canvas.set_wallpaper(None)
|
||||
self.app.manager.parse_metadata(canvas_config)
|
||||
|
||||
# load saved shapes
|
||||
shapes_config = config.get("shapes")
|
||||
|
@ -358,28 +335,7 @@ class CoreClient:
|
|||
shapes_config = json.loads(shapes_config)
|
||||
for shape_config in shapes_config:
|
||||
logging.debug("loading shape: %s", shape_config)
|
||||
shape_type = shape_config["type"]
|
||||
try:
|
||||
shape_type = ShapeType(shape_type)
|
||||
coords = shape_config["iconcoords"]
|
||||
data = AnnotationData(
|
||||
shape_config["label"],
|
||||
shape_config["fontfamily"],
|
||||
shape_config["fontsize"],
|
||||
shape_config["labelcolor"],
|
||||
shape_config["color"],
|
||||
shape_config["border"],
|
||||
shape_config["width"],
|
||||
shape_config["bold"],
|
||||
shape_config["italic"],
|
||||
shape_config["underline"],
|
||||
)
|
||||
shape = Shape(
|
||||
self.app, self.app.canvas, shape_type, *coords, data=data
|
||||
)
|
||||
self.app.canvas.shapes[shape.id] = shape
|
||||
except ValueError:
|
||||
logging.exception("unknown shape: %s", shape_type)
|
||||
Shape.from_metadata(self.app, shape_config)
|
||||
|
||||
# load edges config
|
||||
edges_config = config.get("edges")
|
||||
|
@ -392,6 +348,17 @@ class CoreClient:
|
|||
edge.color = edge_config["color"]
|
||||
edge.redraw()
|
||||
|
||||
# read hidden nodes
|
||||
hidden = config.get("hidden")
|
||||
if hidden:
|
||||
hidden = json.loads(hidden)
|
||||
for _id in hidden:
|
||||
canvas_node = self.canvas_nodes.get(_id)
|
||||
if canvas_node:
|
||||
canvas_node.hide()
|
||||
else:
|
||||
logging.warning("invalid node to hide: %s", _id)
|
||||
|
||||
def create_new_session(self) -> None:
|
||||
"""
|
||||
Create a new session
|
||||
|
@ -450,10 +417,11 @@ class CoreClient:
|
|||
if session_id:
|
||||
session_ids = set(x.id for x in sessions)
|
||||
if session_id not in session_ids:
|
||||
dialog = ErrorDialog(
|
||||
self.app, "Join Session Error", f"{session_id} does not exist"
|
||||
self.app.show_error(
|
||||
"Join Session Error",
|
||||
f"{session_id} does not exist",
|
||||
blocking=True,
|
||||
)
|
||||
dialog.show()
|
||||
self.app.close()
|
||||
else:
|
||||
self.join_session(session_id)
|
||||
|
@ -465,8 +433,7 @@ class CoreClient:
|
|||
dialog.show()
|
||||
except grpc.RpcError as e:
|
||||
logging.exception("core setup error")
|
||||
dialog = ErrorDialog(self.app, "Setup Error", e.details())
|
||||
dialog.show()
|
||||
self.app.show_grpc_exception("Setup Error", e, blocking=True)
|
||||
self.app.close()
|
||||
|
||||
def edit_node(self, core_node: Node) -> None:
|
||||
|
@ -483,7 +450,7 @@ class CoreClient:
|
|||
self.client.add_session_server(self.session.id, server.name, server.address)
|
||||
|
||||
def start_session(self) -> Tuple[bool, List[str]]:
|
||||
self.ifaces_manager.reset_mac()
|
||||
self.ifaces_manager.set_macs([x.link for x in self.links.values()])
|
||||
nodes = [x.to_proto() for x in self.session.nodes.values()]
|
||||
links = []
|
||||
asymmetric_links = []
|
||||
|
@ -548,7 +515,7 @@ class CoreClient:
|
|||
|
||||
def show_mobility_players(self) -> None:
|
||||
for node in self.session.nodes.values():
|
||||
if not NodeUtils.is_mobility(node):
|
||||
if not nutils.is_mobility(node):
|
||||
continue
|
||||
if node.mobility_config:
|
||||
mobility_player = MobilityPlayer(self.app, node)
|
||||
|
@ -557,26 +524,14 @@ class CoreClient:
|
|||
|
||||
def set_metadata(self) -> None:
|
||||
# create canvas data
|
||||
wallpaper_path = None
|
||||
if self.app.canvas.wallpaper_file:
|
||||
wallpaper = Path(self.app.canvas.wallpaper_file)
|
||||
if BACKGROUNDS_PATH == wallpaper.parent:
|
||||
wallpaper_path = wallpaper.name
|
||||
else:
|
||||
wallpaper_path = str(wallpaper)
|
||||
canvas_config = {
|
||||
"wallpaper": wallpaper_path,
|
||||
"wallpaper-style": self.app.canvas.scale_option.get(),
|
||||
"gridlines": self.app.canvas.show_grid.get(),
|
||||
"fit_image": self.app.canvas.adjust_to_dim.get(),
|
||||
"dimensions": self.app.canvas.current_dimensions,
|
||||
}
|
||||
canvas_config = self.app.manager.get_metadata()
|
||||
canvas_config = json.dumps(canvas_config)
|
||||
|
||||
# create shapes data
|
||||
shapes = []
|
||||
for shape in self.app.canvas.shapes.values():
|
||||
shapes.append(shape.metadata())
|
||||
for canvas in self.app.manager.all():
|
||||
for shape in canvas.shapes.values():
|
||||
shapes.append(shape.metadata())
|
||||
shapes = json.dumps(shapes)
|
||||
|
||||
# create edges config
|
||||
|
@ -588,8 +543,14 @@ class CoreClient:
|
|||
edges_config.append(edge_config)
|
||||
edges_config = json.dumps(edges_config)
|
||||
|
||||
# create hidden metadata
|
||||
hidden = [x.core_node.id for x in self.canvas_nodes.values() if x.hidden]
|
||||
hidden = json.dumps(hidden)
|
||||
|
||||
# save metadata
|
||||
metadata = dict(canvas=canvas_config, shapes=shapes, edges=edges_config)
|
||||
metadata = dict(
|
||||
canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden
|
||||
)
|
||||
response = self.client.set_session_metadata(self.session.id, metadata)
|
||||
logging.debug("set session metadata %s, result: %s", metadata, response)
|
||||
|
||||
|
@ -750,9 +711,11 @@ class CoreClient:
|
|||
self.session.id,
|
||||
config_proto.node_id,
|
||||
config_proto.service,
|
||||
startup=config_proto.startup,
|
||||
validate=config_proto.validate,
|
||||
shutdown=config_proto.shutdown,
|
||||
config_proto.files,
|
||||
config_proto.directories,
|
||||
config_proto.startup,
|
||||
config_proto.validate,
|
||||
config_proto.shutdown,
|
||||
)
|
||||
for config_proto in self.get_service_file_configs_proto():
|
||||
self.client.set_node_service_file(
|
||||
|
@ -816,7 +779,7 @@ class CoreClient:
|
|||
node_id = self.next_node_id()
|
||||
position = Position(x=x, y=y)
|
||||
image = None
|
||||
if NodeUtils.is_image_node(node_type):
|
||||
if nutils.has_image(node_type):
|
||||
image = "ubuntu:latest"
|
||||
emane = None
|
||||
if node_type == NodeType.EMANE:
|
||||
|
@ -841,9 +804,9 @@ class CoreClient:
|
|||
image=image,
|
||||
emane=emane,
|
||||
)
|
||||
if NodeUtils.is_custom(node_type, model):
|
||||
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
||||
node.services[:] = services
|
||||
if nutils.is_custom(node):
|
||||
services = nutils.get_custom_services(self.app.guiconfig, model)
|
||||
node.services = set(services)
|
||||
# assign default services to CORE node
|
||||
else:
|
||||
services = self.session.default_services.get(model)
|
||||
|
@ -876,60 +839,14 @@ class CoreClient:
|
|||
links.append(edge.link)
|
||||
self.ifaces_manager.removed(links)
|
||||
|
||||
def create_iface(self, canvas_node: CanvasNode) -> Interface:
|
||||
node = canvas_node.core_node
|
||||
ip4, ip6 = self.ifaces_manager.get_ips(node)
|
||||
ip4_mask = self.ifaces_manager.ip4_mask
|
||||
ip6_mask = self.ifaces_manager.ip6_mask
|
||||
iface_id = canvas_node.next_iface_id()
|
||||
name = f"eth{iface_id}"
|
||||
iface = Interface(
|
||||
id=iface_id,
|
||||
name=name,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
logging.info("create node(%s) interface(%s)", node.name, iface)
|
||||
return iface
|
||||
|
||||
def create_link(
|
||||
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||
) -> Link:
|
||||
"""
|
||||
Create core link for a pair of canvas nodes, with token referencing
|
||||
the canvas edge.
|
||||
"""
|
||||
src_node = canvas_src_node.core_node
|
||||
dst_node = canvas_dst_node.core_node
|
||||
self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
|
||||
src_iface = None
|
||||
if NodeUtils.is_container_node(src_node.type):
|
||||
src_iface = self.create_iface(canvas_src_node)
|
||||
dst_iface = None
|
||||
if NodeUtils.is_container_node(dst_node.type):
|
||||
dst_iface = self.create_iface(canvas_dst_node)
|
||||
link = Link(
|
||||
type=LinkType.WIRED,
|
||||
node1_id=src_node.id,
|
||||
node2_id=dst_node.id,
|
||||
iface1=src_iface,
|
||||
iface2=dst_iface,
|
||||
)
|
||||
logging.info("added link between %s and %s", src_node.name, dst_node.name)
|
||||
return link
|
||||
|
||||
def save_edge(
|
||||
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||
) -> None:
|
||||
def save_edge(self, edge: CanvasEdge) -> None:
|
||||
self.links[edge.token] = edge
|
||||
src_node = canvas_src_node.core_node
|
||||
dst_node = canvas_dst_node.core_node
|
||||
if NodeUtils.is_container_node(src_node.type):
|
||||
src_node = edge.src.core_node
|
||||
dst_node = edge.dst.core_node
|
||||
if nutils.is_container(src_node):
|
||||
src_iface_id = edge.link.iface1.id
|
||||
self.iface_to_edge[(src_node.id, src_iface_id)] = edge
|
||||
if NodeUtils.is_container_node(dst_node.type):
|
||||
if nutils.is_container(dst_node):
|
||||
dst_iface_id = edge.link.iface2.id
|
||||
self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge
|
||||
|
||||
|
@ -948,7 +865,7 @@ class CoreClient:
|
|||
def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not NodeUtils.is_mobility(node):
|
||||
if not nutils.is_mobility(node):
|
||||
continue
|
||||
if not node.mobility_config:
|
||||
continue
|
||||
|
@ -976,7 +893,7 @@ class CoreClient:
|
|||
def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not NodeUtils.is_container_node(node.type):
|
||||
if not nutils.is_container(node):
|
||||
continue
|
||||
if not node.service_configs:
|
||||
continue
|
||||
|
@ -996,7 +913,7 @@ class CoreClient:
|
|||
def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]:
|
||||
configs = []
|
||||
for node in self.session.nodes.values():
|
||||
if not NodeUtils.is_container_node(node.type):
|
||||
if not nutils.is_container(node):
|
||||
continue
|
||||
if not node.service_file_configs:
|
||||
continue
|
||||
|
@ -1013,7 +930,7 @@ class CoreClient:
|
|||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
||||
config_service_protos = []
|
||||
for node in self.session.nodes.values():
|
||||
if not NodeUtils.is_container_node(node.type):
|
||||
if not nutils.is_container(node):
|
||||
continue
|
||||
if not node.config_service_configs:
|
||||
continue
|
||||
|
|
BIN
daemon/core/gui/data/icons/shadow.png
Normal file
BIN
daemon/core/gui/data/icons/shadow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 B |
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -22,9 +22,9 @@ class SizeAndScaleDialog(Dialog):
|
|||
create an instance for size and scale object
|
||||
"""
|
||||
super().__init__(app, "Canvas Size and Scale")
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.manager: CanvasManager = self.app.manager
|
||||
self.section_font: font.Font = font.Font(weight=font.BOLD)
|
||||
width, height = self.canvas.current_dimensions
|
||||
width, height = self.manager.current_dimensions
|
||||
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
|
||||
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
|
||||
location = self.app.core.session.location
|
||||
|
@ -189,9 +189,7 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def click_apply(self) -> None:
|
||||
width, height = self.pixel_width.get(), self.pixel_height.get()
|
||||
self.canvas.redraw_canvas((width, height))
|
||||
if self.canvas.wallpaper:
|
||||
self.canvas.redraw_wallpaper()
|
||||
self.manager.redraw_canvases((width, height))
|
||||
location = self.app.core.session.location
|
||||
location.x = self.x.get()
|
||||
location.y = self.y.get()
|
||||
|
|
|
@ -6,10 +6,10 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import BACKGROUNDS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.images import Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import image_chooser
|
||||
|
||||
|
@ -23,7 +23,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
create an instance of CanvasWallpaper object
|
||||
"""
|
||||
super().__init__(app, "Canvas Background")
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.canvas: CanvasGraph = self.app.manager.current()
|
||||
self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
|
||||
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
|
||||
value=self.canvas.adjust_to_dim.get()
|
||||
|
@ -132,7 +132,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
self.draw_preview()
|
||||
|
||||
def draw_preview(self) -> None:
|
||||
image = Images.create(self.filename.get(), 250, 135)
|
||||
image = images.from_file(self.filename.get(), width=250, height=135)
|
||||
self.image_label.config(image=image)
|
||||
self.image_label.image = image
|
||||
|
||||
|
@ -161,7 +161,6 @@ class CanvasWallpaperDialog(Dialog):
|
|||
def click_apply(self) -> None:
|
||||
self.canvas.scale_option.set(self.scale_option.get())
|
||||
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
|
||||
self.canvas.show_grid.click_handler()
|
||||
filename = self.filename.get()
|
||||
if not filename:
|
||||
filename = None
|
||||
|
|
|
@ -6,10 +6,9 @@ from typing import TYPE_CHECKING, Optional, Set
|
|||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.gui import nodeutils
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import ICONS_PATH, CustomNode
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
|
||||
|
@ -190,7 +189,7 @@ class CustomNodesDialog(Dialog):
|
|||
def click_icon(self) -> None:
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
image = images.from_file(file_path, width=images.NODE_SIZE)
|
||||
self.image = image
|
||||
self.image_file = file_path
|
||||
self.image_button.config(image=self.image)
|
||||
|
@ -217,7 +216,7 @@ class CustomNodesDialog(Dialog):
|
|||
def click_create(self) -> None:
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_nodes:
|
||||
image_file = Path(self.image_file).stem
|
||||
image_file = str(Path(self.image_file).absolute())
|
||||
custom_node = CustomNode(name, image_file, list(self.services))
|
||||
node_draw = NodeDraw.from_custom(custom_node)
|
||||
logging.info(
|
||||
|
@ -237,7 +236,7 @@ class CustomNodesDialog(Dialog):
|
|||
self.selected = name
|
||||
node_draw = self.app.core.custom_nodes.pop(previous_name)
|
||||
node_draw.model = name
|
||||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image_file = str(Path(self.image_file).absolute())
|
||||
node_draw.image = self.image
|
||||
node_draw.services = self.services
|
||||
logging.debug(
|
||||
|
|
|
@ -2,7 +2,8 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui import images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import DIALOG_PAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -25,7 +26,7 @@ class Dialog(tk.Toplevel):
|
|||
self.modal: bool = modal
|
||||
self.title(title)
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
image = Images.get(ImageEnum.CORE, 16)
|
||||
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
|
||||
self.tk.call("wm", "iconphoto", self._w, image)
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
|
|
|
@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Dict, List, Optional
|
|||
import grpc
|
||||
|
||||
from core.api.grpc.wrappers import ConfigOption, Node
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
|
||||
|
@ -143,7 +144,7 @@ class EmaneConfigDialog(Dialog):
|
|||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(
|
||||
self.top,
|
||||
image=image,
|
||||
|
@ -181,7 +182,7 @@ class EmaneConfigDialog(Dialog):
|
|||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
self.emane_model_button = ttk.Button(
|
||||
frame,
|
||||
text=f"{self.emane_model.get()} options",
|
||||
|
@ -192,7 +193,7 @@ class EmaneConfigDialog(Dialog):
|
|||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(
|
||||
frame,
|
||||
text="EMANE options",
|
||||
|
|
|
@ -2,8 +2,9 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import PADY
|
||||
from core.gui.widgets import CodeText
|
||||
|
||||
|
@ -12,9 +13,11 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class ErrorDialog(Dialog):
|
||||
def __init__(self, app: "Application", title: str, details: str) -> None:
|
||||
super().__init__(app, "CORE Exception")
|
||||
self.title: str = title
|
||||
def __init__(
|
||||
self, app: "Application", title: str, message: str, details: str
|
||||
) -> None:
|
||||
super().__init__(app, title)
|
||||
self.message: str = message
|
||||
self.details: str = details
|
||||
self.error_message: Optional[CodeText] = None
|
||||
self.draw()
|
||||
|
@ -22,15 +25,15 @@ class ErrorDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(1, weight=1)
|
||||
image = Images.get(ImageEnum.ERROR, 24)
|
||||
image = images.from_enum(ImageEnum.ERROR, width=images.ERROR_SIZE)
|
||||
label = ttk.Label(
|
||||
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
|
||||
self.top, text=self.message, image=image, compound=tk.LEFT, anchor=tk.CENTER
|
||||
)
|
||||
label.image = image
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
label.grid(sticky=tk.W, pady=PADY)
|
||||
self.error_message = CodeText(self.top)
|
||||
self.error_message.text.insert("1.0", self.details)
|
||||
self.error_message.text.config(state=tk.DISABLED)
|
||||
self.error_message.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.error_message.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(self.top, text="Close", command=lambda: self.destroy())
|
||||
button.grid(sticky=tk.EW)
|
||||
|
|
|
@ -105,9 +105,13 @@ class FindDialog(Dialog):
|
|||
self.tree.selection_set(results[0])
|
||||
|
||||
def close_dialog(self) -> None:
|
||||
self.app.canvas.delete("find")
|
||||
self.clear_find()
|
||||
self.destroy()
|
||||
|
||||
def clear_find(self):
|
||||
for canvas in self.app.manager.all():
|
||||
canvas.delete("find")
|
||||
|
||||
def click_select(self, _event: tk.Event = None) -> None:
|
||||
"""
|
||||
find the node that matches search criteria, circle around that node
|
||||
|
@ -116,13 +120,13 @@ class FindDialog(Dialog):
|
|||
"""
|
||||
item = self.tree.selection()
|
||||
if item:
|
||||
self.app.canvas.delete("find")
|
||||
self.clear_find()
|
||||
node_id = int(self.tree.item(item, "text"))
|
||||
canvas_node = self.app.core.get_canvas_node(node_id)
|
||||
|
||||
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
|
||||
self.app.manager.select(canvas_node.canvas.id)
|
||||
x0, y0, x1, y1 = canvas_node.canvas.bbox(canvas_node.id)
|
||||
dist = 5 * self.app.guiconfig.scale
|
||||
self.app.canvas.create_oval(
|
||||
canvas_node.canvas.create_oval(
|
||||
x0 - dist,
|
||||
y0 - dist,
|
||||
x1 + dist,
|
||||
|
@ -132,9 +136,9 @@ class FindDialog(Dialog):
|
|||
width=3.0 * self.app.guiconfig.scale,
|
||||
)
|
||||
|
||||
_x, _y, _, _ = self.app.canvas.bbox(canvas_node.id)
|
||||
oid = self.app.canvas.find_withtag("rectangle")
|
||||
x0, y0, x1, y1 = self.app.canvas.bbox(oid[0])
|
||||
_x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id)
|
||||
oid = canvas_node.canvas.find_withtag("rectangle")
|
||||
x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0])
|
||||
logging.debug("Dist to most left: %s", abs(x0 - _x))
|
||||
logging.debug("White canvas width: %s", abs(x0 - x1))
|
||||
|
||||
|
@ -150,5 +154,5 @@ class FindDialog(Dialog):
|
|||
xscroll_fraction = xscroll_fraction - 0.05
|
||||
if yscroll_fraction > 0.05:
|
||||
yscroll_fraction = yscroll_fraction - 0.05
|
||||
self.app.canvas.xview_moveto(xscroll_fraction)
|
||||
self.app.canvas.yview_moveto(yscroll_fraction)
|
||||
canvas_node.canvas.xview_moveto(xscroll_fraction)
|
||||
canvas_node.canvas.yview_moveto(yscroll_fraction)
|
||||
|
|
|
@ -70,10 +70,10 @@ class LinkConfigurationDialog(Dialog):
|
|||
|
||||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
src_label = self.app.canvas.nodes[self.edge.src].core_node.name
|
||||
src_label = self.edge.src.core_node.name
|
||||
if self.edge.link.iface1:
|
||||
src_label += f":{self.edge.link.iface1.name}"
|
||||
dst_label = self.app.canvas.nodes[self.edge.dst].core_node.name
|
||||
dst_label = self.edge.dst.core_node.name
|
||||
if self.edge.link.iface2:
|
||||
dst_label += f":{self.edge.link.iface2.name}"
|
||||
label = ttk.Label(
|
||||
|
@ -293,7 +293,7 @@ class LinkConfigurationDialog(Dialog):
|
|||
|
||||
# update edge label
|
||||
self.edge.redraw()
|
||||
self.edge.check_options()
|
||||
self.edge.check_visibility()
|
||||
self.destroy()
|
||||
|
||||
def change_symmetry(self) -> None:
|
||||
|
@ -316,10 +316,8 @@ class LinkConfigurationDialog(Dialog):
|
|||
"""
|
||||
populate link config to the table
|
||||
"""
|
||||
width = self.app.canvas.itemcget(self.edge.id, "width")
|
||||
self.width.set(width)
|
||||
color = self.app.canvas.itemcget(self.edge.id, "fill")
|
||||
self.color.set(color)
|
||||
self.width.set(self.edge.width)
|
||||
self.color.set(self.edge.color)
|
||||
link = self.edge.link
|
||||
if link.options:
|
||||
self.bandwidth.set(str(link.options.bandwidth))
|
||||
|
|
|
@ -84,17 +84,17 @@ class MobilityPlayerDialog(Dialog):
|
|||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.START, ICON_SIZE)
|
||||
image = self.app.get_enum_icon(ImageEnum.START, width=ICON_SIZE)
|
||||
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
||||
self.play_button.image = image
|
||||
self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE)
|
||||
image = self.app.get_enum_icon(ImageEnum.PAUSE, width=ICON_SIZE)
|
||||
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
||||
self.pause_button.image = image
|
||||
self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE)
|
||||
image = self.app.get_enum_icon(ImageEnum.STOP, width=ICON_SIZE)
|
||||
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
||||
self.stop_button.image = image
|
||||
self.stop_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
|
|
|
@ -8,12 +8,12 @@ import netaddr
|
|||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.wrappers import Interface, Node
|
||||
from core.gui import nodeutils, validation
|
||||
from core.gui import images
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui import validation
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.dialogs.emaneconfig import EmaneModelDialog
|
||||
from core.gui.images import Images
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import ListboxScroll, image_chooser
|
||||
|
||||
|
@ -225,27 +225,27 @@ class NodeConfigDialog(Dialog):
|
|||
row += 1
|
||||
|
||||
# node type field
|
||||
if NodeUtils.is_model_node(self.node.type):
|
||||
if nutils.is_model(self.node):
|
||||
label = ttk.Label(frame, text="Type")
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.type,
|
||||
values=list(NodeUtils.NODE_MODELS),
|
||||
values=list(nutils.NODE_MODELS),
|
||||
state=combo_state,
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
# container image field
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
if nutils.has_image(self.node.type):
|
||||
label = ttk.Label(frame, text="Image")
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
if NodeUtils.is_container_node(self.node.type):
|
||||
if nutils.is_container(self.node):
|
||||
# server
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
@ -259,7 +259,7 @@ class NodeConfigDialog(Dialog):
|
|||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
if NodeUtils.is_rj45_node(self.node.type):
|
||||
if nutils.is_rj45(self.node):
|
||||
response = self.app.core.client.get_ifaces()
|
||||
logging.debug("host machine available interfaces: %s", response)
|
||||
ifaces = ListboxScroll(frame)
|
||||
|
@ -371,7 +371,7 @@ class NodeConfigDialog(Dialog):
|
|||
def click_icon(self) -> None:
|
||||
file_path = image_chooser(self, ICONS_PATH)
|
||||
if file_path:
|
||||
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
self.image = images.from_file(file_path, width=images.NODE_SIZE)
|
||||
self.image_button.config(image=self.image)
|
||||
self.image_file = file_path
|
||||
|
||||
|
@ -380,10 +380,10 @@ class NodeConfigDialog(Dialog):
|
|||
|
||||
# update core node
|
||||
self.node.name = self.name.get()
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
if nutils.has_image(self.node.type):
|
||||
self.node.image = self.container_image.get()
|
||||
server = self.server.get()
|
||||
if NodeUtils.is_container_node(self.node.type):
|
||||
if nutils.is_container(self.node):
|
||||
if server == DEFAULT_SERVER:
|
||||
self.node.server = None
|
||||
else:
|
||||
|
|
|
@ -134,7 +134,8 @@ class PreferencesDialog(Dialog):
|
|||
|
||||
# scale toolbar and canvas items
|
||||
self.app.toolbar.scale()
|
||||
self.app.canvas.scale_graph()
|
||||
for canvas in self.app.manager.all():
|
||||
canvas.scale_graph()
|
||||
|
||||
def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
|
||||
scale_value = self.gui_scale.get()
|
||||
|
|
|
@ -2,8 +2,8 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
|
@ -26,7 +26,7 @@ class RunToolDialog(Dialog):
|
|||
store all CORE nodes (nodes that execute commands) from all existing nodes
|
||||
"""
|
||||
for node in self.app.core.session.nodes.values():
|
||||
if NodeUtils.is_container_node(node.type):
|
||||
if nutils.is_container(node):
|
||||
self.executable_nodes[node.name] = node.id
|
||||
|
||||
def draw(self) -> None:
|
||||
|
|
|
@ -8,9 +8,10 @@ import grpc
|
|||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.wrappers import Node, NodeServiceData, ServiceValidationMode
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
|
||||
|
@ -47,11 +48,11 @@ class ServiceConfigDialog(Dialog):
|
|||
self.directory_entry: Optional[ttk.Entry] = None
|
||||
self.default_directories: List[str] = []
|
||||
self.temp_directories: List[str] = []
|
||||
self.documentnew_img: PhotoImage = self.app.get_icon(
|
||||
ImageEnum.DOCUMENTNEW, ICON_SIZE
|
||||
self.documentnew_img: PhotoImage = self.app.get_enum_icon(
|
||||
ImageEnum.DOCUMENTNEW, width=ICON_SIZE
|
||||
)
|
||||
self.editdelete_img: PhotoImage = self.app.get_icon(
|
||||
ImageEnum.EDITDELETE, ICON_SIZE
|
||||
self.editdelete_img: PhotoImage = self.app.get_enum_icon(
|
||||
ImageEnum.EDITDELETE, width=ICON_SIZE
|
||||
)
|
||||
self.notebook: Optional[ttk.Notebook] = None
|
||||
self.metadata_entry: Optional[ttk.Entry] = None
|
||||
|
@ -179,7 +180,7 @@ class ServiceConfigDialog(Dialog):
|
|||
button.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, state=tk.DISABLED)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=2)
|
||||
|
@ -194,11 +195,11 @@ class ServiceConfigDialog(Dialog):
|
|||
value=2,
|
||||
)
|
||||
button.grid(row=0, column=0, sticky=tk.EW)
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1)
|
||||
image = Images.get(ImageEnum.DOCUMENTSAVE, 16)
|
||||
image = images.from_enum(ImageEnum.DOCUMENTSAVE, width=images.BUTTON_SIZE)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=2)
|
||||
|
|
|
@ -6,8 +6,9 @@ from typing import TYPE_CHECKING, List, Optional
|
|||
import grpc
|
||||
|
||||
from core.api.grpc.wrappers import SessionState, SessionSummary
|
||||
from core.gui import images
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.task import ProgressTask
|
||||
from core.gui.themes import PADX, PADY
|
||||
|
||||
|
@ -22,7 +23,6 @@ class SessionsDialog(Dialog):
|
|||
self.selected_session: Optional[int] = None
|
||||
self.selected_id: Optional[int] = None
|
||||
self.tree: Optional[ttk.Treeview] = None
|
||||
self.sessions: List[SessionSummary] = self.get_sessions()
|
||||
self.connect_button: Optional[ttk.Button] = None
|
||||
self.delete_button: Optional[ttk.Button] = None
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
|
@ -32,7 +32,8 @@ class SessionsDialog(Dialog):
|
|||
try:
|
||||
response = self.app.core.client.get_sessions()
|
||||
logging.info("sessions: %s", response)
|
||||
return [SessionSummary.from_proto(x) for x in response.sessions]
|
||||
sessions = sorted(response.sessions, key=lambda x: x.id)
|
||||
return [SessionSummary.from_proto(x) for x in sessions]
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Sessions Error", e)
|
||||
self.destroy()
|
||||
|
@ -79,15 +80,7 @@ class SessionsDialog(Dialog):
|
|||
self.tree.heading("state", text="State")
|
||||
self.tree.column("nodes", stretch=tk.YES, anchor="center")
|
||||
self.tree.heading("nodes", text="Node Count")
|
||||
|
||||
for index, session in enumerate(self.sessions):
|
||||
state_name = SessionState(session.state).name
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
text=str(session.id),
|
||||
values=(session.id, state_name, session.nodes),
|
||||
)
|
||||
self.draw_sessions()
|
||||
self.tree.bind("<Double-1>", self.double_click_join)
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
|
@ -99,20 +92,31 @@ class SessionsDialog(Dialog):
|
|||
xscrollbar.grid(row=1, sticky=tk.EW)
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
def draw_sessions(self) -> None:
|
||||
self.tree.delete(*self.tree.get_children())
|
||||
for index, session in enumerate(self.get_sessions()):
|
||||
state_name = SessionState(session.state).name
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
text=str(session.id),
|
||||
values=(session.id, state_name, session.nodes),
|
||||
)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
frame.grid(sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
||||
image = images.from_enum(ImageEnum.DOCUMENTNEW, width=images.BUTTON_SIZE)
|
||||
b = ttk.Button(
|
||||
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
|
||||
self.connect_button = ttk.Button(
|
||||
frame,
|
||||
image=image,
|
||||
|
@ -124,7 +128,7 @@ class SessionsDialog(Dialog):
|
|||
self.connect_button.image = image
|
||||
self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.DELETE, 16)
|
||||
image = images.from_enum(ImageEnum.DELETE, width=images.BUTTON_SIZE)
|
||||
self.delete_button = ttk.Button(
|
||||
frame,
|
||||
image=image,
|
||||
|
@ -136,7 +140,7 @@ class SessionsDialog(Dialog):
|
|||
self.delete_button.image = image
|
||||
self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.CANCEL, 16)
|
||||
image = images.from_enum(ImageEnum.CANCEL, width=images.BUTTON_SIZE)
|
||||
if self.is_start_app:
|
||||
b = ttk.Button(
|
||||
frame,
|
||||
|
@ -196,12 +200,21 @@ class SessionsDialog(Dialog):
|
|||
def click_delete(self) -> None:
|
||||
if not self.selected_session:
|
||||
return
|
||||
logging.debug("delete session: %s", self.selected_session)
|
||||
logging.info("click delete session: %s", self.selected_session)
|
||||
self.tree.delete(self.selected_id)
|
||||
self.app.core.delete_session(self.selected_session)
|
||||
if self.selected_session == self.app.core.session.id:
|
||||
self.click_new()
|
||||
self.destroy()
|
||||
session_id = None
|
||||
if self.app.core.session:
|
||||
session_id = self.app.core.session.id
|
||||
if self.selected_session == session_id:
|
||||
self.app.core.session = None
|
||||
sessions = self.get_sessions()
|
||||
if not sessions:
|
||||
self.app.core.create_new_session()
|
||||
self.draw_sessions()
|
||||
else:
|
||||
session_id = sessions[0].id
|
||||
self.app.core.join_session(session_id)
|
||||
self.click_select()
|
||||
|
||||
def click_exit(self) -> None:
|
||||
|
|
|
@ -27,7 +27,7 @@ class ShapeDialog(Dialog):
|
|||
else:
|
||||
title = "Add Text"
|
||||
super().__init__(app, title)
|
||||
self.canvas: "CanvasGraph" = app.canvas
|
||||
self.canvas: "CanvasGraph" = app.manager.current()
|
||||
self.fill: Optional[ttk.Label] = None
|
||||
self.border: Optional[ttk.Label] = None
|
||||
self.shape: "Shape" = shape
|
||||
|
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Optional
|
|||
|
||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -17,16 +17,16 @@ if TYPE_CHECKING:
|
|||
class ThroughputDialog(Dialog):
|
||||
def __init__(self, app: "Application") -> None:
|
||||
super().__init__(app, "Throughput Config")
|
||||
self.canvas: CanvasGraph = app.canvas
|
||||
self.manager: CanvasManager = app.manager
|
||||
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
|
||||
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
|
||||
self.transmission: tk.IntVar = tk.IntVar(value=1)
|
||||
self.reception: tk.IntVar = tk.IntVar(value=1)
|
||||
self.threshold: tk.DoubleVar = tk.DoubleVar(
|
||||
value=self.canvas.throughput_threshold
|
||||
value=self.manager.throughput_threshold
|
||||
)
|
||||
self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
|
||||
self.color: str = self.canvas.throughput_color
|
||||
self.width: tk.IntVar = tk.IntVar(value=self.manager.throughput_width)
|
||||
self.color: str = self.manager.throughput_color
|
||||
self.color_button: Optional[tk.Button] = None
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.draw()
|
||||
|
@ -106,7 +106,7 @@ class ThroughputDialog(Dialog):
|
|||
self.color_button.config(bg=self.color, text=self.color, bd=0)
|
||||
|
||||
def click_save(self) -> None:
|
||||
self.canvas.throughput_threshold = self.threshold.get()
|
||||
self.canvas.throughput_width = self.width.get()
|
||||
self.canvas.throughput_color = self.color
|
||||
self.manager.throughput_threshold = self.threshold.get()
|
||||
self.manager.throughput_width = self.width.get()
|
||||
self.manager.throughput_color = self.color
|
||||
self.destroy()
|
||||
|
|
|
@ -21,7 +21,7 @@ RANGE_WIDTH: int = 3
|
|||
class WlanConfigDialog(Dialog):
|
||||
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
|
||||
self.canvas: "CanvasGraph" = app.canvas
|
||||
self.canvas: "CanvasGraph" = app.manager.current()
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
|
|
|
@ -79,15 +79,13 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
|
|||
|
||||
def draw(self) -> None:
|
||||
link = self.edge.link
|
||||
src_canvas_node = self.app.canvas.nodes[self.edge.src]
|
||||
src_node = src_canvas_node.core_node
|
||||
dst_canvas_node = self.app.canvas.nodes[self.edge.dst]
|
||||
dst_node = dst_canvas_node.core_node
|
||||
src_node = self.edge.src.core_node
|
||||
dst_node = self.edge.dst.core_node
|
||||
|
||||
# find interface for each node connected to network
|
||||
net_id = link.network_id
|
||||
iface1 = get_iface(src_canvas_node, net_id)
|
||||
iface2 = get_iface(dst_canvas_node, net_id)
|
||||
iface1 = get_iface(self.edge.src, net_id)
|
||||
iface2 = get_iface(self.edge.dst, net_id)
|
||||
|
||||
frame = DetailsFrame(self)
|
||||
frame.grid(sticky=tk.EW)
|
||||
|
|
|
@ -2,8 +2,8 @@ import tkinter as tk
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.api.grpc.wrappers import NodeType
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.frames.base import DetailsFrame, InfoFrameBase
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -20,21 +20,21 @@ class NodeInfoFrame(InfoFrameBase):
|
|||
node = self.canvas_node.core_node
|
||||
frame = DetailsFrame(self)
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.add_detail("ID", node.id)
|
||||
frame.add_detail("ID", str(node.id))
|
||||
frame.add_detail("Name", node.name)
|
||||
if NodeUtils.is_model_node(node.type):
|
||||
if nutils.is_model(node):
|
||||
frame.add_detail("Type", node.model)
|
||||
if NodeUtils.is_container_node(node.type):
|
||||
if nutils.is_container(node):
|
||||
for index, service in enumerate(sorted(node.services)):
|
||||
if index == 0:
|
||||
frame.add_detail("Services", service)
|
||||
else:
|
||||
frame.add_detail("", service)
|
||||
if node.type == NodeType.EMANE:
|
||||
emane = node.emane.split("_")[1:]
|
||||
emane = "".join(node.emane.split("_")[1:])
|
||||
frame.add_detail("EMANE", emane)
|
||||
if NodeUtils.is_image_node(node.type):
|
||||
if nutils.has_image(node.type):
|
||||
frame.add_detail("Image", node.image)
|
||||
if NodeUtils.is_container_node(node.type):
|
||||
if nutils.is_container(node):
|
||||
server = node.server if node.server else "localhost"
|
||||
frame.add_detail("Server", server)
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import functools
|
||||
import logging
|
||||
import math
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
from core.api.grpc.wrappers import Interface, Link
|
||||
from core.gui import themes
|
||||
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
|
||||
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
|
||||
from core.gui.graph import tags
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.utils import bandwidth_text, delay_jitter_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.graph.node import CanvasNode, ShadowNode
|
||||
|
||||
TEXT_DISTANCE: int = 60
|
||||
EDGE_WIDTH: int = 3
|
||||
|
@ -24,13 +27,40 @@ ARC_DISTANCE: int = 50
|
|||
|
||||
|
||||
def create_wireless_token(src: int, dst: int, network: int) -> str:
|
||||
return f"{src}-{dst}-{network}"
|
||||
if src < dst:
|
||||
node1, node2 = src, dst
|
||||
else:
|
||||
node1, node2 = dst, src
|
||||
return f"{node1}-{node2}-{network}"
|
||||
|
||||
|
||||
def create_edge_token(link: Link) -> str:
|
||||
iface1_id = link.iface1.id if link.iface1 else 0
|
||||
iface2_id = link.iface2.id if link.iface2 else 0
|
||||
return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}"
|
||||
if link.node1_id < link.node2_id:
|
||||
node1 = link.node1_id
|
||||
node1_iface = iface1_id
|
||||
node2 = link.node2_id
|
||||
node2_iface = iface2_id
|
||||
else:
|
||||
node1 = link.node2_id
|
||||
node1_iface = iface2_id
|
||||
node2 = link.node1_id
|
||||
node2_iface = iface1_id
|
||||
return f"{node1}-{node1_iface}-{node2}-{node2_iface}"
|
||||
|
||||
|
||||
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:
|
||||
|
@ -65,21 +95,35 @@ def arc_edges(edges) -> None:
|
|||
class Edge:
|
||||
tag: str = tags.EDGE
|
||||
|
||||
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
|
||||
self.canvas = canvas
|
||||
def __init__(
|
||||
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
|
||||
) -> None:
|
||||
self.app: "Application" = app
|
||||
self.manager: CanvasManager = app.manager
|
||||
self.id: Optional[int] = None
|
||||
self.src: int = src
|
||||
self.dst: int = dst
|
||||
self.id2: Optional[int] = None
|
||||
self.src: "CanvasNode" = src
|
||||
self.src_shadow: Optional[ShadowNode] = None
|
||||
self.dst: Optional["CanvasNode"] = dst
|
||||
self.dst_shadow: Optional[ShadowNode] = None
|
||||
self.link: Optional[Link] = None
|
||||
self.arc: int = 0
|
||||
self.token: Optional[str] = None
|
||||
self.src_label: Optional[int] = None
|
||||
self.src_label2: Optional[int] = None
|
||||
self.middle_label: Optional[int] = None
|
||||
self.middle_label2: Optional[int] = None
|
||||
self.dst_label: Optional[int] = None
|
||||
self.dst_label2: Optional[int] = None
|
||||
self.color: str = EDGE_COLOR
|
||||
self.width: int = EDGE_WIDTH
|
||||
self.linked_wireless: bool = False
|
||||
self.hidden: bool = False
|
||||
if self.dst:
|
||||
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
|
||||
|
||||
def scaled_width(self) -> float:
|
||||
return self.width * self.canvas.app.app_scale
|
||||
return self.width * self.app.app_scale
|
||||
|
||||
def _get_arcpoint(
|
||||
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
|
||||
|
@ -110,11 +154,53 @@ class Edge:
|
|||
arc_y = (perp_m * arc_x) + b
|
||||
return arc_x, arc_y
|
||||
|
||||
def draw(
|
||||
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str
|
||||
) -> None:
|
||||
def arc_common_edges(self) -> None:
|
||||
common_edges = list(self.src.edges & self.dst.edges)
|
||||
common_edges += list(self.src.wireless_edges & self.dst.wireless_edges)
|
||||
arc_edges(common_edges)
|
||||
|
||||
def has_shadows(self) -> bool:
|
||||
# still drawing
|
||||
if not self.dst:
|
||||
return False
|
||||
return self.src.canvas != self.dst.canvas
|
||||
|
||||
def draw(self, state: str) -> None:
|
||||
if not self.has_shadows():
|
||||
dst = self.dst if self.dst else self.src
|
||||
self.id = self.draw_edge(self.src.canvas, self.src, dst, state)
|
||||
elif self.linked_wireless:
|
||||
if self.src.is_wireless():
|
||||
self.src_shadow = self.dst.canvas.get_shadow(self.src)
|
||||
self.id2 = self.draw_edge(
|
||||
self.dst.canvas, self.src_shadow, self.dst, state
|
||||
)
|
||||
if self.dst.is_wireless():
|
||||
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
|
||||
self.id = self.draw_edge(
|
||||
self.src.canvas, self.src, self.dst_shadow, state
|
||||
)
|
||||
else:
|
||||
# draw shadow nodes and 2 lines
|
||||
self.src_shadow = self.dst.canvas.get_shadow(self.src)
|
||||
self.dst_shadow = self.src.canvas.get_shadow(self.dst)
|
||||
self.id = self.draw_edge(self.src.canvas, self.src, self.dst_shadow, state)
|
||||
self.id2 = self.draw_edge(self.dst.canvas, self.src_shadow, self.dst, state)
|
||||
self.src.canvas.organize()
|
||||
if self.has_shadows():
|
||||
self.dst.canvas.organize()
|
||||
|
||||
def draw_edge(
|
||||
self,
|
||||
canvas: "CanvasGraph",
|
||||
src: Union["CanvasNode", "ShadowNode"],
|
||||
dst: Union["CanvasNode", "ShadowNode"],
|
||||
state: str,
|
||||
) -> int:
|
||||
src_pos = src.position()
|
||||
dst_pos = dst.position()
|
||||
arc_pos = self._get_arcpoint(src_pos, dst_pos)
|
||||
self.id = self.canvas.create_line(
|
||||
return canvas.create_line(
|
||||
*src_pos,
|
||||
*arc_pos,
|
||||
*dst_pos,
|
||||
|
@ -126,112 +212,268 @@ class Edge:
|
|||
)
|
||||
|
||||
def redraw(self) -> None:
|
||||
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
|
||||
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
|
||||
src_pos = src_x, src_y
|
||||
self.move_src(src_pos)
|
||||
|
||||
def middle_label_pos(self) -> Tuple[float, float]:
|
||||
_, _, x, y, _, _ = self.canvas.coords(self.id)
|
||||
return x, y
|
||||
self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
|
||||
self.move_src()
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfig(
|
||||
self.id2, width=self.scaled_width(), fill=self.color
|
||||
)
|
||||
self.move_dst()
|
||||
|
||||
def middle_label_text(self, text: str) -> None:
|
||||
if self.middle_label is None:
|
||||
x, y = self.middle_label_pos()
|
||||
self.middle_label = self.canvas.create_text(
|
||||
_, _, x, y, _, _ = self.src.canvas.coords(self.id)
|
||||
self.middle_label = self.src.canvas.create_text(
|
||||
x,
|
||||
y,
|
||||
font=self.canvas.app.edge_font,
|
||||
font=self.app.edge_font,
|
||||
text=text,
|
||||
tags=tags.LINK_LABEL,
|
||||
justify=tk.CENTER,
|
||||
state=self.canvas.show_link_labels.state(),
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
if self.id2:
|
||||
_, _, x, y, _, _ = self.dst.canvas.coords(self.id2)
|
||||
self.middle_label2 = self.dst.canvas.create_text(
|
||||
x,
|
||||
y,
|
||||
font=self.app.edge_font,
|
||||
text=text,
|
||||
tags=tags.LINK_LABEL,
|
||||
justify=tk.CENTER,
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
else:
|
||||
self.canvas.itemconfig(self.middle_label, text=text)
|
||||
self.src.canvas.itemconfig(self.middle_label, text=text)
|
||||
if self.middle_label2:
|
||||
self.dst.canvas.itemconfig(self.middle_label2, text=text)
|
||||
|
||||
def clear_middle_label(self) -> None:
|
||||
self.canvas.delete(self.middle_label)
|
||||
self.src.canvas.delete(self.middle_label)
|
||||
self.middle_label = None
|
||||
|
||||
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
|
||||
v_x, v_y = dst_x - src_x, dst_y - src_y
|
||||
v_len = math.sqrt(v_x ** 2 + v_y ** 2)
|
||||
if v_len == 0:
|
||||
u_x, u_y = 0.0, 0.0
|
||||
else:
|
||||
u_x, u_y = v_x / v_len, v_y / v_len
|
||||
offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y
|
||||
return (
|
||||
(src_x + offset_x, src_y + offset_y),
|
||||
(dst_x - offset_x, dst_y - offset_y),
|
||||
)
|
||||
if self.middle_label2:
|
||||
self.dst.canvas.delete(self.middle_label2)
|
||||
self.middle_label2 = None
|
||||
|
||||
def src_label_text(self, text: str) -> None:
|
||||
if self.src_label is None:
|
||||
src_pos, _ = self.node_label_positions()
|
||||
self.src_label = self.canvas.create_text(
|
||||
*src_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.canvas.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.canvas.show_link_labels.state(),
|
||||
)
|
||||
if self.src_label is None and self.src_label2 is None:
|
||||
if self.id:
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
|
||||
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
self.src_label = self.src.canvas.create_text(
|
||||
*src_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
if self.id2:
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
|
||||
src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
self.src_label2 = self.dst.canvas.create_text(
|
||||
*src_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
else:
|
||||
self.canvas.itemconfig(self.src_label, text=text)
|
||||
if self.src_label:
|
||||
self.src.canvas.itemconfig(self.src_label, text=text)
|
||||
if self.src_label2:
|
||||
self.dst.canvas.itemconfig(self.src_label2, text=text)
|
||||
|
||||
def dst_label_text(self, text: str) -> None:
|
||||
if self.dst_label is None:
|
||||
_, dst_pos = self.node_label_positions()
|
||||
self.dst_label = self.canvas.create_text(
|
||||
*dst_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.canvas.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.canvas.show_link_labels.state(),
|
||||
)
|
||||
if self.dst_label is None and self.dst_label2 is None:
|
||||
if self.id:
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
|
||||
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
self.dst_label = self.src.canvas.create_text(
|
||||
*dst_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
if self.id2:
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
|
||||
_, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
self.dst_label2 = self.dst.canvas.create_text(
|
||||
*dst_pos,
|
||||
text=text,
|
||||
justify=tk.CENTER,
|
||||
font=self.app.edge_font,
|
||||
tags=tags.LINK_LABEL,
|
||||
state=self.manager.show_link_labels.state(),
|
||||
)
|
||||
else:
|
||||
self.canvas.itemconfig(self.dst_label, text=text)
|
||||
if self.dst_label:
|
||||
self.src.canvas.itemconfig(self.dst_label, text=text)
|
||||
if self.dst_label2:
|
||||
self.dst.canvas.itemconfig(self.dst_label2, text=text)
|
||||
|
||||
def move_node(self, node_id: int, pos: Tuple[float, float]) -> None:
|
||||
if self.src == node_id:
|
||||
self.move_src(pos)
|
||||
else:
|
||||
self.move_dst(pos)
|
||||
|
||||
def move_dst(self, dst_pos: Tuple[float, float]) -> None:
|
||||
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
|
||||
def drawing(self, pos: Tuple[float, float]) -> None:
|
||||
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
|
||||
src_pos = src_x, src_y
|
||||
self.moved(src_pos, dst_pos)
|
||||
self.moved(src_pos, pos)
|
||||
|
||||
def move_src(self, src_pos: Tuple[float, float]) -> None:
|
||||
_, _, _, _, dst_x, dst_y = self.canvas.coords(self.id)
|
||||
def move_node(self, node: "CanvasNode") -> None:
|
||||
if self.src == node:
|
||||
self.move_src()
|
||||
else:
|
||||
self.move_dst()
|
||||
|
||||
def move_shadow(self, node: "ShadowNode") -> None:
|
||||
if self.src_shadow == node:
|
||||
self.move_src_shadow()
|
||||
elif self.dst_shadow == node:
|
||||
self.move_dst_shadow()
|
||||
|
||||
def move_src_shadow(self) -> None:
|
||||
if not self.id2:
|
||||
return
|
||||
_, _, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
|
||||
dst_pos = dst_x, dst_y
|
||||
self.moved(src_pos, dst_pos)
|
||||
self.moved2(self.src_shadow.position(), dst_pos)
|
||||
|
||||
def move_dst_shadow(self) -> None:
|
||||
if not self.id:
|
||||
return
|
||||
src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id)
|
||||
src_pos = src_x, src_y
|
||||
self.moved(src_pos, self.dst_shadow.position())
|
||||
|
||||
def move_dst(self) -> None:
|
||||
if self.dst.is_wireless() and self.has_shadows():
|
||||
return
|
||||
dst_pos = self.dst.position()
|
||||
if self.id2:
|
||||
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id2)
|
||||
src_pos = src_x, src_y
|
||||
self.moved2(src_pos, dst_pos)
|
||||
elif self.id:
|
||||
src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id)
|
||||
src_pos = src_x, src_y
|
||||
self.moved(src_pos, dst_pos)
|
||||
|
||||
def move_src(self) -> None:
|
||||
if not self.id:
|
||||
return
|
||||
_, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
|
||||
dst_pos = dst_x, dst_y
|
||||
self.moved(self.src.position(), dst_pos)
|
||||
|
||||
def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
|
||||
arc_pos = self._get_arcpoint(src_pos, dst_pos)
|
||||
self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
|
||||
self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos)
|
||||
if self.middle_label:
|
||||
self.canvas.coords(self.middle_label, *arc_pos)
|
||||
src_pos, dst_pos = self.node_label_positions()
|
||||
self.src.canvas.coords(self.middle_label, *arc_pos)
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id)
|
||||
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
if self.src_label:
|
||||
self.canvas.coords(self.src_label, *src_pos)
|
||||
self.src.canvas.coords(self.src_label, *src_pos)
|
||||
if self.dst_label:
|
||||
self.canvas.coords(self.dst_label, *dst_pos)
|
||||
self.src.canvas.coords(self.dst_label, *dst_pos)
|
||||
|
||||
def moved2(
|
||||
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]
|
||||
) -> None:
|
||||
arc_pos = self._get_arcpoint(src_pos, dst_pos)
|
||||
self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos)
|
||||
if self.middle_label2:
|
||||
self.dst.canvas.coords(self.middle_label2, *arc_pos)
|
||||
src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2)
|
||||
src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y)
|
||||
if self.src_label2:
|
||||
self.dst.canvas.coords(self.src_label2, *src_pos)
|
||||
if self.dst_label2:
|
||||
self.dst.canvas.coords(self.dst_label2, *dst_pos)
|
||||
|
||||
def delete(self) -> None:
|
||||
logging.debug("deleting canvas edge, id: %s", self.id)
|
||||
self.canvas.delete(self.id)
|
||||
self.canvas.delete(self.src_label)
|
||||
self.canvas.delete(self.dst_label)
|
||||
self.src.canvas.delete(self.id)
|
||||
self.src.canvas.delete(self.src_label)
|
||||
self.src.canvas.delete(self.dst_label)
|
||||
if self.dst:
|
||||
self.dst.canvas.delete(self.id2)
|
||||
self.dst.canvas.delete(self.src_label2)
|
||||
self.dst.canvas.delete(self.dst_label2)
|
||||
if self.src_shadow and self.src_shadow.should_delete():
|
||||
self.src_shadow.delete()
|
||||
self.src_shadow = None
|
||||
if self.dst_shadow and self.dst_shadow.should_delete():
|
||||
self.dst_shadow.delete()
|
||||
self.dst_shadow = None
|
||||
self.clear_middle_label()
|
||||
self.id = None
|
||||
self.id2 = None
|
||||
self.src_label = None
|
||||
self.src_label2 = None
|
||||
self.dst_label = None
|
||||
self.dst_label2 = None
|
||||
|
||||
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)
|
||||
state = self.manager.show_link_labels.state()
|
||||
self.set_labels(state)
|
||||
|
||||
def set_labels(self, state: str) -> None:
|
||||
self.src.canvas.itemconfigure(self.src_label, state=state)
|
||||
self.src.canvas.itemconfigure(self.dst_label, state=state)
|
||||
self.src.canvas.itemconfigure(self.middle_label, state=state)
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfigure(self.id2, state=state)
|
||||
self.dst.canvas.itemconfigure(self.src_label2, state=state)
|
||||
self.dst.canvas.itemconfigure(self.dst_label2, state=state)
|
||||
self.dst.canvas.itemconfigure(self.middle_label2, state=state)
|
||||
|
||||
def other_node(self, node: "CanvasNode") -> "CanvasNode":
|
||||
if self.src == node:
|
||||
return self.dst
|
||||
elif self.dst == node:
|
||||
return self.src
|
||||
else:
|
||||
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
|
||||
|
||||
def other_iface(self, node: "CanvasNode") -> Optional[Interface]:
|
||||
if self.src == node:
|
||||
return self.link.iface2 if self.link else None
|
||||
elif self.dst == node:
|
||||
return self.link.iface1 if self.link else None
|
||||
else:
|
||||
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
|
||||
|
||||
def iface(self, node: "CanvasNode") -> Optional[Interface]:
|
||||
if self.src == node:
|
||||
return self.link.iface1 if self.link else None
|
||||
elif self.dst == node:
|
||||
return self.link.iface2 if self.link else None
|
||||
else:
|
||||
raise ValueError(f"node({node.core_node.name}) does not belong to edge")
|
||||
|
||||
|
||||
class CanvasWirelessEdge(Edge):
|
||||
|
@ -239,35 +481,43 @@ class CanvasWirelessEdge(Edge):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
canvas: "CanvasGraph",
|
||||
src: int,
|
||||
dst: int,
|
||||
app: "Application",
|
||||
src: "CanvasNode",
|
||||
dst: "CanvasNode",
|
||||
network_id: int,
|
||||
token: str,
|
||||
src_pos: Tuple[float, float],
|
||||
dst_pos: Tuple[float, float],
|
||||
link: Link,
|
||||
) -> None:
|
||||
logging.debug("drawing wireless link from node %s to node %s", src, dst)
|
||||
super().__init__(canvas, src, dst)
|
||||
super().__init__(app, src, dst)
|
||||
self.src.wireless_edges.add(self)
|
||||
self.dst.wireless_edges.add(self)
|
||||
self.network_id: int = network_id
|
||||
self.link: Link = link
|
||||
self.token: str = token
|
||||
self.width: float = WIRELESS_WIDTH
|
||||
color = link.color if link.color else WIRELESS_COLOR
|
||||
self.color: str = color
|
||||
self.draw(src_pos, dst_pos, self.canvas.show_wireless.state())
|
||||
state = self.manager.show_wireless.state()
|
||||
self.draw(state)
|
||||
if link.label:
|
||||
self.middle_label_text(link.label)
|
||||
if self.src.hidden or self.dst.hidden:
|
||||
self.hide()
|
||||
self.set_binding()
|
||||
|
||||
def set_binding(self) -> None:
|
||||
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
if self.id2 is not None:
|
||||
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
|
||||
|
||||
def show_info(self, _event: tk.Event) -> None:
|
||||
self.canvas.app.display_info(
|
||||
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
|
||||
)
|
||||
self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.src.wireless_edges.discard(self)
|
||||
self.dst.wireless_edges.remove(self)
|
||||
super().delete()
|
||||
|
||||
|
||||
class CanvasEdge(Edge):
|
||||
|
@ -276,47 +526,39 @@ class CanvasEdge(Edge):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
canvas: "CanvasGraph",
|
||||
src: int,
|
||||
src_pos: Tuple[float, float],
|
||||
dst_pos: Tuple[float, float],
|
||||
self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None
|
||||
) -> None:
|
||||
"""
|
||||
Create an instance of canvas edge object
|
||||
"""
|
||||
super().__init__(canvas, src)
|
||||
super().__init__(app, src, dst)
|
||||
self.text_src: Optional[int] = None
|
||||
self.text_dst: Optional[int] = None
|
||||
self.link: Optional[Link] = None
|
||||
self.linked_wireless: bool = False
|
||||
self.asymmetric_link: Optional[Link] = None
|
||||
self.throughput: Optional[float] = None
|
||||
self.draw(src_pos, dst_pos, tk.NORMAL)
|
||||
self.set_binding()
|
||||
self.context: tk.Menu = tk.Menu(self.canvas)
|
||||
self.create_context()
|
||||
self.draw(tk.NORMAL)
|
||||
|
||||
def is_customized(self) -> bool:
|
||||
return self.width != EDGE_WIDTH or self.color != EDGE_COLOR
|
||||
|
||||
def create_context(self) -> None:
|
||||
themes.style_menu(self.context)
|
||||
self.context.add_command(label="Configure", command=self.click_configure)
|
||||
self.context.add_command(label="Delete", command=self.click_delete)
|
||||
|
||||
def set_binding(self) -> None:
|
||||
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
|
||||
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
def set_bindings(self) -> None:
|
||||
if self.id:
|
||||
show_context = functools.partial(self.show_context, self.src.canvas)
|
||||
self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context)
|
||||
self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
|
||||
if self.id2:
|
||||
show_context = functools.partial(self.show_context, self.dst.canvas)
|
||||
self.dst.canvas.tag_bind(self.id2, "<ButtonRelease-3>", show_context)
|
||||
self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info)
|
||||
|
||||
def iface_label(self, iface: Interface) -> str:
|
||||
label = ""
|
||||
if iface.name and self.canvas.show_iface_names.get():
|
||||
if iface.name and self.manager.show_iface_names.get():
|
||||
label = f"{iface.name}"
|
||||
if iface.ip4 and self.canvas.show_ip4s.get():
|
||||
if iface.ip4 and self.manager.show_ip4s.get():
|
||||
label = f"{label}\n" if label else ""
|
||||
label += f"{iface.ip4}/{iface.ip4_mask}"
|
||||
if iface.ip6 and self.canvas.show_ip6s.get():
|
||||
if iface.ip6 and self.manager.show_ip6s.get():
|
||||
label = f"{label}\n" if label else ""
|
||||
label += f"{iface.ip6}/{iface.ip6_mask}"
|
||||
return label
|
||||
|
@ -341,82 +583,126 @@ class CanvasEdge(Edge):
|
|||
super().redraw()
|
||||
self.draw_labels()
|
||||
|
||||
def check_options(self) -> None:
|
||||
if not self.link.options:
|
||||
return
|
||||
if self.link.options.loss == EDGE_LOSS:
|
||||
def show(self) -> None:
|
||||
super().show()
|
||||
self.check_visibility()
|
||||
|
||||
def check_visibility(self) -> None:
|
||||
state = tk.NORMAL
|
||||
hide_links = self.manager.show_links.state() == tk.HIDDEN
|
||||
if self.linked_wireless or hide_links:
|
||||
state = tk.HIDDEN
|
||||
self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id)
|
||||
else:
|
||||
state = tk.NORMAL
|
||||
self.canvas.dtag(self.id, tags.LOSS_EDGES)
|
||||
if self.canvas.show_loss_links.state() == tk.HIDDEN:
|
||||
self.canvas.itemconfigure(self.id, state=state)
|
||||
elif self.link.options:
|
||||
hide_loss = self.manager.show_loss_links.state() == tk.HIDDEN
|
||||
should_hide = self.link.options.loss >= EDGE_LOSS
|
||||
if hide_loss and should_hide:
|
||||
state = tk.HIDDEN
|
||||
if self.id:
|
||||
self.src.canvas.itemconfigure(self.id, state=state)
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfigure(self.id2, state=state)
|
||||
|
||||
def set_throughput(self, throughput: float) -> None:
|
||||
throughput = 0.001 * throughput
|
||||
text = f"{throughput:.3f} kbps"
|
||||
self.middle_label_text(text)
|
||||
if throughput > self.canvas.throughput_threshold:
|
||||
color = self.canvas.throughput_color
|
||||
width = self.canvas.throughput_width
|
||||
if throughput > self.manager.throughput_threshold:
|
||||
color = self.manager.throughput_color
|
||||
width = self.manager.throughput_width
|
||||
else:
|
||||
color = self.color
|
||||
width = self.scaled_width()
|
||||
self.canvas.itemconfig(self.id, fill=color, width=width)
|
||||
self.src.canvas.itemconfig(self.id, fill=color, width=width)
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfig(self.id2, fill=color, width=width)
|
||||
|
||||
def clear_throughput(self) -> None:
|
||||
self.clear_middle_label()
|
||||
if not self.linked_wireless:
|
||||
self.draw_link_options()
|
||||
|
||||
def complete(self, dst: int, linked_wireless: bool) -> None:
|
||||
def complete(self, dst: "CanvasNode", link: Link = None) -> None:
|
||||
logging.debug(
|
||||
"completing wired link from node(%s) to node(%s)",
|
||||
self.src.core_node.name,
|
||||
dst.core_node.name,
|
||||
)
|
||||
self.dst = dst
|
||||
self.linked_wireless = linked_wireless
|
||||
dst_pos = self.canvas.coords(self.dst)
|
||||
self.move_dst(dst_pos)
|
||||
self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless()
|
||||
self.set_bindings()
|
||||
self.check_wireless()
|
||||
logging.debug("draw wired link from node %s to node %s", self.src, dst)
|
||||
if link is None:
|
||||
link = self.app.core.ifaces_manager.create_link(self)
|
||||
if link.iface1:
|
||||
iface1 = link.iface1
|
||||
self.src.ifaces[iface1.id] = iface1
|
||||
if link.iface2:
|
||||
iface2 = link.iface2
|
||||
self.dst.ifaces[iface2.id] = iface2
|
||||
self.token = create_edge_token(link)
|
||||
self.link = link
|
||||
self.src.edges.add(self)
|
||||
self.dst.edges.add(self)
|
||||
if not self.linked_wireless:
|
||||
self.arc_common_edges()
|
||||
self.draw_labels()
|
||||
self.check_visibility()
|
||||
self.app.core.save_edge(self)
|
||||
self.src.canvas.organize()
|
||||
if self.has_shadows():
|
||||
self.dst.canvas.organize()
|
||||
self.manager.edges[self.token] = self
|
||||
|
||||
def check_wireless(self) -> None:
|
||||
if self.linked_wireless:
|
||||
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
|
||||
self.canvas.dtag(self.id, tags.EDGE)
|
||||
self._check_antenna()
|
||||
|
||||
def _check_antenna(self) -> None:
|
||||
src_node = self.canvas.nodes[self.src]
|
||||
dst_node = self.canvas.nodes[self.dst]
|
||||
src_node_type = src_node.core_node.type
|
||||
dst_node_type = dst_node.core_node.type
|
||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||
if is_src_wireless or is_dst_wireless:
|
||||
if is_src_wireless and not is_dst_wireless:
|
||||
dst_node.add_antenna()
|
||||
elif not is_src_wireless and is_dst_wireless:
|
||||
src_node.add_antenna()
|
||||
else:
|
||||
src_node.add_antenna()
|
||||
if not self.linked_wireless:
|
||||
return
|
||||
if self.id:
|
||||
self.src.canvas.itemconfig(self.id, state=tk.HIDDEN)
|
||||
self.src.canvas.dtag(self.id, tags.EDGE)
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN)
|
||||
self.dst.canvas.dtag(self.id2, tags.EDGE)
|
||||
# add antenna to node
|
||||
if self.src.is_wireless() and not self.dst.is_wireless():
|
||||
self.dst.add_antenna()
|
||||
elif not self.src.is_wireless() and self.dst.is_wireless():
|
||||
self.src.add_antenna()
|
||||
else:
|
||||
self.src.add_antenna()
|
||||
|
||||
def reset(self) -> None:
|
||||
self.canvas.delete(self.middle_label)
|
||||
self.middle_label = None
|
||||
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
|
||||
if self.middle_label:
|
||||
self.src.canvas.delete(self.middle_label)
|
||||
self.middle_label = None
|
||||
if self.middle_label2:
|
||||
self.dst.canvas.delete(self.middle_label2)
|
||||
self.middle_label2 = None
|
||||
if self.id:
|
||||
self.src.canvas.itemconfig(
|
||||
self.id, fill=self.color, width=self.scaled_width()
|
||||
)
|
||||
if self.id2:
|
||||
self.dst.canvas.itemconfig(
|
||||
self.id2, fill=self.color, width=self.scaled_width()
|
||||
)
|
||||
|
||||
def show_info(self, _event: tk.Event) -> None:
|
||||
self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self)
|
||||
self.app.display_info(EdgeInfoFrame, app=self.app, edge=self)
|
||||
|
||||
def show_context(self, event: tk.Event) -> None:
|
||||
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
|
||||
self.context.entryconfigure(1, state=state)
|
||||
self.context.tk_popup(event.x_root, event.y_root)
|
||||
def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None:
|
||||
context: tk.Menu = tk.Menu(canvas)
|
||||
themes.style_menu(context)
|
||||
context.add_command(label="Configure", command=self.click_configure)
|
||||
context.add_command(label="Delete", command=self.click_delete)
|
||||
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
|
||||
context.entryconfigure(1, state=state)
|
||||
context.tk_popup(event.x_root, event.y_root)
|
||||
|
||||
def click_delete(self) -> None:
|
||||
self.canvas.delete_edge(self)
|
||||
self.delete()
|
||||
|
||||
def click_configure(self) -> None:
|
||||
dialog = LinkConfigurationDialog(self.canvas.app, self)
|
||||
dialog = LinkConfigurationDialog(self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def draw_link_options(self):
|
||||
|
@ -455,3 +741,21 @@ class CanvasEdge(Edge):
|
|||
lines.append(dup_line)
|
||||
label = "\n".join(lines)
|
||||
self.middle_label_text(label)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.src.edges.discard(self)
|
||||
if self.dst:
|
||||
self.dst.edges.discard(self)
|
||||
if self.link.iface1:
|
||||
del self.src.ifaces[self.link.iface1.id]
|
||||
if self.link.iface2:
|
||||
del self.dst.ifaces[self.link.iface2.id]
|
||||
if self.src.is_wireless():
|
||||
self.dst.delete_antenna()
|
||||
if self.dst.is_wireless():
|
||||
self.src.delete_antenna()
|
||||
self.app.core.deleted_canvas_edges([self])
|
||||
super().delete()
|
||||
if self.dst:
|
||||
self.arc_common_edges()
|
||||
self.manager.edges.pop(self.token, None)
|
||||
|
|
|
@ -1,79 +1,57 @@
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from copy import deepcopy
|
||||
from tkinter import BooleanVar
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from PIL import Image
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.wrappers import (
|
||||
Interface,
|
||||
Link,
|
||||
LinkType,
|
||||
Node,
|
||||
Session,
|
||||
ThroughputsEvent,
|
||||
)
|
||||
from core.api.grpc.wrappers import Interface, Link
|
||||
from core.gui import appconfig
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.dialogs.shapemod import ShapeDialog
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import (
|
||||
EDGE_WIDTH,
|
||||
CanvasEdge,
|
||||
CanvasWirelessEdge,
|
||||
Edge,
|
||||
arc_edges,
|
||||
create_edge_token,
|
||||
create_wireless_token,
|
||||
)
|
||||
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge
|
||||
from core.gui.graph.enums import GraphMode, ScaleOption
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.graph.node import CanvasNode, ShadowNode
|
||||
from core.gui.graph.shape import Shape
|
||||
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||
from core.gui.images import ImageEnum, TypeToImage
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
||||
ZOOM_IN = 1.1
|
||||
ZOOM_OUT = 0.9
|
||||
ICON_SIZE = 48
|
||||
MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT}
|
||||
MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT}
|
||||
|
||||
|
||||
class ShowVar(BooleanVar):
|
||||
def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None:
|
||||
super().__init__(value=value)
|
||||
self.canvas = canvas
|
||||
self.tag = tag
|
||||
|
||||
def state(self) -> str:
|
||||
return tk.NORMAL if self.get() else tk.HIDDEN
|
||||
|
||||
def click_handler(self) -> None:
|
||||
self.canvas.itemconfigure(self.tag, state=self.state())
|
||||
ZOOM_IN: float = 1.1
|
||||
ZOOM_OUT: float = 0.9
|
||||
MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT}
|
||||
MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT}
|
||||
BACKGROUND_COLOR: str = "#cccccc"
|
||||
|
||||
|
||||
class CanvasGraph(tk.Canvas):
|
||||
def __init__(
|
||||
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
|
||||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
manager: "CanvasManager",
|
||||
core: "CoreClient",
|
||||
_id: int,
|
||||
dimensions: Tuple[int, int],
|
||||
) -> None:
|
||||
super().__init__(master, highlightthickness=0, background="#cccccc")
|
||||
super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR)
|
||||
self.id: int = _id
|
||||
self.app: "Application" = app
|
||||
self.manager: "CanvasManager" = manager
|
||||
self.core: "CoreClient" = core
|
||||
self.mode: GraphMode = GraphMode.SELECT
|
||||
self.annotation_type: Optional[ShapeType] = None
|
||||
self.selection: Dict[int, int] = {}
|
||||
self.select_box: Optional[Shape] = None
|
||||
self.selected: Optional[int] = None
|
||||
self.node_draw: Optional[NodeDraw] = None
|
||||
self.nodes: Dict[int, CanvasNode] = {}
|
||||
self.edges: Dict[str, CanvasEdge] = {}
|
||||
self.shadow_nodes: Dict[int, ShadowNode] = {}
|
||||
self.shapes: Dict[int, Shape] = {}
|
||||
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
|
||||
self.shadow_core_nodes: Dict[int, ShadowNode] = {}
|
||||
|
||||
# map wireless/EMANE node to the set of MDRs connected to that node
|
||||
self.wireless_network: Dict[int, Set[int]] = {}
|
||||
|
@ -81,10 +59,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.drawing_edge: Optional[CanvasEdge] = None
|
||||
self.rect: Optional[int] = None
|
||||
self.shape_drawing: bool = False
|
||||
width = self.app.guiconfig.preferences.width
|
||||
height = self.app.guiconfig.preferences.height
|
||||
self.default_dimensions: Tuple[int, int] = (width, height)
|
||||
self.current_dimensions: Tuple[int, int] = self.default_dimensions
|
||||
self.current_dimensions: Tuple[int, int] = dimensions
|
||||
self.ratio: float = 1.0
|
||||
self.offset: Tuple[int, int] = (0, 0)
|
||||
self.cursor: Tuple[int, int] = (0, 0)
|
||||
|
@ -98,23 +73,6 @@ class CanvasGraph(tk.Canvas):
|
|||
self.scale_option: tk.IntVar = tk.IntVar(value=1)
|
||||
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)
|
||||
|
||||
# throughput related
|
||||
self.throughput_threshold: float = 250.0
|
||||
self.throughput_width: int = 10
|
||||
self.throughput_color: str = "#FF0000"
|
||||
|
||||
# drawing related
|
||||
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
|
||||
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
|
||||
self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
|
||||
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
|
||||
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
|
||||
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
|
||||
self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True)
|
||||
self.show_iface_names: BooleanVar = BooleanVar(value=False)
|
||||
self.show_ip4s: BooleanVar = BooleanVar(value=True)
|
||||
self.show_ip6s: BooleanVar = BooleanVar(value=True)
|
||||
|
||||
# bindings
|
||||
self.setup_bindings()
|
||||
|
||||
|
@ -126,7 +84,7 @@ class CanvasGraph(tk.Canvas):
|
|||
if self.rect is not None:
|
||||
self.delete(self.rect)
|
||||
if not dimensions:
|
||||
dimensions = self.default_dimensions
|
||||
dimensions = self.manager.default_dimensions
|
||||
self.current_dimensions = dimensions
|
||||
self.rect = self.create_rectangle(
|
||||
0,
|
||||
|
@ -139,34 +97,6 @@ class CanvasGraph(tk.Canvas):
|
|||
)
|
||||
self.configure(scrollregion=self.bbox(tk.ALL))
|
||||
|
||||
def reset_and_redraw(self, session: Session) -> None:
|
||||
# reset view options to default state
|
||||
self.show_node_labels.set(True)
|
||||
self.show_link_labels.set(True)
|
||||
self.show_grid.set(True)
|
||||
self.show_annotations.set(True)
|
||||
self.show_iface_names.set(False)
|
||||
self.show_ip4s.set(True)
|
||||
self.show_ip6s.set(True)
|
||||
self.show_loss_links.set(True)
|
||||
|
||||
# delete any existing drawn items
|
||||
for tag in tags.RESET_TAGS:
|
||||
self.delete(tag)
|
||||
|
||||
# set the private variables to default value
|
||||
self.mode = GraphMode.SELECT
|
||||
self.annotation_type = None
|
||||
self.node_draw = None
|
||||
self.selected = None
|
||||
self.nodes.clear()
|
||||
self.edges.clear()
|
||||
self.shapes.clear()
|
||||
self.wireless_edges.clear()
|
||||
self.wireless_network.clear()
|
||||
self.drawing_edge = None
|
||||
self.draw_session(session)
|
||||
|
||||
def setup_bindings(self) -> None:
|
||||
"""
|
||||
Bind any mouse events or hot keys to the matching action
|
||||
|
@ -183,6 +113,12 @@ class CanvasGraph(tk.Canvas):
|
|||
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
|
||||
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
|
||||
|
||||
def get_shadow(self, node: CanvasNode) -> ShadowNode:
|
||||
shadow_node = self.shadow_core_nodes.get(node.core_node.id)
|
||||
if not shadow_node:
|
||||
shadow_node = ShadowNode(self.app, self, node)
|
||||
return shadow_node
|
||||
|
||||
def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
|
||||
actual_x = (x - self.offset[0]) / self.ratio
|
||||
actual_y = (y - self.offset[1]) / self.ratio
|
||||
|
@ -204,16 +140,6 @@ class CanvasGraph(tk.Canvas):
|
|||
valid_bottomright = self.inside_canvas(x2, y2)
|
||||
return valid_topleft and valid_bottomright
|
||||
|
||||
def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None:
|
||||
for iface_throughput in throughputs_event.iface_throughputs:
|
||||
node_id = iface_throughput.node_id
|
||||
iface_id = iface_throughput.iface_id
|
||||
throughput = iface_throughput.throughput
|
||||
iface_to_edge_id = (node_id, iface_id)
|
||||
edge = self.core.iface_to_edge.get(iface_to_edge_id)
|
||||
if edge:
|
||||
edge.set_throughput(throughput)
|
||||
|
||||
def draw_grid(self) -> None:
|
||||
"""
|
||||
Create grid.
|
||||
|
@ -228,123 +154,6 @@ class CanvasGraph(tk.Canvas):
|
|||
self.tag_lower(tags.GRIDLINE)
|
||||
self.tag_lower(self.rect)
|
||||
|
||||
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
|
||||
token = create_edge_token(link)
|
||||
if token in self.edges and link.options.unidirectional:
|
||||
edge = self.edges[token]
|
||||
edge.asymmetric_link = link
|
||||
elif token not in self.edges:
|
||||
node1 = src.core_node
|
||||
node2 = dst.core_node
|
||||
src_pos = (node1.position.x, node1.position.y)
|
||||
dst_pos = (node2.position.x, node2.position.y)
|
||||
edge = CanvasEdge(self, src.id, src_pos, dst_pos)
|
||||
self.complete_edge(src, dst, edge, link)
|
||||
|
||||
def delete_wired_edge(self, link: Link) -> None:
|
||||
token = create_edge_token(link)
|
||||
edge = self.edges.get(token)
|
||||
if edge:
|
||||
self.delete_edge(edge)
|
||||
|
||||
def update_wired_edge(self, link: Link) -> None:
|
||||
token = create_edge_token(link)
|
||||
edge = self.edges.get(token)
|
||||
if edge:
|
||||
edge.link.options = deepcopy(link.options)
|
||||
edge.draw_link_options()
|
||||
edge.check_options()
|
||||
|
||||
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
|
||||
network_id = link.network_id if link.network_id else None
|
||||
token = create_wireless_token(src.id, dst.id, network_id)
|
||||
if token in self.wireless_edges:
|
||||
logging.warning("ignoring link that already exists: %s", link)
|
||||
return
|
||||
src_pos = self.coords(src.id)
|
||||
dst_pos = self.coords(dst.id)
|
||||
edge = CanvasWirelessEdge(
|
||||
self, src.id, dst.id, network_id, token, src_pos, dst_pos, link
|
||||
)
|
||||
self.wireless_edges[token] = edge
|
||||
src.wireless_edges.add(edge)
|
||||
dst.wireless_edges.add(edge)
|
||||
self.tag_raise(src.id)
|
||||
self.tag_raise(dst.id)
|
||||
self.arc_common_edges(edge)
|
||||
|
||||
def delete_wireless_edge(
|
||||
self, src: CanvasNode, dst: CanvasNode, link: Link
|
||||
) -> None:
|
||||
network_id = link.network_id if link.network_id else None
|
||||
token = create_wireless_token(src.id, dst.id, network_id)
|
||||
if token not in self.wireless_edges:
|
||||
return
|
||||
edge = self.wireless_edges.pop(token)
|
||||
edge.delete()
|
||||
src.wireless_edges.remove(edge)
|
||||
dst.wireless_edges.remove(edge)
|
||||
self.arc_common_edges(edge)
|
||||
|
||||
def update_wireless_edge(
|
||||
self, src: CanvasNode, dst: CanvasNode, link: Link
|
||||
) -> None:
|
||||
if not link.label:
|
||||
return
|
||||
network_id = link.network_id if link.network_id else None
|
||||
token = create_wireless_token(src.id, dst.id, network_id)
|
||||
if token not in self.wireless_edges:
|
||||
self.add_wireless_edge(src, dst, link)
|
||||
else:
|
||||
edge = self.wireless_edges[token]
|
||||
edge.middle_label_text(link.label)
|
||||
|
||||
def add_core_node(self, core_node: Node) -> None:
|
||||
logging.debug("adding node: %s", core_node)
|
||||
# if the gui can't find node's image, default to the "edit-node" image
|
||||
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
|
||||
if not image:
|
||||
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
|
||||
x = core_node.position.x
|
||||
y = core_node.position.y
|
||||
node = CanvasNode(self.app, x, y, core_node, image)
|
||||
self.nodes[node.id] = node
|
||||
self.core.set_canvas_node(core_node, node)
|
||||
|
||||
def draw_session(self, session: Session) -> None:
|
||||
"""
|
||||
Draw existing session.
|
||||
"""
|
||||
# draw existing nodes
|
||||
for core_node in session.nodes.values():
|
||||
logging.debug("drawing node: %s", core_node)
|
||||
# peer to peer node is not drawn on the GUI
|
||||
if NodeUtils.is_ignore_node(core_node.type):
|
||||
continue
|
||||
self.add_core_node(core_node)
|
||||
# draw existing links
|
||||
for link in session.links:
|
||||
logging.debug("drawing link: %s", link)
|
||||
canvas_node1 = self.core.get_canvas_node(link.node1_id)
|
||||
canvas_node2 = self.core.get_canvas_node(link.node2_id)
|
||||
if link.type == LinkType.WIRELESS:
|
||||
self.add_wireless_edge(canvas_node1, canvas_node2, link)
|
||||
else:
|
||||
self.add_wired_edge(canvas_node1, canvas_node2, link)
|
||||
|
||||
def stopped_session(self) -> None:
|
||||
# clear wireless edges
|
||||
for edge in self.wireless_edges.values():
|
||||
edge.delete()
|
||||
src_node = self.nodes[edge.src]
|
||||
src_node.wireless_edges.remove(edge)
|
||||
dst_node = self.nodes[edge.dst]
|
||||
dst_node.wireless_edges.remove(edge)
|
||||
self.wireless_edges.clear()
|
||||
|
||||
# clear throughputs
|
||||
self.clear_throughputs()
|
||||
|
||||
def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
|
||||
"""
|
||||
Convert window coordinate to canvas coordinate
|
||||
|
@ -363,14 +172,12 @@ class CanvasGraph(tk.Canvas):
|
|||
for _id in overlapping:
|
||||
if self.drawing_edge and self.drawing_edge.id == _id:
|
||||
continue
|
||||
|
||||
if _id in self.nodes:
|
||||
elif _id in self.nodes:
|
||||
selected = _id
|
||||
break
|
||||
|
||||
if _id in self.shapes:
|
||||
elif _id in self.shapes:
|
||||
selected = _id
|
||||
elif _id in self.shadow_nodes:
|
||||
selected = _id
|
||||
|
||||
return selected
|
||||
|
||||
def click_release(self, event: tk.Event) -> None:
|
||||
|
@ -381,13 +188,13 @@ class CanvasGraph(tk.Canvas):
|
|||
x, y = self.canvas_xy(event)
|
||||
if not self.inside_canvas(x, y):
|
||||
return
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
if self.manager.mode == GraphMode.ANNOTATION:
|
||||
self.focus_set()
|
||||
if self.shape_drawing:
|
||||
shape = self.shapes[self.selected]
|
||||
shape.shape_complete(x, y)
|
||||
self.shape_drawing = False
|
||||
elif self.mode == GraphMode.SELECT:
|
||||
elif self.manager.mode == GraphMode.SELECT:
|
||||
self.focus_set()
|
||||
if self.select_box:
|
||||
x0, y0, x1, y1 = self.coords(self.select_box.id)
|
||||
|
@ -403,61 +210,36 @@ class CanvasGraph(tk.Canvas):
|
|||
else:
|
||||
self.focus_set()
|
||||
self.selected = self.get_selected(event)
|
||||
logging.debug(f"click release selected({self.selected}) mode({self.mode})")
|
||||
if self.mode == GraphMode.EDGE:
|
||||
logging.debug(
|
||||
"click release selected(%s) mode(%s)", self.selected, self.manager.mode
|
||||
)
|
||||
if self.manager.mode == GraphMode.EDGE:
|
||||
self.handle_edge_release(event)
|
||||
elif self.mode == GraphMode.NODE:
|
||||
elif self.manager.mode == GraphMode.NODE:
|
||||
self.add_node(x, y)
|
||||
elif self.mode == GraphMode.PICKNODE:
|
||||
self.mode = GraphMode.NODE
|
||||
elif self.manager.mode == GraphMode.PICKNODE:
|
||||
self.manager.mode = GraphMode.NODE
|
||||
self.selected = None
|
||||
|
||||
def handle_edge_release(self, _event: tk.Event) -> None:
|
||||
# not drawing edge return
|
||||
if not self.drawing_edge:
|
||||
return
|
||||
edge = self.drawing_edge
|
||||
self.drawing_edge = None
|
||||
|
||||
# not drawing edge return
|
||||
if edge is None:
|
||||
return
|
||||
|
||||
# edge dst must be a node
|
||||
logging.debug("current selected: %s", self.selected)
|
||||
src_node = self.nodes.get(edge.src)
|
||||
dst_node = self.nodes.get(self.selected)
|
||||
if not dst_node or not src_node:
|
||||
if not dst_node:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# edge dst is same as src, delete edge
|
||||
if edge.src == self.selected:
|
||||
# check if node can be linked
|
||||
if not edge.src.is_linkable(dst_node):
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# rj45 nodes can only support one link
|
||||
if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges:
|
||||
edge.delete()
|
||||
return
|
||||
if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# only 1 link between bridge based nodes
|
||||
is_src_bridge = NodeUtils.is_bridge_node(src_node.core_node)
|
||||
is_dst_bridge = NodeUtils.is_bridge_node(dst_node.core_node)
|
||||
common_links = src_node.edges & dst_node.edges
|
||||
if all([is_src_bridge, is_dst_bridge, common_links]):
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# finalize edge creation
|
||||
self.complete_edge(src_node, dst_node, edge)
|
||||
|
||||
def arc_common_edges(self, edge: Edge) -> None:
|
||||
src_node = self.nodes[edge.src]
|
||||
dst_node = self.nodes[edge.dst]
|
||||
common_edges = list(src_node.edges & dst_node.edges)
|
||||
common_edges += list(src_node.wireless_edges & dst_node.wireless_edges)
|
||||
arc_edges(common_edges)
|
||||
edge.drawing(dst_node.position())
|
||||
edge.complete(dst_node)
|
||||
|
||||
def select_object(self, object_id: int, choose_multiple: bool = False) -> None:
|
||||
"""
|
||||
|
@ -504,28 +286,16 @@ class CanvasGraph(tk.Canvas):
|
|||
# delete node and related edges
|
||||
if object_id in self.nodes:
|
||||
canvas_node = self.nodes.pop(object_id)
|
||||
canvas_node.delete()
|
||||
nodes.append(canvas_node)
|
||||
is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type)
|
||||
# delete related edges
|
||||
for edge in canvas_node.edges:
|
||||
while canvas_node.edges:
|
||||
edge = canvas_node.edges.pop()
|
||||
if edge in edges:
|
||||
continue
|
||||
edges.add(edge)
|
||||
del self.edges[edge.token]
|
||||
edge.delete()
|
||||
# update node connected to edge being deleted
|
||||
other_id = edge.src
|
||||
other_iface = edge.link.iface1
|
||||
if edge.src == object_id:
|
||||
other_id = edge.dst
|
||||
other_iface = edge.link.iface2
|
||||
other_node = self.nodes[other_id]
|
||||
other_node.edges.remove(edge)
|
||||
if other_iface:
|
||||
del other_node.ifaces[other_iface.id]
|
||||
if is_wireless:
|
||||
other_node.delete_antenna()
|
||||
# delete node
|
||||
canvas_node.delete()
|
||||
nodes.append(canvas_node)
|
||||
|
||||
# delete shape
|
||||
if object_id in self.shapes:
|
||||
|
@ -534,27 +304,21 @@ class CanvasGraph(tk.Canvas):
|
|||
|
||||
self.selection.clear()
|
||||
self.core.deleted_canvas_nodes(nodes)
|
||||
self.core.deleted_canvas_edges(edges)
|
||||
|
||||
def delete_edge(self, edge: CanvasEdge) -> None:
|
||||
edge.delete()
|
||||
del self.edges[edge.token]
|
||||
src_node = self.nodes[edge.src]
|
||||
src_node.edges.discard(edge)
|
||||
if edge.link.iface1:
|
||||
del src_node.ifaces[edge.link.iface1.id]
|
||||
dst_node = self.nodes[edge.dst]
|
||||
dst_node.edges.discard(edge)
|
||||
if edge.link.iface2:
|
||||
del dst_node.ifaces[edge.link.iface2.id]
|
||||
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
|
||||
if src_wireless:
|
||||
dst_node.delete_antenna()
|
||||
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
|
||||
if dst_wireless:
|
||||
src_node.delete_antenna()
|
||||
self.core.deleted_canvas_edges([edge])
|
||||
self.arc_common_edges(edge)
|
||||
def hide_selected_objects(self) -> None:
|
||||
for object_id in self.selection:
|
||||
# delete selection box
|
||||
selection_id = self.selection[object_id]
|
||||
self.delete(selection_id)
|
||||
# hide node and related edges
|
||||
if object_id in self.nodes:
|
||||
canvas_node = self.nodes[object_id]
|
||||
canvas_node.hide()
|
||||
|
||||
def show_hidden(self) -> None:
|
||||
for node in self.nodes.values():
|
||||
if node.hidden:
|
||||
node.show()
|
||||
|
||||
def zoom(self, event: tk.Event, factor: float = None) -> None:
|
||||
if not factor:
|
||||
|
@ -588,13 +352,13 @@ class CanvasGraph(tk.Canvas):
|
|||
y_check = self.cursor[1] - self.offset[1]
|
||||
logging.debug("click press offset(%s, %s)", x_check, y_check)
|
||||
is_node = selected in self.nodes
|
||||
if self.mode == GraphMode.EDGE and is_node:
|
||||
pos = self.coords(selected)
|
||||
self.drawing_edge = CanvasEdge(self, selected, pos, pos)
|
||||
if self.manager.mode == GraphMode.EDGE and is_node:
|
||||
node = self.nodes[selected]
|
||||
self.drawing_edge = CanvasEdge(self.app, node)
|
||||
self.organize()
|
||||
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
if is_marker(self.annotation_type):
|
||||
if self.manager.mode == GraphMode.ANNOTATION:
|
||||
if is_marker(self.manager.annotation_type):
|
||||
r = self.app.toolbar.marker_frame.size.get()
|
||||
self.create_oval(
|
||||
x - r,
|
||||
|
@ -604,11 +368,11 @@ class CanvasGraph(tk.Canvas):
|
|||
fill=self.app.toolbar.marker_frame.color,
|
||||
outline="",
|
||||
tags=(tags.MARKER, tags.ANNOTATION),
|
||||
state=self.show_annotations.state(),
|
||||
state=self.manager.show_annotations.state(),
|
||||
)
|
||||
return
|
||||
if selected is None:
|
||||
shape = Shape(self.app, self, self.annotation_type, x, y)
|
||||
shape = Shape(self.app, self, self.manager.annotation_type, x, y)
|
||||
self.selected = shape.id
|
||||
self.shape_drawing = True
|
||||
self.shapes[shape.id] = shape
|
||||
|
@ -629,8 +393,18 @@ class CanvasGraph(tk.Canvas):
|
|||
node.core_node.position.x,
|
||||
node.core_node.position.y,
|
||||
)
|
||||
elif selected in self.shadow_nodes:
|
||||
shadow_node = self.shadow_nodes[selected]
|
||||
self.select_object(shadow_node.id)
|
||||
self.selected = selected
|
||||
logging.debug(
|
||||
"selected shadow node(%s), coords: (%s, %s)",
|
||||
shadow_node.node.core_node.name,
|
||||
shadow_node.node.core_node.position.x,
|
||||
shadow_node.node.core_node.position.y,
|
||||
)
|
||||
else:
|
||||
if self.mode == GraphMode.SELECT:
|
||||
if self.manager.mode == GraphMode.SELECT:
|
||||
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
|
||||
self.select_box = shape
|
||||
self.clear_selection()
|
||||
|
@ -659,7 +433,7 @@ class CanvasGraph(tk.Canvas):
|
|||
if self.select_box:
|
||||
self.select_box.delete()
|
||||
self.select_box = None
|
||||
if is_draw_shape(self.annotation_type) and self.shape_drawing:
|
||||
if is_draw_shape(self.manager.annotation_type) and self.shape_drawing:
|
||||
shape = self.shapes.pop(self.selected)
|
||||
shape.delete()
|
||||
self.shape_drawing = False
|
||||
|
@ -669,14 +443,14 @@ class CanvasGraph(tk.Canvas):
|
|||
y_offset = y - self.cursor[1]
|
||||
self.cursor = x, y
|
||||
|
||||
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
|
||||
self.drawing_edge.move_dst(self.cursor)
|
||||
if self.mode == GraphMode.ANNOTATION:
|
||||
if is_draw_shape(self.annotation_type) and self.shape_drawing:
|
||||
if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None:
|
||||
self.drawing_edge.drawing(self.cursor)
|
||||
if self.manager.mode == GraphMode.ANNOTATION:
|
||||
if is_draw_shape(self.manager.annotation_type) and self.shape_drawing:
|
||||
shape = self.shapes[self.selected]
|
||||
shape.shape_motion(x, y)
|
||||
return
|
||||
elif is_marker(self.annotation_type):
|
||||
elif is_marker(self.manager.annotation_type):
|
||||
r = self.app.toolbar.marker_frame.size.get()
|
||||
self.create_oval(
|
||||
x - r,
|
||||
|
@ -689,21 +463,26 @@ class CanvasGraph(tk.Canvas):
|
|||
)
|
||||
return
|
||||
|
||||
if self.mode == GraphMode.EDGE:
|
||||
if self.manager.mode == GraphMode.EDGE:
|
||||
return
|
||||
|
||||
# move selected objects
|
||||
if self.selection:
|
||||
for selected_id in self.selection:
|
||||
if self.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
|
||||
if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes:
|
||||
shape = self.shapes[selected_id]
|
||||
shape.motion(x_offset, y_offset)
|
||||
|
||||
if self.mode in MOVE_NODE_MODES and selected_id in self.nodes:
|
||||
elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes:
|
||||
node = self.nodes[selected_id]
|
||||
node.motion(x_offset, y_offset, update=self.core.is_runtime())
|
||||
elif (
|
||||
self.manager.mode in MOVE_NODE_MODES
|
||||
and selected_id in self.shadow_nodes
|
||||
):
|
||||
shadow_node = self.shadow_nodes[selected_id]
|
||||
shadow_node.motion(x_offset, y_offset)
|
||||
else:
|
||||
if self.select_box and self.mode == GraphMode.SELECT:
|
||||
if self.select_box and self.manager.mode == GraphMode.SELECT:
|
||||
self.select_box.shape_motion(x, y)
|
||||
|
||||
def press_delete(self, _event: tk.Event) -> None:
|
||||
|
@ -729,17 +508,15 @@ class CanvasGraph(tk.Canvas):
|
|||
return
|
||||
actual_x, actual_y = self.get_actual_coords(x, y)
|
||||
core_node = self.core.create_node(
|
||||
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
||||
actual_x,
|
||||
actual_y,
|
||||
self.manager.node_draw.node_type,
|
||||
self.manager.node_draw.model,
|
||||
)
|
||||
if not core_node:
|
||||
return
|
||||
try:
|
||||
image_enum = self.node_draw.image_enum
|
||||
self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE)
|
||||
except AttributeError:
|
||||
image_file = self.node_draw.image_file
|
||||
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE)
|
||||
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
|
||||
core_node.canvas = self.id
|
||||
node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image)
|
||||
self.nodes[node.id] = node
|
||||
self.core.set_canvas_node(core_node, node)
|
||||
|
||||
|
@ -847,7 +624,7 @@ class CanvasGraph(tk.Canvas):
|
|||
# redraw gridlines to new canvas size
|
||||
self.delete(tags.GRIDLINE)
|
||||
self.draw_grid()
|
||||
self.app.canvas.show_grid.click_handler()
|
||||
self.app.manager.show_grid.click_handler()
|
||||
|
||||
def redraw_wallpaper(self) -> None:
|
||||
if self.adjust_to_dim.get():
|
||||
|
@ -871,7 +648,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.tag_raise(tag)
|
||||
|
||||
def set_wallpaper(self, filename: Optional[str]) -> None:
|
||||
logging.debug("setting wallpaper: %s", filename)
|
||||
logging.info("setting canvas(%s) background: %s", self.id, filename)
|
||||
if filename:
|
||||
img = Image.open(filename)
|
||||
self.wallpaper = img
|
||||
|
@ -884,44 +661,16 @@ class CanvasGraph(tk.Canvas):
|
|||
self.wallpaper_file = None
|
||||
|
||||
def is_selection_mode(self) -> bool:
|
||||
return self.mode == GraphMode.SELECT
|
||||
return self.manager.mode == GraphMode.SELECT
|
||||
|
||||
def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge:
|
||||
"""
|
||||
create an edge between source node and destination node
|
||||
"""
|
||||
pos = (src.core_node.position.x, src.core_node.position.y)
|
||||
edge = CanvasEdge(self, src.id, pos, pos)
|
||||
self.complete_edge(src, dst, edge)
|
||||
edge = CanvasEdge(self.app, src)
|
||||
edge.complete(dst)
|
||||
return edge
|
||||
|
||||
def complete_edge(
|
||||
self,
|
||||
src: CanvasNode,
|
||||
dst: CanvasNode,
|
||||
edge: CanvasEdge,
|
||||
link: Optional[Link] = None,
|
||||
) -> None:
|
||||
linked_wireless = self.is_linked_wireless(src.id, dst.id)
|
||||
edge.complete(dst.id, linked_wireless)
|
||||
if link is None:
|
||||
link = self.core.create_link(edge, src, dst)
|
||||
edge.link = link
|
||||
if link.iface1:
|
||||
iface1 = link.iface1
|
||||
src.ifaces[iface1.id] = iface1
|
||||
if link.iface2:
|
||||
iface2 = link.iface2
|
||||
dst.ifaces[iface2.id] = iface2
|
||||
src.edges.add(edge)
|
||||
dst.edges.add(edge)
|
||||
edge.token = create_edge_token(edge.link)
|
||||
self.arc_common_edges(edge)
|
||||
edge.draw_labels()
|
||||
edge.check_options()
|
||||
self.edges[edge.token] = edge
|
||||
self.core.save_edge(edge, src, dst)
|
||||
|
||||
def copy(self) -> None:
|
||||
if self.core.is_runtime():
|
||||
logging.debug("copy is disabled during runtime state")
|
||||
|
@ -952,7 +701,9 @@ class CanvasGraph(tk.Canvas):
|
|||
)
|
||||
if not copy:
|
||||
continue
|
||||
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
|
||||
node = CanvasNode(
|
||||
self.app, self, scaled_x, scaled_y, copy, canvas_node.image
|
||||
)
|
||||
# copy configurations and services
|
||||
node.core_node.services = core_node.services.copy()
|
||||
node.core_node.config_services = core_node.config_services.copy()
|
||||
|
@ -1039,49 +790,45 @@ class CanvasGraph(tk.Canvas):
|
|||
)
|
||||
self.tag_raise(tags.NODE)
|
||||
|
||||
def is_linked_wireless(self, src: int, dst: int) -> bool:
|
||||
src_node = self.nodes[src]
|
||||
dst_node = self.nodes[dst]
|
||||
src_node_type = src_node.core_node.type
|
||||
dst_node_type = dst_node.core_node.type
|
||||
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
|
||||
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
|
||||
|
||||
# update the wlan/EMANE network
|
||||
wlan_network = self.wireless_network
|
||||
if is_src_wireless and not is_dst_wireless:
|
||||
if src not in wlan_network:
|
||||
wlan_network[src] = set()
|
||||
wlan_network[src].add(dst)
|
||||
elif not is_src_wireless and is_dst_wireless:
|
||||
if dst not in wlan_network:
|
||||
wlan_network[dst] = set()
|
||||
wlan_network[dst].add(src)
|
||||
return is_src_wireless or is_dst_wireless
|
||||
|
||||
def clear_throughputs(self) -> None:
|
||||
for edge in self.edges.values():
|
||||
edge.clear_throughput()
|
||||
|
||||
def scale_graph(self) -> None:
|
||||
for nid, canvas_node in self.nodes.items():
|
||||
img = None
|
||||
if NodeUtils.is_custom(
|
||||
canvas_node.core_node.type, canvas_node.core_node.model
|
||||
):
|
||||
for custom_node in self.app.guiconfig.nodes:
|
||||
if custom_node.name == canvas_node.core_node.model:
|
||||
img = self.app.get_custom_icon(custom_node.image, ICON_SIZE)
|
||||
else:
|
||||
image_enum = TypeToImage.get(
|
||||
canvas_node.core_node.type, canvas_node.core_node.model
|
||||
)
|
||||
img = self.app.get_icon(image_enum, ICON_SIZE)
|
||||
|
||||
self.itemconfig(nid, image=img)
|
||||
canvas_node.image = img
|
||||
for node_id, canvas_node in self.nodes.items():
|
||||
image = nutils.get_icon(canvas_node.core_node, self.app)
|
||||
self.itemconfig(node_id, image=image)
|
||||
canvas_node.image = image
|
||||
canvas_node.scale_text()
|
||||
canvas_node.scale_antennas()
|
||||
for edge_id in self.find_withtag(tags.EDGE):
|
||||
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
|
||||
|
||||
for edge_id in self.find_withtag(tags.EDGE):
|
||||
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
|
||||
def get_metadata(self) -> Dict[str, Any]:
|
||||
wallpaper_path = None
|
||||
if self.wallpaper_file:
|
||||
wallpaper = Path(self.wallpaper_file)
|
||||
if appconfig.BACKGROUNDS_PATH == wallpaper.parent:
|
||||
wallpaper_path = wallpaper.name
|
||||
else:
|
||||
wallpaper_path = str(wallpaper)
|
||||
return dict(
|
||||
id=self.id,
|
||||
wallpaper=wallpaper_path,
|
||||
wallpaper_style=self.scale_option.get(),
|
||||
fit_image=self.adjust_to_dim.get(),
|
||||
)
|
||||
|
||||
def parse_metadata(self, config: Dict[str, Any]) -> None:
|
||||
fit_image = config.get("fit_image", False)
|
||||
self.adjust_to_dim.set(fit_image)
|
||||
wallpaper_style = config.get("wallpaper_style", 1)
|
||||
self.scale_option.set(wallpaper_style)
|
||||
wallpaper = config.get("wallpaper")
|
||||
if wallpaper:
|
||||
wallpaper = Path(wallpaper)
|
||||
if not wallpaper.is_file():
|
||||
wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)
|
||||
logging.info("canvas(%s), wallpaper: %s", self.id, wallpaper)
|
||||
if wallpaper.is_file():
|
||||
self.set_wallpaper(str(wallpaper))
|
||||
else:
|
||||
self.app.show_error(
|
||||
"Background Error", f"background file not found: {wallpaper}"
|
||||
)
|
||||
|
|
383
daemon/core/gui/graph/manager.py
Normal file
383
daemon/core/gui/graph/manager.py
Normal file
|
@ -0,0 +1,383 @@
|
|||
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 import nodeutils as nutils
|
||||
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.nodeutils import NodeDraw
|
||||
|
||||
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 ShowNodeLabels(ShowVar):
|
||||
def click_handler(self) -> None:
|
||||
state = self.state()
|
||||
for canvas in self.manager.all():
|
||||
for node in canvas.nodes.values():
|
||||
if not node.hidden:
|
||||
node.set_label(state)
|
||||
|
||||
|
||||
class ShowLinks(ShowVar):
|
||||
def click_handler(self) -> None:
|
||||
for edge in self.manager.edges.values():
|
||||
if not edge.hidden:
|
||||
edge.check_visibility()
|
||||
|
||||
|
||||
class ShowLinkLabels(ShowVar):
|
||||
def click_handler(self) -> None:
|
||||
state = self.state()
|
||||
for edge in self.manager.edges.values():
|
||||
if not edge.hidden:
|
||||
edge.set_labels(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 = ShowNodeLabels(
|
||||
self, tags.NODE_LABEL, value=True
|
||||
)
|
||||
self.show_link_labels: ShowVar = ShowLinkLabels(
|
||||
self, tags.LINK_LABEL, value=True
|
||||
)
|
||||
self.show_links: ShowVar = ShowLinks(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 = ShowLinks(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)", canvas_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 nutils.should_ignore(core_node):
|
||||
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]
|
||||
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)
|
||||
image = nutils.get_icon(core_node, self.app)
|
||||
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_visibility()
|
||||
|
||||
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)
|
||||
edge.complete(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
|
||||
|
||||
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)
|
|
@ -2,13 +2,16 @@ import functools
|
|||
import logging
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
|
||||
import grpc
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.services_pb2 import ServiceAction
|
||||
from core.api.grpc.wrappers import Interface, Node, NodeType
|
||||
from core.gui import nodeutils, themes
|
||||
from core.gui import images
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui import themes
|
||||
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
|
||||
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
|
||||
from core.gui.dialogs.nodeconfig import NodeConfigDialog
|
||||
|
@ -19,8 +22,7 @@ from core.gui.frames.node import NodeInfoFrame
|
|||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
|
||||
from core.gui.graph.tooltip import CanvasTooltip
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
|
||||
from core.gui.images import ImageEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -31,10 +33,16 @@ NODE_TEXT_OFFSET: int = 5
|
|||
|
||||
class CanvasNode:
|
||||
def __init__(
|
||||
self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage
|
||||
self,
|
||||
app: "Application",
|
||||
canvas: "CanvasGraph",
|
||||
x: float,
|
||||
y: float,
|
||||
core_node: Node,
|
||||
image: PhotoImage,
|
||||
):
|
||||
self.app: "Application" = app
|
||||
self.canvas: "CanvasGraph" = app.canvas
|
||||
self.canvas: "CanvasGraph" = canvas
|
||||
self.image: PhotoImage = image
|
||||
self.core_node: Node = core_node
|
||||
self.id: int = self.canvas.create_image(
|
||||
|
@ -49,7 +57,7 @@ class CanvasNode:
|
|||
tags=tags.NODE_LABEL,
|
||||
font=self.app.icon_text_font,
|
||||
fill="#0000CD",
|
||||
state=self.canvas.show_node_labels.state(),
|
||||
state=self.app.manager.show_node_labels.state(),
|
||||
)
|
||||
self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
|
||||
self.edges: Set[CanvasEdge] = set()
|
||||
|
@ -57,10 +65,14 @@ class CanvasNode:
|
|||
self.wireless_edges: Set[CanvasWirelessEdge] = set()
|
||||
self.antennas: List[int] = []
|
||||
self.antenna_images: Dict[int, PhotoImage] = {}
|
||||
self.hidden: bool = False
|
||||
self.setup_bindings()
|
||||
self.context: tk.Menu = tk.Menu(self.canvas)
|
||||
themes.style_menu(self.context)
|
||||
|
||||
def position(self) -> Tuple[int, int]:
|
||||
return self.canvas.coords(self.id)
|
||||
|
||||
def next_iface_id(self) -> int:
|
||||
i = 0
|
||||
while i in self.ifaces:
|
||||
|
@ -81,9 +93,9 @@ class CanvasNode:
|
|||
self.delete_antennas()
|
||||
|
||||
def add_antenna(self) -> None:
|
||||
x, y = self.canvas.coords(self.id)
|
||||
x, y = self.position()
|
||||
offset = len(self.antennas) * 8 * self.app.app_scale
|
||||
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
|
||||
img = self.app.get_enum_icon(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
|
||||
antenna_id = self.canvas.create_image(
|
||||
x - 16 + offset,
|
||||
y - int(23 * self.app.app_scale),
|
||||
|
@ -139,15 +151,14 @@ class CanvasNode:
|
|||
|
||||
def move(self, x: float, y: float) -> None:
|
||||
x, y = self.canvas.get_scaled_coords(x, y)
|
||||
current_x, current_y = self.canvas.coords(self.id)
|
||||
current_x, current_y = self.position()
|
||||
x_offset = x - current_x
|
||||
y_offset = y - current_y
|
||||
self.motion(x_offset, y_offset, update=False)
|
||||
|
||||
def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None:
|
||||
original_position = self.canvas.coords(self.id)
|
||||
original_position = self.position()
|
||||
self.canvas.move(self.id, x_offset, y_offset)
|
||||
pos = self.canvas.coords(self.id)
|
||||
|
||||
# check new position
|
||||
bbox = self.canvas.bbox(self.id)
|
||||
|
@ -165,11 +176,12 @@ class CanvasNode:
|
|||
|
||||
# move edges
|
||||
for edge in self.edges:
|
||||
edge.move_node(self.id, pos)
|
||||
edge.move_node(self)
|
||||
for edge in self.wireless_edges:
|
||||
edge.move_node(self.id, pos)
|
||||
edge.move_node(self)
|
||||
|
||||
# set actual coords for node and update core is running
|
||||
pos = self.position()
|
||||
real_x, real_y = self.canvas.get_actual_coords(*pos)
|
||||
self.core_node.position.x = real_x
|
||||
self.core_node.position.y = real_y
|
||||
|
@ -179,7 +191,7 @@ class CanvasNode:
|
|||
def on_enter(self, event: tk.Event) -> None:
|
||||
is_runtime = self.app.core.is_runtime()
|
||||
has_observer = self.app.core.observer is not None
|
||||
is_container = NodeUtils.is_container_node(self.core_node.type)
|
||||
is_container = nutils.is_container(self.core_node)
|
||||
if is_runtime and has_observer and is_container:
|
||||
self.tooltip.text.set("waiting...")
|
||||
self.tooltip.on_enter(event)
|
||||
|
@ -194,7 +206,7 @@ class CanvasNode:
|
|||
|
||||
def double_click(self, event: tk.Event) -> None:
|
||||
if self.app.core.is_runtime():
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
if nutils.is_container(self.core_node):
|
||||
self.canvas.core.launch_terminal(self.core_node.id)
|
||||
else:
|
||||
self.show_config()
|
||||
|
@ -222,9 +234,25 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
label="Mobility Player", command=self.show_mobility_player
|
||||
)
|
||||
if nutils.is_container(self.core_node):
|
||||
services_menu = tk.Menu(self.context)
|
||||
for service in sorted(self.core_node.services):
|
||||
service_menu = tk.Menu(services_menu)
|
||||
themes.style_menu(service_menu)
|
||||
start_func = functools.partial(self.start_service, service)
|
||||
service_menu.add_command(label="Start", command=start_func)
|
||||
stop_func = functools.partial(self.stop_service, service)
|
||||
service_menu.add_command(label="Stop", command=stop_func)
|
||||
restart_func = functools.partial(self.restart_service, service)
|
||||
service_menu.add_command(label="Restart", command=restart_func)
|
||||
validate_func = functools.partial(self.validate_service, service)
|
||||
service_menu.add_command(label="Validate", command=validate_func)
|
||||
services_menu.add_cascade(label=service, menu=service_menu)
|
||||
themes.style_menu(services_menu)
|
||||
self.context.add_cascade(label="Services", menu=services_menu)
|
||||
else:
|
||||
self.context.add_command(label="Configure", command=self.show_config)
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
if nutils.is_container(self.core_node):
|
||||
self.context.add_command(label="Services", command=self.show_services)
|
||||
self.context.add_command(
|
||||
label="Config Services", command=self.show_config_services
|
||||
|
@ -241,31 +269,44 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
label="Mobility Config", command=self.show_mobility_config
|
||||
)
|
||||
if NodeUtils.is_wireless_node(self.core_node.type):
|
||||
if nutils.is_wireless(self.core_node):
|
||||
self.context.add_command(
|
||||
label="Link To Selected", command=self.wireless_link_selected
|
||||
)
|
||||
|
||||
link_menu = tk.Menu(self.context)
|
||||
for canvas in self.app.manager.all():
|
||||
canvas_menu = tk.Menu(link_menu)
|
||||
themes.style_menu(canvas_menu)
|
||||
for node in canvas.nodes.values():
|
||||
if not self.is_linkable(node):
|
||||
continue
|
||||
func_link = functools.partial(self.click_link, node)
|
||||
canvas_menu.add_command(
|
||||
label=node.core_node.name, command=func_link
|
||||
)
|
||||
link_menu.add_cascade(label=f"Canvas {canvas.id}", menu=canvas_menu)
|
||||
themes.style_menu(link_menu)
|
||||
self.context.add_cascade(label="Link", menu=link_menu)
|
||||
|
||||
unlink_menu = tk.Menu(self.context)
|
||||
for edge in self.edges:
|
||||
link = edge.link
|
||||
if self.id == edge.src:
|
||||
other_id = edge.dst
|
||||
other_iface = link.iface2.name if link.iface2 else None
|
||||
else:
|
||||
other_id = edge.src
|
||||
other_iface = link.iface1.name if link.iface1 else None
|
||||
other_node = self.canvas.nodes[other_id]
|
||||
other_name = other_node.core_node.name
|
||||
label = f"{other_name}:{other_iface}" if other_iface else other_name
|
||||
other_node = edge.other_node(self)
|
||||
other_iface = edge.other_iface(self)
|
||||
label = other_node.core_node.name
|
||||
if other_iface:
|
||||
label = f"{label}:{other_iface.name}"
|
||||
func_unlink = functools.partial(self.click_unlink, edge)
|
||||
unlink_menu.add_command(label=label, command=func_unlink)
|
||||
themes.style_menu(unlink_menu)
|
||||
self.context.add_cascade(label="Unlink", menu=unlink_menu)
|
||||
|
||||
edit_menu = tk.Menu(self.context)
|
||||
themes.style_menu(edit_menu)
|
||||
edit_menu.add_command(label="Cut", command=self.click_cut)
|
||||
edit_menu.add_command(label="Copy", command=self.canvas_copy)
|
||||
edit_menu.add_command(label="Delete", command=self.canvas_delete)
|
||||
edit_menu.add_command(label="Hide", command=self.click_hide)
|
||||
self.context.add_cascade(label="Edit", menu=edit_menu)
|
||||
self.context.tk_popup(event.x_root, event.y_root)
|
||||
|
||||
|
@ -273,10 +314,18 @@ class CanvasNode:
|
|||
self.canvas_copy()
|
||||
self.canvas_delete()
|
||||
|
||||
def click_hide(self) -> None:
|
||||
self.canvas.clear_selection()
|
||||
self.hide()
|
||||
|
||||
def click_unlink(self, edge: CanvasEdge) -> None:
|
||||
self.canvas.delete_edge(edge)
|
||||
edge.delete()
|
||||
self.app.default_info()
|
||||
|
||||
def click_link(self, node: "CanvasNode") -> None:
|
||||
edge = CanvasEdge(self.app, self, node)
|
||||
edge.complete(node)
|
||||
|
||||
def canvas_delete(self) -> None:
|
||||
self.canvas.clear_selection()
|
||||
self.canvas.select_object(self.id)
|
||||
|
@ -320,15 +369,11 @@ class CanvasNode:
|
|||
def has_emane_link(self, iface_id: int) -> Node:
|
||||
result = None
|
||||
for edge in self.edges:
|
||||
if self.id == edge.src:
|
||||
other_id = edge.dst
|
||||
edge_iface_id = edge.link.iface1.id
|
||||
else:
|
||||
other_id = edge.src
|
||||
edge_iface_id = edge.link.iface2.id
|
||||
other_node = edge.other_node(self)
|
||||
iface = edge.iface(self)
|
||||
edge_iface_id = iface.id if iface else None
|
||||
if edge_iface_id != iface_id:
|
||||
continue
|
||||
other_node = self.canvas.nodes[other_id]
|
||||
if other_node.core_node.type == NodeType.EMANE:
|
||||
result = other_node.core_node
|
||||
break
|
||||
|
@ -344,7 +389,7 @@ class CanvasNode:
|
|||
def scale_antennas(self) -> None:
|
||||
for i in range(len(self.antennas)):
|
||||
antenna_id = self.antennas[i]
|
||||
image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
|
||||
image = self.app.get_enum_icon(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
|
||||
self.canvas.itemconfig(antenna_id, image=image)
|
||||
self.antenna_images[antenna_id] = image
|
||||
node_x, node_y = self.canvas.coords(self.id)
|
||||
|
@ -358,5 +403,166 @@ class CanvasNode:
|
|||
logging.error(f"node icon does not exist: {icon_path}")
|
||||
return
|
||||
self.core_node.icon = icon_path
|
||||
self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
|
||||
self.image = images.from_file(icon_path, width=images.NODE_SIZE)
|
||||
self.canvas.itemconfig(self.id, image=self.image)
|
||||
|
||||
def is_linkable(self, node: "CanvasNode") -> bool:
|
||||
# cannot link to self
|
||||
if self == node:
|
||||
return False
|
||||
# rj45 nodes can only support one link
|
||||
if nutils.is_rj45(self.core_node) and self.edges:
|
||||
return False
|
||||
if nutils.is_rj45(node.core_node) and node.edges:
|
||||
return False
|
||||
# only 1 link between bridge based nodes
|
||||
is_src_bridge = nutils.is_bridge(self.core_node)
|
||||
is_dst_bridge = nutils.is_bridge(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 hide(self) -> None:
|
||||
self.hidden = True
|
||||
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
|
||||
self.canvas.itemconfig(self.text_id, state=tk.HIDDEN)
|
||||
for antenna in self.antennas:
|
||||
self.canvas.itemconfig(antenna, state=tk.HIDDEN)
|
||||
for edge in self.edges:
|
||||
if not edge.hidden:
|
||||
edge.hide()
|
||||
for edge in self.wireless_edges:
|
||||
if not edge.hidden:
|
||||
edge.hide()
|
||||
|
||||
def show(self) -> None:
|
||||
self.hidden = False
|
||||
self.canvas.itemconfig(self.id, state=tk.NORMAL)
|
||||
state = self.app.manager.show_node_labels.state()
|
||||
self.set_label(state)
|
||||
for antenna in self.antennas:
|
||||
self.canvas.itemconfig(antenna, state=tk.NORMAL)
|
||||
for edge in self.edges:
|
||||
other_node = edge.other_node(self)
|
||||
if edge.hidden and not other_node.hidden:
|
||||
edge.show()
|
||||
for edge in self.wireless_edges:
|
||||
other_node = edge.other_node(self)
|
||||
if edge.hidden and not other_node.hidden:
|
||||
edge.show()
|
||||
|
||||
def set_label(self, state: str) -> None:
|
||||
self.canvas.itemconfig(self.text_id, state=state)
|
||||
|
||||
def _service_action(self, service: str, action: ServiceAction) -> None:
|
||||
session_id = self.app.core.session.id
|
||||
try:
|
||||
response = self.app.core.client.service_action(
|
||||
session_id, self.core_node.id, service, action
|
||||
)
|
||||
if not response.result:
|
||||
self.app.show_error("Service Action Error", "Action Failed!")
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Service Error", e)
|
||||
|
||||
def start_service(self, service: str) -> None:
|
||||
self._service_action(service, ServiceAction.START)
|
||||
|
||||
def stop_service(self, service: str) -> None:
|
||||
self._service_action(service, ServiceAction.STOP)
|
||||
|
||||
def restart_service(self, service: str) -> None:
|
||||
self._service_action(service, ServiceAction.RESTART)
|
||||
|
||||
def validate_service(self, service: str) -> None:
|
||||
self._service_action(service, ServiceAction.VALIDATE)
|
||||
|
||||
def is_wireless(self) -> bool:
|
||||
return nutils.is_wireless(self.core_node)
|
||||
|
||||
|
||||
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_enum_icon(
|
||||
ImageEnum.SHADOW, width=images.NODE_SIZE
|
||||
)
|
||||
self.draw()
|
||||
self.setup_bindings()
|
||||
|
||||
def setup_bindings(self) -> None:
|
||||
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.node.double_click)
|
||||
self.canvas.tag_bind(self.id, "<Enter>", self.node.on_enter)
|
||||
self.canvas.tag_bind(self.id, "<Leave>", self.node.on_leave)
|
||||
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.node.show_context)
|
||||
self.canvas.tag_bind(self.id, "<Button-1>", self.node.show_info)
|
||||
|
||||
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.other_node(self.node)
|
||||
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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
from core.gui.dialogs.shapemod import ShapeDialog
|
||||
from core.gui.graph import tags
|
||||
|
@ -69,6 +69,31 @@ class Shape:
|
|||
self.shape_data = data
|
||||
self.draw()
|
||||
|
||||
@classmethod
|
||||
def from_metadata(cls, app: "Application", config: Dict[str, Any]) -> None:
|
||||
shape_type = config["type"]
|
||||
try:
|
||||
shape_type = ShapeType(shape_type)
|
||||
coords = config["iconcoords"]
|
||||
data = AnnotationData(
|
||||
config["label"],
|
||||
config["fontfamily"],
|
||||
config["fontsize"],
|
||||
config["labelcolor"],
|
||||
config["color"],
|
||||
config["border"],
|
||||
config["width"],
|
||||
config["bold"],
|
||||
config["italic"],
|
||||
config["underline"],
|
||||
)
|
||||
canvas_id = config.get("canvas", 1)
|
||||
canvas = app.manager.get(canvas_id)
|
||||
shape = Shape(app, canvas, shape_type, *coords, data=data)
|
||||
canvas.shapes[shape.id] = shape
|
||||
except ValueError:
|
||||
logging.exception("unknown shape: %s", shape_type)
|
||||
|
||||
def draw(self) -> None:
|
||||
if self.created:
|
||||
dash = None
|
||||
|
@ -85,7 +110,7 @@ class Shape:
|
|||
fill=self.shape_data.fill_color,
|
||||
outline=self.shape_data.border_color,
|
||||
width=self.shape_data.border_width,
|
||||
state=self.canvas.show_annotations.state(),
|
||||
state=self.app.manager.show_annotations.state(),
|
||||
)
|
||||
self.draw_shape_text()
|
||||
elif self.shape_type == ShapeType.RECTANGLE:
|
||||
|
@ -99,7 +124,7 @@ class Shape:
|
|||
fill=self.shape_data.fill_color,
|
||||
outline=self.shape_data.border_color,
|
||||
width=self.shape_data.border_width,
|
||||
state=self.canvas.show_annotations.state(),
|
||||
state=self.app.manager.show_annotations.state(),
|
||||
)
|
||||
self.draw_shape_text()
|
||||
elif self.shape_type == ShapeType.TEXT:
|
||||
|
@ -111,7 +136,7 @@ class Shape:
|
|||
text=self.shape_data.text,
|
||||
fill=self.shape_data.text_color,
|
||||
font=font,
|
||||
state=self.canvas.show_annotations.state(),
|
||||
state=self.app.manager.show_annotations.state(),
|
||||
)
|
||||
else:
|
||||
logging.error("unknown shape type: %s", self.shape_type)
|
||||
|
@ -139,7 +164,7 @@ class Shape:
|
|||
text=self.shape_data.text,
|
||||
fill=self.shape_data.text_color,
|
||||
font=font,
|
||||
state=self.canvas.show_annotations.state(),
|
||||
state=self.app.manager.show_annotations.state(),
|
||||
)
|
||||
|
||||
def shape_motion(self, x1: float, y1: float) -> None:
|
||||
|
@ -184,6 +209,7 @@ class Shape:
|
|||
x1, y1 = self.canvas.get_actual_coords(x1, y1)
|
||||
coords = (x1, y1)
|
||||
return {
|
||||
"canvas": self.canvas.id,
|
||||
"type": self.shape_type.value,
|
||||
"iconcoords": coords,
|
||||
"label": self.shape_data.text,
|
||||
|
|
|
@ -14,6 +14,7 @@ NODE: str = "node"
|
|||
WALLPAPER: str = "wallpaper"
|
||||
SELECTION: str = "selectednodes"
|
||||
MARKER: str = "marker"
|
||||
HIDDEN: str = "hidden"
|
||||
ORGANIZE_TAGS: List[str] = [
|
||||
WALLPAPER,
|
||||
GRIDLINE,
|
||||
|
|
|
@ -1,53 +1,46 @@
|
|||
from enum import Enum
|
||||
from tkinter import messagebox
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from PIL import Image
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.wrappers import NodeType
|
||||
from core.api.grpc.wrappers import Node, NodeType
|
||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||
|
||||
NODE_SIZE: int = 48
|
||||
ANTENNA_SIZE: int = 32
|
||||
BUTTON_SIZE: int = 16
|
||||
ERROR_SIZE: int = 24
|
||||
DIALOG_SIZE: int = 16
|
||||
IMAGES: Dict[str, str] = {}
|
||||
|
||||
class Images:
|
||||
images: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage:
|
||||
if height is None:
|
||||
height = width
|
||||
image = Image.open(file_path)
|
||||
image = image.resize((width, height), Image.ANTIALIAS)
|
||||
return PhotoImage(image)
|
||||
|
||||
@classmethod
|
||||
def load_all(cls) -> None:
|
||||
for image in LOCAL_ICONS_PATH.glob("*"):
|
||||
cls.images[image.stem] = str(image)
|
||||
|
||||
@classmethod
|
||||
def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage:
|
||||
file_path = cls.images[image_enum.value]
|
||||
return cls.create(file_path, width, height)
|
||||
|
||||
@classmethod
|
||||
def get_with_image_file(
|
||||
cls, stem: str, width: int, height: int = None
|
||||
) -> PhotoImage:
|
||||
file_path = cls.images[stem]
|
||||
return cls.create(file_path, width, height)
|
||||
|
||||
@classmethod
|
||||
def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage:
|
||||
def load_all() -> None:
|
||||
for image in LOCAL_ICONS_PATH.glob("*"):
|
||||
try:
|
||||
file_path = cls.images[name]
|
||||
return cls.create(file_path, width, height)
|
||||
except KeyError:
|
||||
messagebox.showwarning(
|
||||
"Missing image file",
|
||||
f"{name}.png is missing at daemon/core/gui/data/icons, drop image "
|
||||
f"file at daemon/core/gui/data/icons and restart the gui",
|
||||
)
|
||||
ImageEnum(image.stem)
|
||||
IMAGES[image.stem] = str(image)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def from_file(
|
||||
file_path: str, *, width: int, height: int = None, scale: float = 1.0
|
||||
) -> PhotoImage:
|
||||
if height is None:
|
||||
height = width
|
||||
width = int(width * scale)
|
||||
height = int(height * scale)
|
||||
image = Image.open(file_path)
|
||||
image = image.resize((width, height), Image.ANTIALIAS)
|
||||
return PhotoImage(image)
|
||||
|
||||
|
||||
def from_enum(
|
||||
image_enum: "ImageEnum", *, width: int, height: int = None, scale: float = 1.0
|
||||
) -> PhotoImage:
|
||||
file_path = IMAGES[image_enum.value]
|
||||
return from_file(file_path, width=width, height=height, scale=scale)
|
||||
|
||||
|
||||
class ImageEnum(Enum):
|
||||
|
@ -90,25 +83,29 @@ class ImageEnum(Enum):
|
|||
SHUTDOWN = "shutdown"
|
||||
CANCEL = "cancel"
|
||||
ERROR = "error"
|
||||
SHADOW = "shadow"
|
||||
|
||||
|
||||
class TypeToImage:
|
||||
type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = {
|
||||
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
||||
(NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
||||
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||
(NodeType.HUB, ""): ImageEnum.HUB,
|
||||
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||
(NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||
(NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
||||
(NodeType.LXC, ""): ImageEnum.LXC,
|
||||
}
|
||||
TYPE_MAP: Dict[Tuple[NodeType, str], ImageEnum] = {
|
||||
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
||||
(NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
||||
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||
(NodeType.HUB, ""): ImageEnum.HUB,
|
||||
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||
(NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||
(NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
||||
(NodeType.LXC, ""): ImageEnum.LXC,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, node_type, model) -> Optional[ImageEnum]:
|
||||
return cls.type_to_image.get((node_type, model))
|
||||
|
||||
def from_node(node: Node, *, scale: float) -> Optional[PhotoImage]:
|
||||
image = None
|
||||
image_enum = TYPE_MAP.get((node.type, node.model))
|
||||
if image_enum:
|
||||
image = from_enum(image_enum, width=NODE_SIZE, scale=scale)
|
||||
return image
|
||||
|
|
|
@ -4,13 +4,19 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
|||
import netaddr
|
||||
from netaddr import EUI, IPNetwork
|
||||
|
||||
from core.api.grpc.wrappers import Interface, Link, Node
|
||||
from core.api.grpc.wrappers import Interface, Link, LinkType, Node
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.graph.edges import CanvasEdge
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
IP4_MASK: int = 24
|
||||
IP6_MASK: int = 64
|
||||
WIRELESS_IP4_MASK: int = 32
|
||||
WIRELESS_IP6_MASK: int = 128
|
||||
|
||||
|
||||
def get_index(iface: Interface) -> Optional[int]:
|
||||
if not iface.ip4:
|
||||
|
@ -47,25 +53,24 @@ class InterfaceManager:
|
|||
self.app: "Application" = app
|
||||
ip4 = self.app.guiconfig.ips.ip4
|
||||
ip6 = self.app.guiconfig.ips.ip6
|
||||
self.ip4_mask: int = 24
|
||||
self.ip6_mask: int = 64
|
||||
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}")
|
||||
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}")
|
||||
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{IP4_MASK}")
|
||||
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{IP6_MASK}")
|
||||
mac = self.app.guiconfig.mac
|
||||
self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded)
|
||||
self.current_mac: Optional[EUI] = None
|
||||
self.current_subnets: Optional[Subnets] = None
|
||||
self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {}
|
||||
self.used_macs: Set[str] = set()
|
||||
|
||||
def update_ips(self, ip4: str, ip6: str) -> None:
|
||||
self.reset()
|
||||
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}")
|
||||
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}")
|
||||
|
||||
def reset_mac(self) -> None:
|
||||
self.current_mac = self.mac
|
||||
self.ip4_subnets = IPNetwork(f"{ip4}/{IP4_MASK}")
|
||||
self.ip6_subnets = IPNetwork(f"{ip6}/{IP6_MASK}")
|
||||
|
||||
def next_mac(self) -> str:
|
||||
while str(self.current_mac) in self.used_macs:
|
||||
value = self.current_mac.value + 1
|
||||
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
|
||||
mac = str(self.current_mac)
|
||||
value = self.current_mac.value + 1
|
||||
self.current_mac = EUI(value, dialect=netaddr.mac_unix_expanded)
|
||||
|
@ -114,6 +119,15 @@ class InterfaceManager:
|
|||
subnets.used_indexes.discard(index)
|
||||
self.current_subnets = None
|
||||
|
||||
def set_macs(self, links: List[Link]) -> None:
|
||||
self.current_mac = self.mac
|
||||
self.used_macs.clear()
|
||||
for link in links:
|
||||
if link.iface1:
|
||||
self.used_macs.add(link.iface1.mac)
|
||||
if link.iface2:
|
||||
self.used_macs.add(link.iface2.mac)
|
||||
|
||||
def joined(self, links: List[Link]) -> None:
|
||||
ifaces = []
|
||||
for link in links:
|
||||
|
@ -133,7 +147,7 @@ class InterfaceManager:
|
|||
self.used_subnets[subnets.key()] = subnets
|
||||
|
||||
def next_index(self, node: Node) -> int:
|
||||
if NodeUtils.is_router_node(node):
|
||||
if nutils.is_router(node):
|
||||
index = 1
|
||||
else:
|
||||
index = 20
|
||||
|
@ -153,10 +167,10 @@ class InterfaceManager:
|
|||
def get_subnets(self, iface: Interface) -> Subnets:
|
||||
ip4_subnet = self.ip4_subnets
|
||||
if iface.ip4:
|
||||
ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr
|
||||
ip4_subnet = IPNetwork(f"{iface.ip4}/{IP4_MASK}").cidr
|
||||
ip6_subnet = self.ip6_subnets
|
||||
if iface.ip6:
|
||||
ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6_mask}").cidr
|
||||
ip6_subnet = IPNetwork(f"{iface.ip6}/{IP6_MASK}").cidr
|
||||
subnets = Subnets(ip4_subnet, ip6_subnet)
|
||||
return self.used_subnets.get(subnets.key(), subnets)
|
||||
|
||||
|
@ -165,8 +179,8 @@ class InterfaceManager:
|
|||
) -> None:
|
||||
src_node = canvas_src_node.core_node
|
||||
dst_node = canvas_dst_node.core_node
|
||||
is_src_container = NodeUtils.is_container_node(src_node.type)
|
||||
is_dst_container = NodeUtils.is_container_node(dst_node.type)
|
||||
is_src_container = nutils.is_container(src_node)
|
||||
is_dst_container = nutils.is_container(dst_node)
|
||||
if is_src_container and is_dst_container:
|
||||
self.current_subnets = self.next_subnets()
|
||||
elif is_src_container and not is_dst_container:
|
||||
|
@ -188,19 +202,16 @@ class InterfaceManager:
|
|||
self, canvas_node: CanvasNode, visited: Set[int] = None
|
||||
) -> Optional[IPNetwork]:
|
||||
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
|
||||
canvas = self.app.canvas
|
||||
subnets = None
|
||||
if not visited:
|
||||
visited = set()
|
||||
visited.add(canvas_node.core_node.id)
|
||||
for edge in canvas_node.edges:
|
||||
src_node = canvas.nodes[edge.src]
|
||||
dst_node = canvas.nodes[edge.dst]
|
||||
iface = edge.link.iface1
|
||||
check_node = src_node
|
||||
if src_node == canvas_node:
|
||||
check_node = edge.src
|
||||
if edge.src == canvas_node:
|
||||
iface = edge.link.iface2
|
||||
check_node = dst_node
|
||||
check_node = edge.dst
|
||||
if check_node.core_node.id in visited:
|
||||
continue
|
||||
visited.add(check_node.core_node.id)
|
||||
|
@ -212,3 +223,48 @@ class InterfaceManager:
|
|||
logging.info("found subnets: %s", subnets)
|
||||
break
|
||||
return subnets
|
||||
|
||||
def create_link(self, edge: CanvasEdge) -> Link:
|
||||
"""
|
||||
Create core link for a given edge based on src/dst nodes.
|
||||
"""
|
||||
src_node = edge.src.core_node
|
||||
dst_node = edge.dst.core_node
|
||||
self.determine_subnets(edge.src, edge.dst)
|
||||
src_iface = None
|
||||
if nutils.is_container(src_node):
|
||||
src_iface = self.create_iface(edge.src, edge.linked_wireless)
|
||||
dst_iface = None
|
||||
if nutils.is_container(dst_node):
|
||||
dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
|
||||
link = Link(
|
||||
type=LinkType.WIRED,
|
||||
node1_id=src_node.id,
|
||||
node2_id=dst_node.id,
|
||||
iface1=src_iface,
|
||||
iface2=dst_iface,
|
||||
)
|
||||
logging.info("added link between %s and %s", src_node.name, dst_node.name)
|
||||
return link
|
||||
|
||||
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
|
||||
node = canvas_node.core_node
|
||||
ip4, ip6 = self.get_ips(node)
|
||||
if wireless_link:
|
||||
ip4_mask = WIRELESS_IP4_MASK
|
||||
ip6_mask = WIRELESS_IP6_MASK
|
||||
else:
|
||||
ip4_mask = IP4_MASK
|
||||
ip6_mask = IP6_MASK
|
||||
iface_id = canvas_node.next_iface_id()
|
||||
name = f"eth{iface_id}"
|
||||
iface = Interface(
|
||||
id=iface_id,
|
||||
name=name,
|
||||
ip4=ip4,
|
||||
ip4_mask=ip4_mask,
|
||||
ip6=ip6,
|
||||
ip6_mask=ip6_mask,
|
||||
)
|
||||
logging.info("create node(%s) interface(%s)", node.name, iface)
|
||||
return iface
|
||||
|
|
|
@ -6,6 +6,7 @@ from functools import partial
|
|||
from tkinter import filedialog, messagebox
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui import images
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.dialogs.about import AboutDialog
|
||||
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
||||
|
@ -22,8 +23,7 @@ from core.gui.dialogs.servers import ServersDialog
|
|||
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
from core.gui.dialogs.throughput import ThroughputDialog
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
from core.gui.nodeutils import ICON_SIZE
|
||||
from core.gui.graph.manager import CanvasManager
|
||||
from core.gui.observers import ObserversMenu
|
||||
from core.gui.task import ProgressTask
|
||||
|
||||
|
@ -45,9 +45,10 @@ class Menubar(tk.Menu):
|
|||
super().__init__(app)
|
||||
self.app: "Application" = app
|
||||
self.core: CoreClient = app.core
|
||||
self.canvas: CanvasGraph = app.canvas
|
||||
self.manager: CanvasManager = app.manager
|
||||
self.recent_menu: Optional[tk.Menu] = None
|
||||
self.edit_menu: Optional[tk.Menu] = None
|
||||
self.canvas_menu: Optional[tk.Menu] = None
|
||||
self.observers_menu: Optional[ObserversMenu] = None
|
||||
self.draw()
|
||||
|
||||
|
@ -106,6 +107,7 @@ class Menubar(tk.Menu):
|
|||
menu = tk.Menu(self)
|
||||
menu.add_command(label="Preferences", command=self.click_preferences)
|
||||
menu.add_command(label="Custom Nodes", command=self.click_custom_nodes)
|
||||
menu.add_command(label="Show Hidden Nodes", command=self.click_show_hidden)
|
||||
menu.add_separator()
|
||||
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
|
||||
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
|
||||
|
@ -116,11 +118,13 @@ class Menubar(tk.Menu):
|
|||
menu.add_command(
|
||||
label="Delete", accelerator="Ctrl+D", command=self.click_delete
|
||||
)
|
||||
menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide)
|
||||
self.add_cascade(label="Edit", menu=menu)
|
||||
self.app.master.bind_all("<Control-x>", self.click_cut)
|
||||
self.app.master.bind_all("<Control-c>", self.click_copy)
|
||||
self.app.master.bind_all("<Control-v>", self.click_paste)
|
||||
self.app.master.bind_all("<Control-d>", self.click_delete)
|
||||
self.app.master.bind_all("<Control-h>", self.click_hide)
|
||||
self.edit_menu = menu
|
||||
|
||||
def draw_canvas_menu(self) -> None:
|
||||
|
@ -128,9 +132,13 @@ class Menubar(tk.Menu):
|
|||
Create canvas menu
|
||||
"""
|
||||
menu = tk.Menu(self)
|
||||
menu.add_command(label="New", command=self.click_canvas_add)
|
||||
menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
|
||||
menu.add_separator()
|
||||
menu.add_command(label="Delete", command=self.click_canvas_delete)
|
||||
menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
|
||||
self.add_cascade(label="Canvas", menu=menu)
|
||||
self.canvas_menu = menu
|
||||
|
||||
def draw_view_menu(self) -> None:
|
||||
"""
|
||||
|
@ -145,52 +153,52 @@ class Menubar(tk.Menu):
|
|||
menu.add_checkbutton(
|
||||
label="Interface Names",
|
||||
command=self.click_edge_label_change,
|
||||
variable=self.canvas.show_iface_names,
|
||||
variable=self.manager.show_iface_names,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="IPv4 Addresses",
|
||||
command=self.click_edge_label_change,
|
||||
variable=self.canvas.show_ip4s,
|
||||
variable=self.manager.show_ip4s,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="IPv6 Addresses",
|
||||
command=self.click_edge_label_change,
|
||||
variable=self.canvas.show_ip6s,
|
||||
variable=self.manager.show_ip6s,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Node Labels",
|
||||
command=self.canvas.show_node_labels.click_handler,
|
||||
variable=self.canvas.show_node_labels,
|
||||
command=self.manager.show_node_labels.click_handler,
|
||||
variable=self.manager.show_node_labels,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Link Labels",
|
||||
command=self.canvas.show_link_labels.click_handler,
|
||||
variable=self.canvas.show_link_labels,
|
||||
command=self.manager.show_link_labels.click_handler,
|
||||
variable=self.manager.show_link_labels,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Links",
|
||||
command=self.canvas.show_links.click_handler,
|
||||
variable=self.canvas.show_links,
|
||||
command=self.manager.show_links.click_handler,
|
||||
variable=self.manager.show_links,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Loss Links",
|
||||
command=self.canvas.show_loss_links.click_handler,
|
||||
variable=self.canvas.show_loss_links,
|
||||
command=self.manager.show_loss_links.click_handler,
|
||||
variable=self.manager.show_loss_links,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Wireless Links",
|
||||
command=self.canvas.show_wireless.click_handler,
|
||||
variable=self.canvas.show_wireless,
|
||||
command=self.manager.show_wireless.click_handler,
|
||||
variable=self.manager.show_wireless,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Annotations",
|
||||
command=self.canvas.show_annotations.click_handler,
|
||||
variable=self.canvas.show_annotations,
|
||||
command=self.manager.show_annotations.click_handler,
|
||||
variable=self.manager.show_annotations,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Canvas Grid",
|
||||
command=self.canvas.show_grid.click_handler,
|
||||
variable=self.canvas.show_grid,
|
||||
command=self.manager.show_grid.click_handler,
|
||||
variable=self.manager.show_grid,
|
||||
)
|
||||
self.add_cascade(label="View", menu=menu)
|
||||
|
||||
|
@ -334,17 +342,12 @@ class Menubar(tk.Menu):
|
|||
self.app.save_config()
|
||||
self.app.menubar.update_recent_files()
|
||||
|
||||
def change_menubar_item_state(self, is_runtime: bool) -> None:
|
||||
labels = {"Copy", "Paste", "Delete", "Cut"}
|
||||
for i in range(self.edit_menu.index(tk.END) + 1):
|
||||
try:
|
||||
label = self.edit_menu.entrycget(i, "label")
|
||||
if label not in labels:
|
||||
continue
|
||||
state = tk.DISABLED if is_runtime else tk.NORMAL
|
||||
self.edit_menu.entryconfig(i, state=state)
|
||||
except tk.TclError:
|
||||
pass
|
||||
def set_state(self, is_runtime: bool) -> None:
|
||||
state = tk.DISABLED if is_runtime else tk.NORMAL
|
||||
for entry in {"Copy", "Paste", "Delete", "Cut"}:
|
||||
self.edit_menu.entryconfigure(entry, state=state)
|
||||
for entry in {"Delete"}:
|
||||
self.canvas_menu.entryconfigure(entry, state=state)
|
||||
|
||||
def prompt_save_running_session(self, quit_app: bool = False) -> None:
|
||||
"""
|
||||
|
@ -372,6 +375,12 @@ class Menubar(tk.Menu):
|
|||
dialog = PreferencesDialog(self.app)
|
||||
dialog.show()
|
||||
|
||||
def click_canvas_add(self) -> None:
|
||||
self.manager.add_canvas()
|
||||
|
||||
def click_canvas_delete(self) -> None:
|
||||
self.manager.delete_canvas()
|
||||
|
||||
def click_canvas_size_and_scale(self) -> None:
|
||||
dialog = SizeAndScaleDialog(self.app)
|
||||
dialog.show()
|
||||
|
@ -401,17 +410,29 @@ class Menubar(tk.Menu):
|
|||
dialog.show()
|
||||
|
||||
def click_copy(self, _event: tk.Event = None) -> None:
|
||||
self.canvas.copy()
|
||||
canvas = self.manager.current()
|
||||
canvas.copy()
|
||||
|
||||
def click_paste(self, _event: tk.Event = None) -> None:
|
||||
self.canvas.paste()
|
||||
canvas = self.manager.current()
|
||||
canvas.paste()
|
||||
|
||||
def click_delete(self, _event: tk.Event = None) -> None:
|
||||
self.canvas.delete_selected_objects()
|
||||
canvas = self.manager.current()
|
||||
canvas.delete_selected_objects()
|
||||
|
||||
def click_hide(self, _event: tk.Event = None) -> None:
|
||||
canvas = self.manager.current()
|
||||
canvas.hide_selected_objects()
|
||||
|
||||
def click_cut(self, _event: tk.Event = None) -> None:
|
||||
self.canvas.copy()
|
||||
self.canvas.delete_selected_objects()
|
||||
canvas = self.manager.current()
|
||||
canvas.copy()
|
||||
canvas.delete_selected_objects()
|
||||
|
||||
def click_show_hidden(self, _event: tk.Event = None) -> None:
|
||||
for canvas in self.manager.all():
|
||||
canvas.show_hidden()
|
||||
|
||||
def click_session_options(self) -> None:
|
||||
logging.debug("Click options")
|
||||
|
@ -439,14 +460,15 @@ class Menubar(tk.Menu):
|
|||
dialog.show()
|
||||
|
||||
def click_autogrid(self) -> None:
|
||||
width, height = self.canvas.current_dimensions
|
||||
padding = (ICON_SIZE / 2) + 10
|
||||
layout_size = padding + ICON_SIZE
|
||||
width, height = self.manager.current_dimensions
|
||||
padding = (images.NODE_SIZE / 2) + 10
|
||||
layout_size = padding + images.NODE_SIZE
|
||||
col_count = width // layout_size
|
||||
logging.info(
|
||||
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count
|
||||
)
|
||||
for i, node in enumerate(self.canvas.nodes.values()):
|
||||
canvas = self.manager.current()
|
||||
for i, node in enumerate(canvas.nodes.values()):
|
||||
col = i % col_count
|
||||
row = i // col_count
|
||||
x = (col * layout_size) + padding
|
||||
|
@ -460,7 +482,7 @@ class Menubar(tk.Menu):
|
|||
self.app.hide_info()
|
||||
|
||||
def click_edge_label_change(self) -> None:
|
||||
for edge in self.canvas.edges.values():
|
||||
for edge in self.manager.edges.values():
|
||||
edge.draw_labels()
|
||||
|
||||
def click_mac_config(self) -> None:
|
||||
|
|
|
@ -1,14 +1,139 @@
|
|||
import logging
|
||||
from typing import List, Optional, Set
|
||||
from typing import TYPE_CHECKING, List, Optional, Set
|
||||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.wrappers import Node, NodeType
|
||||
from core.gui import images
|
||||
from core.gui.appconfig import CustomNode, GuiConfig
|
||||
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||
from core.gui.images import ImageEnum
|
||||
|
||||
ICON_SIZE: int = 48
|
||||
ANTENNA_SIZE: int = 32
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
NODES: List["NodeDraw"] = []
|
||||
NETWORK_NODES: List["NodeDraw"] = []
|
||||
NODE_ICONS = {}
|
||||
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
||||
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
|
||||
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
||||
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
|
||||
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
||||
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
|
||||
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
||||
ANTENNA_ICON: Optional[PhotoImage] = None
|
||||
|
||||
|
||||
def setup() -> None:
|
||||
global ANTENNA_ICON
|
||||
nodes = [
|
||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
||||
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
|
||||
(ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"),
|
||||
(ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"),
|
||||
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
|
||||
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
|
||||
(ImageEnum.LXC, NodeType.LXC, "LXC", None),
|
||||
]
|
||||
for image_enum, node_type, label, model in nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, label, model)
|
||||
NODES.append(node_draw)
|
||||
NODE_ICONS[(node_type, model)] = node_draw.image
|
||||
network_nodes = [
|
||||
(ImageEnum.HUB, NodeType.HUB, "Hub"),
|
||||
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
|
||||
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
|
||||
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
||||
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
|
||||
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
|
||||
]
|
||||
for image_enum, node_type, label in network_nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, label)
|
||||
NETWORK_NODES.append(node_draw)
|
||||
NODE_ICONS[(node_type, None)] = node_draw.image
|
||||
ANTENNA_ICON = images.from_enum(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
|
||||
|
||||
|
||||
def is_bridge(node: Node) -> bool:
|
||||
return node.type in BRIDGE_NODES
|
||||
|
||||
|
||||
def is_mobility(node: Node) -> bool:
|
||||
return node.type in MOBILITY_NODES
|
||||
|
||||
|
||||
def is_router(node: Node) -> bool:
|
||||
return is_model(node) and node.model in ROUTER_NODES
|
||||
|
||||
|
||||
def should_ignore(node: Node) -> bool:
|
||||
return node.type in IGNORE_NODES
|
||||
|
||||
|
||||
def is_container(node: Node) -> bool:
|
||||
return node.type in CONTAINER_NODES
|
||||
|
||||
|
||||
def is_model(node: Node) -> bool:
|
||||
return node.type == NodeType.DEFAULT
|
||||
|
||||
|
||||
def has_image(node_type: NodeType) -> bool:
|
||||
return node_type in IMAGE_NODES
|
||||
|
||||
|
||||
def is_wireless(node: Node) -> bool:
|
||||
return node.type in WIRELESS_NODES
|
||||
|
||||
|
||||
def is_rj45(node: Node) -> bool:
|
||||
return node.type in RJ45_NODES
|
||||
|
||||
|
||||
def is_custom(node: Node) -> bool:
|
||||
return is_model(node) and node.model not in NODE_MODELS
|
||||
|
||||
|
||||
def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
|
||||
for custom_node in gui_config.nodes:
|
||||
if custom_node.name == name:
|
||||
return custom_node.services
|
||||
return []
|
||||
|
||||
|
||||
def _get_custom_file(config: GuiConfig, name: str) -> Optional[str]:
|
||||
for custom_node in config.nodes:
|
||||
if custom_node.name == name:
|
||||
return custom_node.image
|
||||
return None
|
||||
|
||||
|
||||
def get_icon(node: Node, app: "Application") -> PhotoImage:
|
||||
scale = app.app_scale
|
||||
image = None
|
||||
# node icon was overriden with a specific value
|
||||
if node.icon:
|
||||
try:
|
||||
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
|
||||
except OSError:
|
||||
logging.error("invalid icon: %s", node.icon)
|
||||
# custom node
|
||||
elif is_custom(node):
|
||||
image_file = _get_custom_file(app.guiconfig, node.model)
|
||||
logging.info("custom node file: %s", image_file)
|
||||
if image_file:
|
||||
image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale)
|
||||
# built in node
|
||||
else:
|
||||
image = images.from_node(node, scale=scale)
|
||||
# default image, if everything above fails
|
||||
if not image:
|
||||
image = images.from_enum(
|
||||
ImageEnum.EDITNODE, width=images.NODE_SIZE, scale=scale
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
class NodeDraw:
|
||||
|
@ -33,7 +158,7 @@ class NodeDraw:
|
|||
) -> "NodeDraw":
|
||||
node_draw = NodeDraw()
|
||||
node_draw.image_enum = image_enum
|
||||
node_draw.image = Images.get(image_enum, ICON_SIZE)
|
||||
node_draw.image = images.from_enum(image_enum, width=images.NODE_SIZE)
|
||||
node_draw.node_type = node_type
|
||||
node_draw.label = label
|
||||
node_draw.model = model
|
||||
|
@ -45,135 +170,10 @@ class NodeDraw:
|
|||
node_draw = NodeDraw()
|
||||
node_draw.custom = True
|
||||
node_draw.image_file = custom_node.image
|
||||
node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE)
|
||||
node_draw.image = images.from_file(custom_node.image, width=images.NODE_SIZE)
|
||||
node_draw.node_type = NodeType.DEFAULT
|
||||
node_draw.services = custom_node.services
|
||||
node_draw.label = custom_node.name
|
||||
node_draw.model = custom_node.name
|
||||
node_draw.tooltip = custom_node.name
|
||||
return node_draw
|
||||
|
||||
|
||||
class NodeUtils:
|
||||
NODES: List[NodeDraw] = []
|
||||
NETWORK_NODES: List[NodeDraw] = []
|
||||
NODE_ICONS = {}
|
||||
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
||||
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
|
||||
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
||||
BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH}
|
||||
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
||||
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
|
||||
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
||||
ANTENNA_ICON: PhotoImage = None
|
||||
|
||||
@classmethod
|
||||
def is_bridge_node(cls, node: Node) -> bool:
|
||||
return node.type in cls.BRIDGE_NODES
|
||||
|
||||
@classmethod
|
||||
def is_mobility(cls, node: Node) -> bool:
|
||||
return node.type in cls.MOBILITY_NODES
|
||||
|
||||
@classmethod
|
||||
def is_router_node(cls, node: Node) -> bool:
|
||||
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES
|
||||
|
||||
@classmethod
|
||||
def is_ignore_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type in cls.IGNORE_NODES
|
||||
|
||||
@classmethod
|
||||
def is_container_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type in cls.CONTAINER_NODES
|
||||
|
||||
@classmethod
|
||||
def is_model_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type == NodeType.DEFAULT
|
||||
|
||||
@classmethod
|
||||
def is_image_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type in cls.IMAGE_NODES
|
||||
|
||||
@classmethod
|
||||
def is_wireless_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type in cls.WIRELESS_NODES
|
||||
|
||||
@classmethod
|
||||
def is_rj45_node(cls, node_type: NodeType) -> bool:
|
||||
return node_type in cls.RJ45_NODES
|
||||
|
||||
@classmethod
|
||||
def node_icon(
|
||||
cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale: float = 1.0
|
||||
) -> PhotoImage:
|
||||
|
||||
image_enum = TypeToImage.get(node_type, model)
|
||||
if image_enum:
|
||||
return Images.get(image_enum, int(ICON_SIZE * scale))
|
||||
else:
|
||||
image_stem = cls.get_image_file(gui_config, model)
|
||||
if image_stem:
|
||||
return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
|
||||
|
||||
@classmethod
|
||||
def node_image(
|
||||
cls, core_node: Node, gui_config: GuiConfig, scale: float = 1.0
|
||||
) -> PhotoImage:
|
||||
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
|
||||
if core_node.icon:
|
||||
try:
|
||||
image = Images.create(core_node.icon, int(ICON_SIZE * scale))
|
||||
except OSError:
|
||||
logging.error("invalid icon: %s", core_node.icon)
|
||||
return image
|
||||
|
||||
@classmethod
|
||||
def is_custom(cls, node_type: NodeType, model: str) -> bool:
|
||||
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
|
||||
|
||||
@classmethod
|
||||
def get_custom_node_services(cls, gui_config: GuiConfig, name: str) -> List[str]:
|
||||
for custom_node in gui_config.nodes:
|
||||
if custom_node.name == name:
|
||||
return custom_node.services
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_image_file(cls, gui_config: GuiConfig, name: str) -> Optional[str]:
|
||||
for custom_node in gui_config.nodes:
|
||||
if custom_node.name == name:
|
||||
return custom_node.image
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def setup(cls) -> None:
|
||||
nodes = [
|
||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
||||
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
|
||||
(ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"),
|
||||
(ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"),
|
||||
(ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"),
|
||||
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None),
|
||||
(ImageEnum.LXC, NodeType.LXC, "LXC", None),
|
||||
]
|
||||
for image_enum, node_type, label, model in nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, label, model)
|
||||
cls.NODES.append(node_draw)
|
||||
cls.NODE_ICONS[(node_type, model)] = node_draw.image
|
||||
|
||||
network_nodes = [
|
||||
(ImageEnum.HUB, NodeType.HUB, "Hub"),
|
||||
(ImageEnum.SWITCH, NodeType.SWITCH, "Switch"),
|
||||
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"),
|
||||
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
||||
(ImageEnum.RJ45, NodeType.RJ45, "RJ45"),
|
||||
(ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"),
|
||||
]
|
||||
for image_enum, node_type, label in network_nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, label)
|
||||
cls.NETWORK_NODES.append(node_draw)
|
||||
cls.NODE_ICONS[(node_type, None)] = node_draw.image
|
||||
cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE)
|
||||
|
|
|
@ -48,7 +48,6 @@ class StatusBar(ttk.Frame):
|
|||
|
||||
self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
|
||||
self.zoom.grid(row=0, column=1, sticky=tk.EW)
|
||||
self.set_zoom(self.app.canvas.ratio)
|
||||
|
||||
self.cpu_label = ttk.Label(
|
||||
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE
|
||||
|
|
|
@ -7,13 +7,14 @@ from typing import TYPE_CHECKING, Callable, List, Optional
|
|||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.gui import nodeutils as nutils
|
||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||
from core.gui.dialogs.runtool import RunToolDialog
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.enums import GraphMode
|
||||
from core.gui.graph.shapeutils import ShapeType, is_marker
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
from core.gui.nodeutils import NodeDraw
|
||||
from core.gui.observers import ObserversMenu
|
||||
from core.gui.task import ProgressTask
|
||||
from core.gui.themes import Styles
|
||||
|
@ -57,11 +58,11 @@ class PickerFrame(ttk.Frame):
|
|||
image_file: str = None,
|
||||
) -> None:
|
||||
if image_enum:
|
||||
bar_image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
|
||||
image = self.app.get_icon(image_enum, PICKER_SIZE)
|
||||
bar_image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
|
||||
image = self.app.get_enum_icon(image_enum, width=PICKER_SIZE)
|
||||
else:
|
||||
bar_image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
|
||||
image = self.app.get_custom_icon(image_file, PICKER_SIZE)
|
||||
bar_image = self.app.get_file_icon(image_file, width=TOOLBAR_SIZE)
|
||||
image = self.app.get_file_icon(image_file, width=PICKER_SIZE)
|
||||
button = ttk.Button(
|
||||
self, image=image, text=label, compound=tk.TOP, style=Styles.picker_button
|
||||
)
|
||||
|
@ -92,7 +93,7 @@ class ButtonBar(ttk.Frame):
|
|||
def create_button(
|
||||
self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
|
||||
) -> ttk.Button:
|
||||
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
|
||||
image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
|
||||
button = ttk.Button(self, image=image, command=func)
|
||||
button.image = image
|
||||
button.grid(sticky=tk.EW)
|
||||
|
@ -121,7 +122,7 @@ class MarkerFrame(ttk.Frame):
|
|||
def draw(self) -> None:
|
||||
self.columnconfigure(0, weight=1)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.DELETE, 16)
|
||||
image = self.app.get_enum_icon(ImageEnum.DELETE, width=16)
|
||||
button = ttk.Button(self, image=image, width=2, command=self.click_clear)
|
||||
button.image = image
|
||||
button.grid(sticky=tk.EW, pady=self.PAD)
|
||||
|
@ -144,7 +145,8 @@ class MarkerFrame(ttk.Frame):
|
|||
Tooltip(self.color_frame, "Marker Color")
|
||||
|
||||
def click_clear(self) -> None:
|
||||
self.app.canvas.delete(tags.MARKER)
|
||||
canvas = self.app.manager.current()
|
||||
canvas.delete(tags.MARKER)
|
||||
|
||||
def click_color(self, _event: tk.Event) -> None:
|
||||
dialog = ColorPickerDialog(self.app, self.app, self.color)
|
||||
|
@ -189,8 +191,8 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
# these variables help keep track of what images being drawn so that scaling
|
||||
# is possible since PhotoImage does not have resize method
|
||||
self.current_node: NodeDraw = NodeUtils.NODES[0]
|
||||
self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0]
|
||||
self.current_node: NodeDraw = nutils.NODES[0]
|
||||
self.current_network: NodeDraw = nutils.NETWORK_NODES[0]
|
||||
self.current_annotation: ShapeType = ShapeType.MARKER
|
||||
self.annotation_enum: ImageEnum = ImageEnum.MARKER
|
||||
|
||||
|
@ -257,12 +259,12 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def draw_node_picker(self) -> None:
|
||||
self.hide_marker()
|
||||
self.app.canvas.mode = GraphMode.NODE
|
||||
self.app.canvas.node_draw = self.current_node
|
||||
self.app.manager.mode = GraphMode.NODE
|
||||
self.app.manager.node_draw = self.current_node
|
||||
self.design_frame.select_radio(self.node_button)
|
||||
self.picker = PickerFrame(self.app, self.node_button)
|
||||
# draw default nodes
|
||||
for node_draw in NodeUtils.NODES:
|
||||
for node_draw in nutils.NODES:
|
||||
func = partial(
|
||||
self.update_button, self.node_button, node_draw, NodeTypeEnum.NODE
|
||||
)
|
||||
|
@ -278,12 +280,12 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def click_selection(self) -> None:
|
||||
self.design_frame.select_radio(self.select_button)
|
||||
self.app.canvas.mode = GraphMode.SELECT
|
||||
self.app.manager.mode = GraphMode.SELECT
|
||||
self.hide_marker()
|
||||
|
||||
def click_runtime_selection(self) -> None:
|
||||
self.runtime_frame.select_radio(self.runtime_select_button)
|
||||
self.app.canvas.mode = GraphMode.SELECT
|
||||
self.app.manager.mode = GraphMode.SELECT
|
||||
self.hide_marker()
|
||||
|
||||
def click_start(self) -> None:
|
||||
|
@ -291,8 +293,8 @@ class Toolbar(ttk.Frame):
|
|||
Start session handler redraw buttons, send node and link messages to grpc
|
||||
server.
|
||||
"""
|
||||
self.app.menubar.change_menubar_item_state(is_runtime=True)
|
||||
self.app.canvas.mode = GraphMode.SELECT
|
||||
self.app.menubar.set_state(is_runtime=True)
|
||||
self.app.manager.mode = GraphMode.SELECT
|
||||
enable_buttons(self.design_frame, enabled=False)
|
||||
task = ProgressTask(
|
||||
self.app, "Start", self.app.core.start_session, self.start_callback
|
||||
|
@ -308,7 +310,9 @@ class Toolbar(ttk.Frame):
|
|||
enable_buttons(self.design_frame, enabled=True)
|
||||
if exceptions:
|
||||
message = "\n".join(exceptions)
|
||||
self.app.show_error("Start Session Error", message)
|
||||
self.app.show_exception_data(
|
||||
"Start Exception", "Session failed to start", message
|
||||
)
|
||||
|
||||
def set_runtime(self) -> None:
|
||||
enable_buttons(self.runtime_frame, enabled=True)
|
||||
|
@ -324,7 +328,7 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def click_link(self) -> None:
|
||||
self.design_frame.select_radio(self.link_button)
|
||||
self.app.canvas.mode = GraphMode.EDGE
|
||||
self.app.manager.mode = GraphMode.EDGE
|
||||
self.hide_marker()
|
||||
|
||||
def update_button(
|
||||
|
@ -337,7 +341,7 @@ class Toolbar(ttk.Frame):
|
|||
logging.debug("update button(%s): %s", button, node_draw)
|
||||
button.configure(image=image)
|
||||
button.image = image
|
||||
self.app.canvas.node_draw = node_draw
|
||||
self.app.manager.node_draw = node_draw
|
||||
if type_enum == NodeTypeEnum.NODE:
|
||||
self.current_node = node_draw
|
||||
elif type_enum == NodeTypeEnum.NETWORK:
|
||||
|
@ -348,11 +352,11 @@ class Toolbar(ttk.Frame):
|
|||
Draw the options for link-layer button.
|
||||
"""
|
||||
self.hide_marker()
|
||||
self.app.canvas.mode = GraphMode.NODE
|
||||
self.app.canvas.node_draw = self.current_network
|
||||
self.app.manager.mode = GraphMode.NODE
|
||||
self.app.manager.node_draw = self.current_network
|
||||
self.design_frame.select_radio(self.network_button)
|
||||
self.picker = PickerFrame(self.app, self.network_button)
|
||||
for node_draw in NodeUtils.NETWORK_NODES:
|
||||
for node_draw in nutils.NETWORK_NODES:
|
||||
func = partial(
|
||||
self.update_button, self.network_button, node_draw, NodeTypeEnum.NETWORK
|
||||
)
|
||||
|
@ -364,8 +368,8 @@ class Toolbar(ttk.Frame):
|
|||
Draw the options for marker button.
|
||||
"""
|
||||
self.design_frame.select_radio(self.annotation_button)
|
||||
self.app.canvas.mode = GraphMode.ANNOTATION
|
||||
self.app.canvas.annotation_type = self.current_annotation
|
||||
self.app.manager.mode = GraphMode.ANNOTATION
|
||||
self.app.manager.annotation_type = self.current_annotation
|
||||
if is_marker(self.current_annotation):
|
||||
self.show_marker()
|
||||
self.picker = PickerFrame(self.app, self.annotation_button)
|
||||
|
@ -382,7 +386,7 @@ class Toolbar(ttk.Frame):
|
|||
self.picker.show()
|
||||
|
||||
def create_observe_button(self) -> None:
|
||||
image = self.app.get_icon(ImageEnum.OBSERVE, TOOLBAR_SIZE)
|
||||
image = self.app.get_enum_icon(ImageEnum.OBSERVE, width=TOOLBAR_SIZE)
|
||||
menu_button = ttk.Menubutton(
|
||||
self.runtime_frame, image=image, direction=tk.RIGHT
|
||||
)
|
||||
|
@ -396,7 +400,7 @@ class Toolbar(ttk.Frame):
|
|||
redraw buttons on the toolbar, send node and link messages to grpc server
|
||||
"""
|
||||
logging.info("clicked stop button")
|
||||
self.app.menubar.change_menubar_item_state(is_runtime=False)
|
||||
self.app.menubar.set_state(is_runtime=False)
|
||||
self.app.core.close_mobility_players()
|
||||
enable_buttons(self.runtime_frame, enabled=False)
|
||||
task = ProgressTask(
|
||||
|
@ -406,7 +410,7 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def stop_callback(self, result: bool) -> None:
|
||||
self.set_design()
|
||||
self.app.canvas.stopped_session()
|
||||
self.app.manager.stopped_session()
|
||||
|
||||
def update_annotation(
|
||||
self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage
|
||||
|
@ -414,7 +418,7 @@ class Toolbar(ttk.Frame):
|
|||
logging.debug("clicked annotation")
|
||||
self.annotation_button.configure(image=image)
|
||||
self.annotation_button.image = image
|
||||
self.app.canvas.annotation_type = shape_type
|
||||
self.app.manager.annotation_type = shape_type
|
||||
self.current_annotation = shape_type
|
||||
self.annotation_enum = image_enum
|
||||
if is_marker(shape_type):
|
||||
|
@ -435,8 +439,8 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def click_marker_button(self) -> None:
|
||||
self.runtime_frame.select_radio(self.runtime_marker_button)
|
||||
self.app.canvas.mode = GraphMode.ANNOTATION
|
||||
self.app.canvas.annotation_type = ShapeType.MARKER
|
||||
self.app.manager.mode = GraphMode.ANNOTATION
|
||||
self.app.manager.annotation_type = ShapeType.MARKER
|
||||
self.show_marker()
|
||||
|
||||
def scale_button(
|
||||
|
@ -444,9 +448,9 @@ class Toolbar(ttk.Frame):
|
|||
) -> None:
|
||||
image = None
|
||||
if image_enum:
|
||||
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
|
||||
image = self.app.get_enum_icon(image_enum, width=TOOLBAR_SIZE)
|
||||
elif image_file:
|
||||
image = self.app.get_custom_icon(image_file, TOOLBAR_SIZE)
|
||||
image = self.app.get_file_icon(image_file, width=TOOLBAR_SIZE)
|
||||
if image:
|
||||
button.config(image=image)
|
||||
button.image = image
|
||||
|
|
|
@ -10,9 +10,9 @@ from pyproj import Transformer
|
|||
|
||||
from core.emulator.enumerations import RegisterTlvs
|
||||
|
||||
SCALE_FACTOR = 100.0
|
||||
CRS_WGS84 = 4326
|
||||
CRS_PROJ = 3857
|
||||
SCALE_FACTOR: float = 100.0
|
||||
CRS_WGS84: int = 4326
|
||||
CRS_PROJ: int = 3857
|
||||
|
||||
|
||||
class GeoLocation:
|
||||
|
|
|
@ -921,8 +921,6 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
"""
|
||||
super().__init__(session, _id)
|
||||
self.file: Optional[str] = None
|
||||
self.refresh_ms: Optional[int] = None
|
||||
self.loop: Optional[bool] = None
|
||||
self.autostart: Optional[str] = None
|
||||
self.nodemap: Dict[int, int] = {}
|
||||
self.script_start: Optional[str] = None
|
||||
|
@ -937,7 +935,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
self.file,
|
||||
)
|
||||
self.refresh_ms = int(config["refresh_ms"])
|
||||
self.loop = config["loop"].lower() == "on"
|
||||
self.loop = config["loop"] == "1"
|
||||
self.autostart = config["autostart"]
|
||||
self.parsemap(config["map"])
|
||||
self.script_start = config["script_start"]
|
||||
|
|
|
@ -159,7 +159,7 @@ class NodeBase(abc.ABC):
|
|||
ifaces = []
|
||||
for iface_id in sorted(self.ifaces):
|
||||
iface = self.ifaces[iface_id]
|
||||
if not control and getattr(iface, "control", False):
|
||||
if not control and iface.control:
|
||||
continue
|
||||
ifaces.append(iface)
|
||||
return ifaces
|
||||
|
|
|
@ -73,6 +73,7 @@ class CoreInterface:
|
|||
self.net_client: LinuxNetClient = get_net_client(
|
||||
self.session.use_ovs(), self.host_cmd
|
||||
)
|
||||
self.control: bool = False
|
||||
|
||||
def host_cmd(
|
||||
self,
|
||||
|
|
|
@ -4,7 +4,7 @@ sdt.py: Scripted Display Tool (SDT3D) helper
|
|||
|
||||
import logging
|
||||
import socket
|
||||
from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple
|
||||
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from core.constants import CORE_CONF_DIR, CORE_DATA_DIR
|
||||
|
@ -26,11 +26,12 @@ def get_link_id(node1_id: int, node2_id: int, network_id: int) -> str:
|
|||
return link_id
|
||||
|
||||
|
||||
CORE_LAYER = "CORE"
|
||||
NODE_LAYER = "CORE::Nodes"
|
||||
LINK_LAYER = "CORE::Links"
|
||||
CORE_LAYERS = [CORE_LAYER, LINK_LAYER, NODE_LAYER]
|
||||
DEFAULT_LINK_COLOR = "red"
|
||||
CORE_LAYER: str = "CORE"
|
||||
NODE_LAYER: str = "CORE::Nodes"
|
||||
LINK_LAYER: str = "CORE::Links"
|
||||
WIRED_LINK_LAYER: str = f"{LINK_LAYER}::wired"
|
||||
CORE_LAYERS: List[str] = [CORE_LAYER, LINK_LAYER, NODE_LAYER, WIRED_LINK_LAYER]
|
||||
DEFAULT_LINK_COLOR: str = "red"
|
||||
|
||||
|
||||
class Sdt:
|
||||
|
@ -323,7 +324,7 @@ class Sdt:
|
|||
if all([lat is not None, lon is not None, alt is not None]):
|
||||
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||
self.cmd(f"node {node.id} {pos}")
|
||||
elif node_data.message_type == 0:
|
||||
elif node_data.message_type == MessageFlags.NONE:
|
||||
lat, lon, alt = self.session.location.getgeo(x, y, 0)
|
||||
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
|
||||
self.cmd(f"node {node.id} {pos}")
|
||||
|
@ -365,13 +366,10 @@ class Sdt:
|
|||
color = self.session.get_link_color(network_id)
|
||||
line = f"{color},2"
|
||||
link_id = get_link_id(node1_id, node2_id, network_id)
|
||||
layer = LINK_LAYER
|
||||
if network_id:
|
||||
node = self.session.nodes.get(network_id)
|
||||
if node:
|
||||
network_name = node.name
|
||||
layer = f"{layer}::{network_name}"
|
||||
self.network_layers.add(layer)
|
||||
layer = self.get_network_layer(network_id)
|
||||
else:
|
||||
layer = WIRED_LINK_LAYER
|
||||
link_label = ""
|
||||
if label:
|
||||
link_label = f'linklabel on,"{label}"'
|
||||
|
@ -380,6 +378,15 @@ class Sdt:
|
|||
f"{link_label}"
|
||||
)
|
||||
|
||||
def get_network_layer(self, network_id: int) -> str:
|
||||
node = self.session.nodes.get(network_id)
|
||||
if node:
|
||||
layer = f"{LINK_LAYER}::{node.name}"
|
||||
self.network_layers.add(layer)
|
||||
else:
|
||||
layer = WIRED_LINK_LAYER
|
||||
return layer
|
||||
|
||||
def delete_link(self, node1_id: int, node2_id: int, network_id: int = None) -> None:
|
||||
"""
|
||||
Handle deleting a link in SDT.
|
||||
|
|
|
@ -66,7 +66,7 @@ class FRRZebra(CoreService):
|
|||
for iface in node.get_ifaces():
|
||||
cfg += "interface %s\n" % iface.name
|
||||
# include control interfaces in addressing but not routing daemons
|
||||
if hasattr(iface, "control") and iface.control is True:
|
||||
if iface.control:
|
||||
cfg += " "
|
||||
cfg += "\n ".join(map(cls.addrstr, iface.ips()))
|
||||
cfg += "\n"
|
||||
|
|
|
@ -63,7 +63,7 @@ class Zebra(CoreService):
|
|||
for iface in node.get_ifaces():
|
||||
cfg += "interface %s\n" % iface.name
|
||||
# include control interfaces in addressing but not routing daemons
|
||||
if getattr(iface, "control", False):
|
||||
if iface.control:
|
||||
cfg += " "
|
||||
cfg += "\n ".join(map(cls.addrstr, iface.ips()))
|
||||
cfg += "\n"
|
||||
|
|
|
@ -581,7 +581,7 @@ if [ "x$1" = "xstart" ]; then
|
|||
|
||||
"""
|
||||
for iface in node.get_ifaces():
|
||||
if hasattr(iface, "control") and iface.control is True:
|
||||
if iface.control:
|
||||
cfg += "# "
|
||||
redir = "< /dev/null"
|
||||
cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % (
|
||||
|
|
|
@ -13,7 +13,7 @@ from core.errors import CoreXmlError
|
|||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.lxd import LxcNode
|
||||
from core.nodes.network import CtrlNet, WlanNode
|
||||
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -253,15 +253,13 @@ class DeviceElement(NodeElement):
|
|||
class NetworkElement(NodeElement):
|
||||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||
super().__init__(session, node, "network")
|
||||
model = getattr(self.node, "model", None)
|
||||
if model:
|
||||
add_attribute(self.element, "model", model.name)
|
||||
mobility = getattr(self.node, "mobility", None)
|
||||
if mobility:
|
||||
add_attribute(self.element, "mobility", mobility.name)
|
||||
grekey = getattr(self.node, "grekey", None)
|
||||
if grekey and grekey is not None:
|
||||
add_attribute(self.element, "grekey", grekey)
|
||||
if isinstance(self.node, (WlanNode, EmaneNet)):
|
||||
if self.node.model:
|
||||
add_attribute(self.element, "model", self.node.model.name)
|
||||
if self.node.mobility:
|
||||
add_attribute(self.element, "mobility", self.node.mobility.name)
|
||||
if isinstance(self.node, GreTapBridge):
|
||||
add_attribute(self.element, "grekey", self.node.grekey)
|
||||
self.add_type()
|
||||
|
||||
def add_type(self) -> None:
|
||||
|
|
|
@ -49,3 +49,6 @@ iface1 = iface_helper.create_iface(n1_id, 0)
|
|||
core.add_link(session_id, n1_id, emane_id, iface1)
|
||||
iface1 = iface_helper.create_iface(n2_id, 0)
|
||||
core.add_link(session_id, n2_id, emane_id, iface1)
|
||||
|
||||
# change session state
|
||||
core.set_session_state(session_id, SessionState.INSTANTIATION)
|
||||
|
|
|
@ -752,6 +752,7 @@ message Node {
|
|||
Geo geo = 12;
|
||||
string dir = 13;
|
||||
string channel = 14;
|
||||
int32 canvas = 15;
|
||||
}
|
||||
|
||||
message Link {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "core"
|
||||
version = "7.4.0"
|
||||
version = "7.5.0"
|
||||
description = "CORE Common Open Research Emulator"
|
||||
authors = ["Boeing Research and Technology"]
|
||||
license = "BSD-2-Clause"
|
||||
|
|
|
@ -262,6 +262,15 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None:
|
|||
print_iface(iface)
|
||||
|
||||
|
||||
@coreclient
|
||||
def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
|
||||
response = core.delete_session(args.id)
|
||||
if args.json:
|
||||
print_json(response)
|
||||
else:
|
||||
print(f"delete session({args.id}): {response.result}")
|
||||
|
||||
|
||||
@coreclient
|
||||
def add_node(core: CoreGrpcClient, args: Namespace) -> None:
|
||||
session_id = get_current_session(core, args.session)
|
||||
|
@ -374,6 +383,19 @@ def delete_link(core: CoreGrpcClient, args: Namespace) -> None:
|
|||
print(f"delete link: {response.result}")
|
||||
|
||||
|
||||
def setup_sessions_parser(parent: _SubParsersAction) -> None:
|
||||
parser = parent.add_parser("session", help="session interactions")
|
||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True)
|
||||
subparsers = parser.add_subparsers(help="session commands")
|
||||
subparsers.required = True
|
||||
subparsers.dest = "command"
|
||||
|
||||
delete_parser = subparsers.add_parser("delete", help="delete a session")
|
||||
delete_parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||
delete_parser.set_defaults(func=delete_session)
|
||||
|
||||
|
||||
def setup_node_parser(parent: _SubParsersAction) -> None:
|
||||
parser = parent.add_parser("node", help="node interactions")
|
||||
parser.formatter_class = ArgumentDefaultsHelpFormatter
|
||||
|
@ -528,6 +550,7 @@ def main() -> None:
|
|||
subparsers = parser.add_subparsers(help="supported commands")
|
||||
subparsers.required = True
|
||||
subparsers.dest = "command"
|
||||
setup_sessions_parser(subparsers)
|
||||
setup_node_parser(subparsers)
|
||||
setup_link_parser(subparsers)
|
||||
setup_query_parser(subparsers)
|
||||
|
|
|
@ -3,9 +3,8 @@ import argparse
|
|||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.gui import appconfig
|
||||
from core.gui import appconfig, images
|
||||
from core.gui.app import Application
|
||||
from core.gui.images import Images
|
||||
|
||||
if __name__ == "__main__":
|
||||
# parse flags
|
||||
|
@ -28,6 +27,6 @@ if __name__ == "__main__":
|
|||
logging.getLogger("PIL").setLevel(logging.ERROR)
|
||||
|
||||
# start app
|
||||
Images.load_all()
|
||||
images.load_all()
|
||||
app = Application(args.proxy, args.session)
|
||||
app.mainloop()
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
* Links are created using Linux bridges and virtual ethernet peers
|
||||
* Packets sent over links are manipulated using traffic control
|
||||
* Controlled via the CORE GUI
|
||||
* Provides both a custo TLV API and gRPC API
|
||||
* Provides both a custom TLV API and gRPC API
|
||||
* Python program that leverages a small C binary for node creation
|
||||
* core-gui
|
||||
* GUI and daemon communicate over the custom TLV API
|
||||
|
|
Loading…
Reference in a new issue