diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 145f7029..d27da28e 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -67,6 +67,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption image=node_proto.image, services=node_proto.services, config_services=node_proto.config_services, + canvas=node_proto.canvas, ) if node_proto.emane: options.emane = node_proto.emane @@ -290,6 +291,7 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: config_services=config_services, dir=node_dir, channel=channel, + canvas=node.canvas, ) diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index 8cc55446..1ef43be2 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -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, ) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 9264ce84..e0f1c1a4 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -591,7 +591,6 @@ class Session: :raises core.CoreError: when node to update does not exist """ node = self.get_node(node_id, NodeBase) - node.canvas = options.canvas node.icon = options.icon self.set_node_position(node, options) self.sdt.edit_node(node, options.lon, options.lat, options.alt) diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index be744bb4..d404d923 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -13,7 +13,7 @@ from core.gui.coreclient import CoreClient from core.gui.dialogs.error import ErrorDialog from core.gui.frames.base import InfoFrameBase from core.gui.frames.default import DefaultInfoFrame -from core.gui.graph.graph import CanvasGraph +from core.gui.graph.manager import CanvasManager from core.gui.images import ImageEnum, Images from core.gui.menubar import Menubar from core.gui.nodeutils import NodeUtils @@ -35,7 +35,7 @@ class Application(ttk.Frame): self.menubar: Optional[Menubar] = None self.toolbar: Optional[Toolbar] = None self.right_frame: Optional[ttk.Frame] = None - self.canvas: Optional[CanvasGraph] = None + self.manager: Optional[CanvasManager] = None self.statusbar: Optional[StatusBar] = None self.progress: Optional[Progressbar] = None self.infobar: Optional[ttk.Frame] = None @@ -136,20 +136,8 @@ class Application(ttk.Frame): label.grid(sticky=tk.EW, pady=PADY) def draw_canvas(self) -> None: - canvas_frame = ttk.Frame(self.right_frame) - canvas_frame.rowconfigure(0, weight=1) - canvas_frame.columnconfigure(0, weight=1) - canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1) - self.canvas = CanvasGraph(canvas_frame, self, self.core) - self.canvas.grid(sticky=tk.NSEW) - scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) - scroll_y.grid(row=0, column=1, sticky=tk.NS) - scroll_x = ttk.Scrollbar( - canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview - ) - scroll_x.grid(row=1, column=0, sticky=tk.EW) - self.canvas.configure(xscrollcommand=scroll_x.set) - self.canvas.configure(yscrollcommand=scroll_y.set) + self.manager = CanvasManager(self.right_frame, self, self.core) + self.manager.notebook.grid(sticky=tk.NSEW) def draw_status(self) -> None: self.statusbar = StatusBar(self.right_frame, self) @@ -201,8 +189,10 @@ class Application(ttk.Frame): def joined_session_update(self) -> None: if self.core.is_runtime(): + self.menubar.set_state(is_runtime=True) self.toolbar.set_runtime() else: + self.menubar.set_state(is_runtime=False) self.toolbar.set_design() def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage: diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 6bc213eb..3f629a80 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -185,7 +185,8 @@ class GuiConfig(yaml.YAMLObject): def copy_files(current_path: Path, new_path: Path) -> None: for current_file in current_path.glob("*"): new_file = new_path.joinpath(current_file.name) - shutil.copy(current_file, new_file) + if not new_file.exists(): + shutil.copy(current_file, new_file) def find_terminal() -> Optional[str]: @@ -197,16 +198,14 @@ def find_terminal() -> Optional[str]: def check_directory() -> None: - if HOME_PATH.exists(): - return - HOME_PATH.mkdir() - BACKGROUNDS_PATH.mkdir() - CUSTOM_EMANE_PATH.mkdir() - CUSTOM_SERVICE_PATH.mkdir() - ICONS_PATH.mkdir() - MOBILITY_PATH.mkdir() - XMLS_PATH.mkdir() - SCRIPT_PATH.mkdir() + HOME_PATH.mkdir(exist_ok=True) + BACKGROUNDS_PATH.mkdir(exist_ok=True) + CUSTOM_EMANE_PATH.mkdir(exist_ok=True) + CUSTOM_SERVICE_PATH.mkdir(exist_ok=True) + ICONS_PATH.mkdir(exist_ok=True) + MOBILITY_PATH.mkdir(exist_ok=True) + XMLS_PATH.mkdir(exist_ok=True) + SCRIPT_PATH.mkdir(exist_ok=True) copy_files(LOCAL_ICONS_PATH, ICONS_PATH) copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 01225c6b..31b312e6 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,7 +6,6 @@ import json import logging import os import tkinter as tk -from pathlib import Path from tkinter import messagebox from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple @@ -40,16 +39,14 @@ from core.api.grpc.wrappers import ( SessionState, ThroughputsEvent, ) -from core.gui import appconfig -from core.gui.appconfig import BACKGROUNDS_PATH, XMLS_PATH, CoreServer, Observer +from core.gui.appconfig import XMLS_PATH, CoreServer, Observer from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog from core.gui.graph.edges import CanvasEdge from core.gui.graph.node import CanvasNode -from core.gui.graph.shape import AnnotationData, Shape -from core.gui.graph.shapeutils import ShapeType +from core.gui.graph.shape import Shape from core.gui.interface import InterfaceManager from core.gui.nodeutils import NodeDraw, NodeUtils @@ -207,27 +204,26 @@ class CoreClient: canvas_node2 = self.canvas_nodes[node2_id] if event.link.type == LinkType.WIRELESS: if event.message_type == MessageType.ADD: - self.app.canvas.add_wireless_edge( + self.app.manager.add_wireless_edge( canvas_node1, canvas_node2, event.link ) elif event.message_type == MessageType.DELETE: - self.app.canvas.delete_wireless_edge( + self.app.manager.delete_wireless_edge( canvas_node1, canvas_node2, event.link ) elif event.message_type == MessageType.NONE: - self.app.canvas.update_wireless_edge( + self.app.manager.update_wireless_edge( canvas_node1, canvas_node2, event.link ) else: logging.warning("unknown link event: %s", event) else: if event.message_type == MessageType.ADD: - self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link) - self.app.canvas.organize() + self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link) elif event.message_type == MessageType.DELETE: - self.app.canvas.delete_wired_edge(event.link) + self.app.manager.delete_wired_edge(event.link) elif event.message_type == MessageType.NONE: - self.app.canvas.update_wired_edge(event.link) + self.app.manager.update_wired_edge(event.link) else: logging.warning("unknown link event: %s", event) @@ -243,13 +239,13 @@ class CoreClient: canvas_node.update_icon(node.icon) elif event.message_type == MessageType.DELETE: canvas_node = self.canvas_nodes[node.id] - self.app.canvas.clear_selection() - self.app.canvas.select_object(canvas_node.id) - self.app.canvas.delete_selected_objects() + canvas_node.canvas.clear_selection() + canvas_node.canvas.select_object(canvas_node.id) + canvas_node.canvas.delete_selected_objects() elif event.message_type == MessageType.ADD: if node.id in self.session.nodes: logging.error("core node already exists: %s", node) - self.app.canvas.add_core_node(node) + self.app.manager.add_core_node(node) else: logging.warning("unknown node event: %s", event) @@ -262,7 +258,7 @@ class CoreClient: if self.handling_throughputs: self.handling_throughputs.cancel() self.handling_throughputs = None - self.app.canvas.clear_throughputs() + self.app.manager.clear_throughputs() def cancel_events(self) -> None: if self.handling_events: @@ -293,7 +289,7 @@ class CoreClient: ) return logging.debug("handling throughputs event: %s", event) - self.app.after(0, self.app.canvas.set_throughputs, event) + self.app.after(0, self.app.manager.set_throughputs, event) def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None: self.app.after(0, self.app.statusbar.set_cpu, event.usage) @@ -315,9 +311,7 @@ class CoreClient: self.session.id, self.handle_events ) self.ifaces_manager.joined(self.session.links) - self.app.canvas.reset_and_redraw(self.session) - self.parse_metadata() - self.app.canvas.organize() + self.app.manager.join(self.session) if self.is_runtime(): self.show_mobility_players() self.app.after(0, self.app.joined_session_update) @@ -334,23 +328,7 @@ class CoreClient: logging.debug("canvas metadata: %s", canvas_config) if canvas_config: canvas_config = json.loads(canvas_config) - gridlines = canvas_config.get("gridlines", True) - self.app.canvas.show_grid.set(gridlines) - fit_image = canvas_config.get("fit_image", False) - self.app.canvas.adjust_to_dim.set(fit_image) - wallpaper_style = canvas_config.get("wallpaper-style", 1) - self.app.canvas.scale_option.set(wallpaper_style) - width = self.app.guiconfig.preferences.width - height = self.app.guiconfig.preferences.height - dimensions = canvas_config.get("dimensions", [width, height]) - self.app.canvas.redraw_canvas(dimensions) - wallpaper = canvas_config.get("wallpaper") - if wallpaper: - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) - else: - self.app.canvas.redraw_canvas() - self.app.canvas.set_wallpaper(None) + self.app.manager.parse_metadata(canvas_config) # load saved shapes shapes_config = config.get("shapes") @@ -358,28 +336,7 @@ class CoreClient: shapes_config = json.loads(shapes_config) for shape_config in shapes_config: logging.debug("loading shape: %s", shape_config) - shape_type = shape_config["type"] - try: - shape_type = ShapeType(shape_type) - coords = shape_config["iconcoords"] - data = AnnotationData( - shape_config["label"], - shape_config["fontfamily"], - shape_config["fontsize"], - shape_config["labelcolor"], - shape_config["color"], - shape_config["border"], - shape_config["width"], - shape_config["bold"], - shape_config["italic"], - shape_config["underline"], - ) - shape = Shape( - self.app, self.app.canvas, shape_type, *coords, data=data - ) - self.app.canvas.shapes[shape.id] = shape - except ValueError: - logging.exception("unknown shape: %s", shape_type) + Shape.from_metadata(self.app, shape_config) # load edges config edges_config = config.get("edges") @@ -392,6 +349,17 @@ class CoreClient: edge.color = edge_config["color"] edge.redraw() + # read hidden nodes + hidden = config.get("hidden") + if hidden: + hidden = json.loads(hidden) + for _id in hidden: + canvas_node = self.canvas_nodes.get(_id) + if canvas_node: + canvas_node.hide() + else: + logging.warning("invalid node to hide: %s", _id) + def create_new_session(self) -> None: """ Create a new session @@ -557,26 +525,14 @@ class CoreClient: def set_metadata(self) -> None: # create canvas data - wallpaper_path = None - if self.app.canvas.wallpaper_file: - wallpaper = Path(self.app.canvas.wallpaper_file) - if BACKGROUNDS_PATH == wallpaper.parent: - wallpaper_path = wallpaper.name - else: - wallpaper_path = str(wallpaper) - canvas_config = { - "wallpaper": wallpaper_path, - "wallpaper-style": self.app.canvas.scale_option.get(), - "gridlines": self.app.canvas.show_grid.get(), - "fit_image": self.app.canvas.adjust_to_dim.get(), - "dimensions": self.app.canvas.current_dimensions, - } + canvas_config = self.app.manager.get_metadata() canvas_config = json.dumps(canvas_config) # create shapes data shapes = [] - for shape in self.app.canvas.shapes.values(): - shapes.append(shape.metadata()) + for canvas in self.app.manager.all(): + for shape in canvas.shapes.values(): + shapes.append(shape.metadata()) shapes = json.dumps(shapes) # create edges config @@ -588,8 +544,14 @@ class CoreClient: edges_config.append(edge_config) edges_config = json.dumps(edges_config) + # create hidden metadata + hidden = [x.core_node.id for x in self.canvas_nodes.values() if x.hidden] + hidden = json.dumps(hidden) + # save metadata - metadata = dict(canvas=canvas_config, shapes=shapes, edges=edges_config) + metadata = dict( + canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden + ) response = self.client.set_session_metadata(self.session.id, metadata) logging.debug("set session metadata %s, result: %s", metadata, response) diff --git a/daemon/core/gui/data/icons/shadow.png b/daemon/core/gui/data/icons/shadow.png new file mode 100644 index 00000000..6d6f3571 Binary files /dev/null and b/daemon/core/gui/data/icons/shadow.png differ diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index e50bf986..8155cb57 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -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() diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 629f9f36..505b6b74 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -23,7 +23,7 @@ class CanvasWallpaperDialog(Dialog): create an instance of CanvasWallpaper object """ super().__init__(app, "Canvas Background") - self.canvas: CanvasGraph = self.app.canvas + self.canvas: CanvasGraph = self.app.manager.current() self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get()) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar( value=self.canvas.adjust_to_dim.get() @@ -161,7 +161,6 @@ class CanvasWallpaperDialog(Dialog): def click_apply(self) -> None: self.canvas.scale_option.set(self.scale_option.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - self.canvas.show_grid.click_handler() filename = self.filename.get() if not filename: filename = None diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 6bfac47b..3b899ef8 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -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) diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 6cb22862..e8fd2a07 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -70,10 +70,10 @@ class LinkConfigurationDialog(Dialog): def draw(self) -> None: self.top.columnconfigure(0, weight=1) - src_label = self.app.canvas.nodes[self.edge.src].core_node.name + src_label = self.edge.src.core_node.name if self.edge.link.iface1: src_label += f":{self.edge.link.iface1.name}" - dst_label = self.app.canvas.nodes[self.edge.dst].core_node.name + dst_label = self.edge.dst.core_node.name if self.edge.link.iface2: dst_label += f":{self.edge.link.iface2.name}" label = ttk.Label( @@ -316,10 +316,8 @@ class LinkConfigurationDialog(Dialog): """ populate link config to the table """ - width = self.app.canvas.itemcget(self.edge.id, "width") - self.width.set(width) - color = self.app.canvas.itemcget(self.edge.id, "fill") - self.color.set(color) + self.width.set(self.edge.width) + self.color.set(self.edge.color) link = self.edge.link if link.options: self.bandwidth.set(str(link.options.bandwidth)) diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index d0c58dfa..f78e5c48 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -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() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 71a33fd6..6d818f94 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -199,7 +199,10 @@ class SessionsDialog(Dialog): logging.debug("delete session: %s", self.selected_session) self.tree.delete(self.selected_id) self.app.core.delete_session(self.selected_session) - if self.selected_session == self.app.core.session.id: + session_id = None + if self.app.core.session: + session_id = self.app.core.session.id + if self.selected_session == session_id: self.click_new() self.destroy() self.click_select() diff --git a/daemon/core/gui/dialogs/shapemod.py b/daemon/core/gui/dialogs/shapemod.py index 255092ec..d0e200ee 100644 --- a/daemon/core/gui/dialogs/shapemod.py +++ b/daemon/core/gui/dialogs/shapemod.py @@ -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 diff --git a/daemon/core/gui/dialogs/throughput.py b/daemon/core/gui/dialogs/throughput.py index 0b59a6ac..493d4da4 100644 --- a/daemon/core/gui/dialogs/throughput.py +++ b/daemon/core/gui/dialogs/throughput.py @@ -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() diff --git a/daemon/core/gui/dialogs/wlanconfig.py b/daemon/core/gui/dialogs/wlanconfig.py index 05362cc6..237ca8a5 100644 --- a/daemon/core/gui/dialogs/wlanconfig.py +++ b/daemon/core/gui/dialogs/wlanconfig.py @@ -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 diff --git a/daemon/core/gui/frames/link.py b/daemon/core/gui/frames/link.py index 086f7ca8..bde0aec8 100644 --- a/daemon/core/gui/frames/link.py +++ b/daemon/core/gui/frames/link.py @@ -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) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 216fc7f2..bf5ecbc1 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -1,18 +1,25 @@ +import functools import logging import math import tkinter as tk -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple, Union + +from PIL.ImageTk import PhotoImage from core.api.grpc.wrappers import Interface, Link from core.gui import themes from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags -from core.gui.nodeutils import NodeUtils +from core.gui.images import ImageEnum +from core.gui.nodeutils import ICON_SIZE from core.gui.utils import bandwidth_text, delay_jitter_text if TYPE_CHECKING: + from core.gui.app import Application from core.gui.graph.graph import CanvasGraph + from core.gui.graph.manager import CanvasManager + from core.gui.graph.node import CanvasNode TEXT_DISTANCE: int = 60 EDGE_WIDTH: int = 3 @@ -33,6 +40,19 @@ def create_edge_token(link: Link) -> str: return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}" +def node_label_positions( + src_x: int, src_y: int, dst_x: int, dst_y: int +) -> Tuple[Tuple[float, float], Tuple[float, float]]: + v_x, v_y = dst_x - src_x, dst_y - src_y + v_len = math.sqrt(v_x ** 2 + v_y ** 2) + if v_len == 0: + u_x, u_y = 0.0, 0.0 + else: + u_x, u_y = v_x / v_len, v_y / v_len + offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y + return (src_x + offset_x, src_y + offset_y), (dst_x - offset_x, dst_y - offset_y) + + def arc_edges(edges) -> None: if not edges: return @@ -62,24 +82,114 @@ def arc_edges(edges) -> None: edge.redraw() +class ShadowNode: + def __init__( + self, app: "Application", canvas: "CanvasGraph", node: "CanvasNode" + ) -> None: + self.app: "Application" = app + self.canvas: "CanvasGraph" = canvas + self.node: "CanvasNode" = node + self.id: Optional[int] = None + self.text_id: Optional[int] = None + self.image: PhotoImage = self.app.get_icon(ImageEnum.SHADOW, ICON_SIZE) + self.draw() + + def draw(self) -> None: + x, y = self.node.position() + self.id: int = self.canvas.create_image( + x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE + ) + self.text_id = self.canvas.create_text( + x, + y + 20, + text=f"{self.node.get_label()} [{self.node.canvas.id}]", + tags=tags.NODE_LABEL, + font=self.app.icon_text_font, + fill="#0000CD", + state=self.app.manager.show_node_labels.state(), + justify=tk.CENTER, + ) + self.canvas.shadow_nodes[self.id] = self + self.canvas.shadow_core_nodes[self.node.core_node.id] = self + + def position(self) -> Tuple[int, int]: + return self.canvas.coords(self.id) + + def should_delete(self) -> bool: + for edge in self.node.edges: + other_node = edge.src + if self.node == edge.src: + other_node = edge.dst + if not other_node.is_wireless() and other_node.canvas == self.canvas: + return False + return True + + def motion(self, x_offset, y_offset) -> None: + original_position = self.position() + self.canvas.move(self.id, x_offset, y_offset) + + # check new position + bbox = self.canvas.bbox(self.id) + if not self.canvas.valid_position(*bbox): + self.canvas.coords(self.id, original_position) + return + + # move text and selection box + self.canvas.move(self.text_id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) + + # move edges + for edge in self.node.edges: + edge.move_shadow(self) + for edge in self.node.wireless_edges: + edge.move_shadow(self) + + def delete(self): + self.canvas.shadow_nodes.pop(self.id, None) + self.canvas.shadow_core_nodes.pop(self.node.core_node.id, None) + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) + + def hide(self) -> None: + self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self.canvas.itemconfig(self.text_id, state=tk.HIDDEN) + + def show(self) -> None: + self.canvas.itemconfig(self.id, state=tk.NORMAL) + self.canvas.itemconfig(self.text_id, state=tk.NORMAL) + + class Edge: tag: str = tags.EDGE - def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: - self.canvas = canvas + def __init__( + self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None + ) -> None: + self.app: "Application" = app + self.manager: CanvasManager = app.manager self.id: Optional[int] = None - self.src: int = src - self.dst: int = dst + self.id2: Optional[int] = None + self.src: "CanvasNode" = src + self.src_shadow: Optional[ShadowNode] = None + self.dst: Optional["CanvasNode"] = dst + self.dst_shadow: Optional[ShadowNode] = None self.arc: int = 0 self.token: Optional[str] = None self.src_label: Optional[int] = None + self.src_label2: Optional[int] = None self.middle_label: Optional[int] = None + self.middle_label2: Optional[int] = None self.dst_label: Optional[int] = None + self.dst_label2: Optional[int] = None self.color: str = EDGE_COLOR self.width: int = EDGE_WIDTH + self.linked_wireless: bool = False + self.hidden: bool = False + if self.dst: + self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless() def scaled_width(self) -> float: - return self.width * self.canvas.app.app_scale + return self.width * self.app.app_scale def _get_arcpoint( self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] @@ -110,11 +220,57 @@ class Edge: arc_y = (perp_m * arc_x) + b return arc_x, arc_y - def draw( - self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str - ) -> None: + def arc_common_edges(self) -> None: + common_edges = list(self.src.edges & self.dst.edges) + common_edges += list(self.src.wireless_edges & self.dst.wireless_edges) + arc_edges(common_edges) + + def has_shadows(self) -> bool: + # still drawing + if not self.dst: + return False + return self.src.canvas != self.dst.canvas + + def draw(self, state: str) -> None: + if not self.has_shadows(): + dst = self.dst if self.dst else self.src + self.id = self.draw_edge(self.src.canvas, self.src, dst, state) + elif self.linked_wireless: + if self.src.is_wireless(): + self.src_shadow = self.dst.canvas.get_shadow(self.src) + self.id2 = self.draw_edge( + self.dst.canvas, self.src_shadow, self.dst, state + ) + if self.dst.is_wireless(): + self.dst_shadow = self.src.canvas.get_shadow(self.dst) + self.id = self.draw_edge( + self.src.canvas, self.src, self.dst_shadow, state + ) + else: + # draw shadow nodes and 2 lines + self.src_shadow = self.dst.canvas.get_shadow(self.src) + self.dst_shadow = self.src.canvas.get_shadow(self.dst) + self.id = self.draw_edge(self.src.canvas, self.src, self.dst_shadow, state) + self.id2 = self.draw_edge(self.dst.canvas, self.src_shadow, self.dst, state) + self.src.canvas.organize() + self.dst.canvas.organize() + logging.info( + "drawed edge: src shadow(%s) dst shadow(%s)", + self.src_shadow, + self.dst_shadow, + ) + + def draw_edge( + self, + canvas: "CanvasGraph", + src: Union["CanvasNode", "ShadowNode"], + dst: Union["CanvasNode", "ShadowNode"], + state: str, + ) -> int: + src_pos = src.position() + dst_pos = dst.position() arc_pos = self._get_arcpoint(src_pos, dst_pos) - self.id = self.canvas.create_line( + return canvas.create_line( *src_pos, *arc_pos, *dst_pos, @@ -126,112 +282,243 @@ class Edge: ) def redraw(self) -> None: - self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) - src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) - src_pos = src_x, src_y - self.move_src(src_pos) - - def middle_label_pos(self) -> Tuple[float, float]: - _, _, x, y, _, _ = self.canvas.coords(self.id) - return x, y + self.src.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) + self.move_src() + if self.id2: + self.dst.canvas.itemconfig( + self.id2, width=self.scaled_width(), fill=self.color + ) + self.move_dst() def middle_label_text(self, text: str) -> None: if self.middle_label is None: - x, y = self.middle_label_pos() - self.middle_label = self.canvas.create_text( + _, _, x, y, _, _ = self.src.canvas.coords(self.id) + self.middle_label = self.src.canvas.create_text( x, y, - font=self.canvas.app.edge_font, + font=self.app.edge_font, text=text, tags=tags.LINK_LABEL, justify=tk.CENTER, - state=self.canvas.show_link_labels.state(), + state=self.manager.show_link_labels.state(), ) + if self.id2: + _, _, x, y, _, _ = self.dst.canvas.coords(self.id2) + self.middle_label2 = self.dst.canvas.create_text( + x, + y, + font=self.app.edge_font, + text=text, + tags=tags.LINK_LABEL, + justify=tk.CENTER, + state=self.manager.show_link_labels.state(), + ) else: - self.canvas.itemconfig(self.middle_label, text=text) + self.src.canvas.itemconfig(self.middle_label, text=text) + if self.middle_label2: + self.dst.canvas.itemconfig(self.middle_label2, text=text) def clear_middle_label(self) -> None: - self.canvas.delete(self.middle_label) + self.src.canvas.delete(self.middle_label) self.middle_label = None - - def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: - src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id) - v_x, v_y = dst_x - src_x, dst_y - src_y - v_len = math.sqrt(v_x ** 2 + v_y ** 2) - if v_len == 0: - u_x, u_y = 0.0, 0.0 - else: - u_x, u_y = v_x / v_len, v_y / v_len - offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y - return ( - (src_x + offset_x, src_y + offset_y), - (dst_x - offset_x, dst_y - offset_y), - ) + if self.middle_label2: + self.dst.canvas.delete(self.middle_label2) + self.middle_label2 = None def src_label_text(self, text: str) -> None: - if self.src_label is None: - src_pos, _ = self.node_label_positions() - self.src_label = self.canvas.create_text( - *src_pos, - text=text, - justify=tk.CENTER, - font=self.canvas.app.edge_font, - tags=tags.LINK_LABEL, - state=self.canvas.show_link_labels.state(), - ) + if self.src_label is None and self.src_label2 is None: + if self.id: + logging.info("src label id") + src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id) + src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y) + self.src_label = self.src.canvas.create_text( + *src_pos, + text=text, + justify=tk.CENTER, + font=self.app.edge_font, + tags=tags.LINK_LABEL, + state=self.manager.show_link_labels.state(), + ) + if self.id2: + logging.info("src label id2") + src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2) + src_pos, _ = node_label_positions(src_x, src_y, dst_x, dst_y) + self.src_label2 = self.dst.canvas.create_text( + *src_pos, + text=text, + justify=tk.CENTER, + font=self.app.edge_font, + tags=tags.LINK_LABEL, + state=self.manager.show_link_labels.state(), + ) else: - self.canvas.itemconfig(self.src_label, text=text) + if self.src_label: + self.src.canvas.itemconfig(self.src_label, text=text) + if self.src_label2: + self.dst.canvas.itemconfig(self.src_label2, text=text) def dst_label_text(self, text: str) -> None: - if self.dst_label is None: - _, dst_pos = self.node_label_positions() - self.dst_label = self.canvas.create_text( - *dst_pos, - text=text, - justify=tk.CENTER, - font=self.canvas.app.edge_font, - tags=tags.LINK_LABEL, - state=self.canvas.show_link_labels.state(), - ) + if self.dst_label is None and self.dst_label2 is None: + if self.id: + src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id) + _, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y) + self.dst_label = self.src.canvas.create_text( + *dst_pos, + text=text, + justify=tk.CENTER, + font=self.app.edge_font, + tags=tags.LINK_LABEL, + state=self.manager.show_link_labels.state(), + ) + if self.id2: + src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2) + _, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y) + self.dst_label2 = self.dst.canvas.create_text( + *dst_pos, + text=text, + justify=tk.CENTER, + font=self.app.edge_font, + tags=tags.LINK_LABEL, + state=self.manager.show_link_labels.state(), + ) else: - self.canvas.itemconfig(self.dst_label, text=text) + if self.dst_label: + self.src.canvas.itemconfig(self.dst_label, text=text) + if self.dst_label2: + self.dst.canvas.itemconfig(self.dst_label2, text=text) - def move_node(self, node_id: int, pos: Tuple[float, float]) -> None: - if self.src == node_id: - self.move_src(pos) - else: - self.move_dst(pos) - - def move_dst(self, dst_pos: Tuple[float, float]) -> None: - src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) + def drawing(self, pos: Tuple[float, float]) -> None: + src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id) src_pos = src_x, src_y - self.moved(src_pos, dst_pos) + self.moved(src_pos, pos) - def move_src(self, src_pos: Tuple[float, float]) -> None: - _, _, _, _, dst_x, dst_y = self.canvas.coords(self.id) + def move_node(self, node: "CanvasNode") -> None: + if self.src == node: + self.move_src() + else: + self.move_dst() + + def move_shadow(self, node: "ShadowNode") -> None: + if self.src_shadow == node: + self.move_src_shadow() + elif self.dst_shadow == node: + self.move_dst_shadow() + + def move_src_shadow(self) -> None: + if not self.id2: + return + _, _, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2) dst_pos = dst_x, dst_y - self.moved(src_pos, dst_pos) + self.moved2(self.src_shadow.position(), dst_pos) + + def move_dst_shadow(self) -> None: + if not self.id: + return + src_x, src_y, _, _, _, _ = self.src.canvas.coords(self.id) + src_pos = src_x, src_y + self.moved(src_pos, self.dst_shadow.position()) + + def move_dst(self) -> None: + if self.dst.is_wireless() and self.has_shadows(): + return + dst_pos = self.dst.position() + if self.id2: + src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id2) + src_pos = src_x, src_y + self.moved2(src_pos, dst_pos) + elif self.id: + src_x, src_y, _, _, _, _ = self.dst.canvas.coords(self.id) + src_pos = src_x, src_y + self.moved(src_pos, dst_pos) + + def move_src(self) -> None: + if not self.id: + return + _, _, _, _, dst_x, dst_y = self.src.canvas.coords(self.id) + dst_pos = dst_x, dst_y + self.moved(self.src.position(), dst_pos) def moved(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None: arc_pos = self._get_arcpoint(src_pos, dst_pos) - self.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) + self.src.canvas.coords(self.id, *src_pos, *arc_pos, *dst_pos) if self.middle_label: - self.canvas.coords(self.middle_label, *arc_pos) - src_pos, dst_pos = self.node_label_positions() + self.src.canvas.coords(self.middle_label, *arc_pos) + src_x, src_y, _, _, dst_x, dst_y = self.src.canvas.coords(self.id) + src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y) if self.src_label: - self.canvas.coords(self.src_label, *src_pos) + self.src.canvas.coords(self.src_label, *src_pos) if self.dst_label: - self.canvas.coords(self.dst_label, *dst_pos) + self.src.canvas.coords(self.dst_label, *dst_pos) + + def moved2( + self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float] + ) -> None: + arc_pos = self._get_arcpoint(src_pos, dst_pos) + self.dst.canvas.coords(self.id2, *src_pos, *arc_pos, *dst_pos) + if self.middle_label2: + self.dst.canvas.coords(self.middle_label2, *arc_pos) + src_x, src_y, _, _, dst_x, dst_y = self.dst.canvas.coords(self.id2) + src_pos, dst_pos = node_label_positions(src_x, src_y, dst_x, dst_y) + if self.src_label2: + self.dst.canvas.coords(self.src_label2, *src_pos) + if self.dst_label2: + self.dst.canvas.coords(self.dst_label2, *dst_pos) def delete(self) -> None: logging.debug("deleting canvas edge, id: %s", self.id) - self.canvas.delete(self.id) - self.canvas.delete(self.src_label) - self.canvas.delete(self.dst_label) + self.src.canvas.delete(self.id) + self.src.canvas.delete(self.src_label) + self.src.canvas.delete(self.dst_label) + if self.dst: + self.dst.canvas.delete(self.id2) + self.dst.canvas.delete(self.src_label2) + self.dst.canvas.delete(self.dst_label2) + if self.src_shadow and self.src_shadow.should_delete(): + self.src_shadow.delete() + self.src_shadow = None + if self.dst_shadow and self.dst_shadow.should_delete(): + self.dst_shadow.delete() + self.dst_shadow = None self.clear_middle_label() self.id = None + self.id2 = None self.src_label = None + self.src_label2 = None self.dst_label = None + self.dst_label2 = None + self.manager.edges.pop(self.token, None) + + def hide(self) -> None: + self.hidden = True + if self.src_shadow: + self.src_shadow.hide() + if self.dst_shadow: + self.dst_shadow.hide() + self.src.canvas.itemconfigure(self.id, state=tk.HIDDEN) + self.src.canvas.itemconfigure(self.src_label, state=tk.HIDDEN) + self.src.canvas.itemconfigure(self.dst_label, state=tk.HIDDEN) + self.src.canvas.itemconfigure(self.middle_label, state=tk.HIDDEN) + if self.id2: + self.dst.canvas.itemconfigure(self.id2, state=tk.HIDDEN) + self.dst.canvas.itemconfigure(self.src_label2, state=tk.HIDDEN) + self.dst.canvas.itemconfigure(self.dst_label2, state=tk.HIDDEN) + self.dst.canvas.itemconfigure(self.middle_label2, state=tk.HIDDEN) + + def show(self) -> None: + self.hidden = False + if self.src_shadow: + self.src_shadow.show() + if self.dst_shadow: + self.dst_shadow.show() + self.src.canvas.itemconfigure(self.id, state=tk.NORMAL) + self.src.canvas.itemconfigure(self.src_label, state=tk.NORMAL) + self.src.canvas.itemconfigure(self.dst_label, state=tk.NORMAL) + self.src.canvas.itemconfigure(self.middle_label, state=tk.NORMAL) + if self.id2: + self.dst.canvas.itemconfigure(self.id2, state=tk.NORMAL) + self.dst.canvas.itemconfigure(self.src_label2, state=tk.NORMAL) + self.dst.canvas.itemconfigure(self.dst_label2, state=tk.NORMAL) + self.dst.canvas.itemconfigure(self.middle_label2, state=tk.NORMAL) class CanvasWirelessEdge(Edge): @@ -239,35 +526,38 @@ class CanvasWirelessEdge(Edge): def __init__( self, - canvas: "CanvasGraph", - src: int, - dst: int, + app: "Application", + src: "CanvasNode", + dst: "CanvasNode", network_id: int, token: str, - src_pos: Tuple[float, float], - dst_pos: Tuple[float, float], link: Link, ) -> None: logging.debug("drawing wireless link from node %s to node %s", src, dst) - super().__init__(canvas, src, dst) + super().__init__(app, src, dst) self.network_id: int = network_id self.link: Link = link self.token: str = token self.width: float = WIRELESS_WIDTH color = link.color if link.color else WIRELESS_COLOR self.color: str = color - self.draw(src_pos, dst_pos, self.canvas.show_wireless.state()) + self.draw(self.manager.show_wireless.state()) if link.label: self.middle_label_text(link.label) self.set_binding() def set_binding(self) -> None: - self.canvas.tag_bind(self.id, "<Button-1>", self.show_info) + self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info) + if self.id2 is not None: + self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info) def show_info(self, _event: tk.Event) -> None: - self.canvas.app.display_info( - WirelessEdgeInfoFrame, app=self.canvas.app, edge=self - ) + self.app.display_info(WirelessEdgeInfoFrame, app=self.app, edge=self) + + def delete(self) -> None: + self.src.wireless_edges.discard(self) + self.dst.wireless_edges.remove(self) + super().delete() class CanvasEdge(Edge): @@ -276,47 +566,40 @@ class CanvasEdge(Edge): """ def __init__( - self, - canvas: "CanvasGraph", - src: int, - src_pos: Tuple[float, float], - dst_pos: Tuple[float, float], + self, app: "Application", src: "CanvasNode", dst: "CanvasNode" = None ) -> None: """ Create an instance of canvas edge object """ - super().__init__(canvas, src) + super().__init__(app, src, dst) self.text_src: Optional[int] = None self.text_dst: Optional[int] = None self.link: Optional[Link] = None - self.linked_wireless: bool = False self.asymmetric_link: Optional[Link] = None self.throughput: Optional[float] = None - self.draw(src_pos, dst_pos, tk.NORMAL) - self.set_binding() - self.context: tk.Menu = tk.Menu(self.canvas) - self.create_context() + self.draw(tk.NORMAL) def is_customized(self) -> bool: return self.width != EDGE_WIDTH or self.color != EDGE_COLOR - def create_context(self) -> None: - themes.style_menu(self.context) - self.context.add_command(label="Configure", command=self.click_configure) - self.context.add_command(label="Delete", command=self.click_delete) - - def set_binding(self) -> None: - self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context) - self.canvas.tag_bind(self.id, "<Button-1>", self.show_info) + def set_bindings(self) -> None: + if self.id: + show_context = functools.partial(self.show_context, self.src.canvas) + self.src.canvas.tag_bind(self.id, "<ButtonRelease-3>", show_context) + self.src.canvas.tag_bind(self.id, "<Button-1>", self.show_info) + if self.id2: + show_context = functools.partial(self.show_context, self.dst.canvas) + self.dst.canvas.tag_bind(self.id2, "<ButtonRelease-3>", show_context) + self.dst.canvas.tag_bind(self.id2, "<Button-1>", self.show_info) def iface_label(self, iface: Interface) -> str: label = "" - if iface.name and self.canvas.show_iface_names.get(): + if iface.name and self.manager.show_iface_names.get(): label = f"{iface.name}" - if iface.ip4 and self.canvas.show_ip4s.get(): + if iface.ip4 and self.manager.show_ip4s.get(): label = f"{label}\n" if label else "" label += f"{iface.ip4}/{iface.ip4_mask}" - if iface.ip6 and self.canvas.show_ip6s.get(): + if iface.ip6 and self.manager.show_ip6s.get(): label = f"{label}\n" if label else "" label += f"{iface.ip6}/{iface.ip6_mask}" return label @@ -346,77 +629,98 @@ class CanvasEdge(Edge): return if self.link.options.loss == EDGE_LOSS: state = tk.HIDDEN - self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id) + if self.id: + self.src.canvas.addtag_withtag(tags.LOSS_EDGES, self.id) + if self.id2: + self.dst.canvas.addtag_withtag(tags.LOSS_EDGES, self.id2) else: state = tk.NORMAL - self.canvas.dtag(self.id, tags.LOSS_EDGES) - if self.canvas.show_loss_links.state() == tk.HIDDEN: - self.canvas.itemconfigure(self.id, state=state) + if self.id: + self.src.canvas.dtag(self.id, tags.LOSS_EDGES) + if self.id2: + self.dst.canvas.dtag(self.id2, tags.LOSS_EDGES) + if self.manager.show_loss_links.state() == tk.HIDDEN: + if self.id: + self.src.canvas.itemconfigure(self.id, state=state) + if self.id2: + self.dst.canvas.itemconfigure(self.id2, state=state) def set_throughput(self, throughput: float) -> None: throughput = 0.001 * throughput text = f"{throughput:.3f} kbps" self.middle_label_text(text) - if throughput > self.canvas.throughput_threshold: - color = self.canvas.throughput_color - width = self.canvas.throughput_width + if throughput > self.manager.throughput_threshold: + color = self.manager.throughput_color + width = self.manager.throughput_width else: color = self.color width = self.scaled_width() - self.canvas.itemconfig(self.id, fill=color, width=width) + self.src.canvas.itemconfig(self.id, fill=color, width=width) + if self.id2: + self.dst.canvas.itemconfig(self.id2, fill=color, width=width) def clear_throughput(self) -> None: self.clear_middle_label() if not self.linked_wireless: self.draw_link_options() - def complete(self, dst: int, linked_wireless: bool) -> None: + def complete(self, dst: "CanvasNode") -> None: self.dst = dst - self.linked_wireless = linked_wireless - dst_pos = self.canvas.coords(self.dst) - self.move_dst(dst_pos) + self.linked_wireless = self.src.is_wireless() or self.dst.is_wireless() + self.set_bindings() self.check_wireless() logging.debug("draw wired link from node %s to node %s", self.src, dst) def check_wireless(self) -> None: - if self.linked_wireless: - self.canvas.itemconfig(self.id, state=tk.HIDDEN) - self.canvas.dtag(self.id, tags.EDGE) - self._check_antenna() - - def _check_antenna(self) -> None: - src_node = self.canvas.nodes[self.src] - dst_node = self.canvas.nodes[self.dst] - src_node_type = src_node.core_node.type - dst_node_type = dst_node.core_node.type - is_src_wireless = NodeUtils.is_wireless_node(src_node_type) - is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - if is_src_wireless or is_dst_wireless: - if is_src_wireless and not is_dst_wireless: - dst_node.add_antenna() - elif not is_src_wireless and is_dst_wireless: - src_node.add_antenna() - else: - src_node.add_antenna() + if not self.linked_wireless: + return + if self.id: + self.src.canvas.itemconfig(self.id, state=tk.HIDDEN) + self.src.canvas.dtag(self.id, tags.EDGE) + if self.id2: + self.dst.canvas.itemconfig(self.id2, state=tk.HIDDEN) + self.dst.canvas.dtag(self.id2, tags.EDGE) + # add antenna to node + if self.src.is_wireless() and not self.dst.is_wireless(): + self.dst.add_antenna() + elif not self.src.is_wireless() and self.dst.is_wireless(): + self.src.add_antenna() + else: + self.src.add_antenna() def reset(self) -> None: - self.canvas.delete(self.middle_label) - self.middle_label = None - self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) + if self.middle_label: + self.src.canvas.delete(self.middle_label) + self.middle_label = None + if self.middle_label2: + self.dst.canvas.delete(self.middle_label2) + self.middle_label2 = None + if self.id: + self.src.canvas.itemconfig( + self.id, fill=self.color, width=self.scaled_width() + ) + if self.id2: + self.dst.canvas.itemconfig( + self.id2, fill=self.color, width=self.scaled_width() + ) def show_info(self, _event: tk.Event) -> None: - self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self) + self.app.display_info(EdgeInfoFrame, app=self.app, edge=self) - def show_context(self, event: tk.Event) -> None: - state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL - self.context.entryconfigure(1, state=state) - self.context.tk_popup(event.x_root, event.y_root) + def show_context(self, canvas: "CanvasGraph", event: tk.Event) -> None: + context: tk.Menu = tk.Menu(canvas) + themes.style_menu(context) + context.add_command(label="Configure", command=self.click_configure) + context.add_command(label="Delete", command=self.click_delete) + state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL + context.entryconfigure(1, state=state) + context.tk_popup(event.x_root, event.y_root) def click_delete(self) -> None: - self.canvas.delete_edge(self) + self.delete() def click_configure(self) -> None: - dialog = LinkConfigurationDialog(self.canvas.app, self) + dialog = LinkConfigurationDialog(self.app, self) dialog.show() def draw_link_options(self): @@ -455,3 +759,20 @@ class CanvasEdge(Edge): lines.append(dup_line) label = "\n".join(lines) self.middle_label_text(label) + + def delete(self) -> None: + self.src.edges.discard(self) + if self.dst: + self.dst.edges.discard(self) + if self.link.iface1: + del self.src.ifaces[self.link.iface1.id] + if self.link.iface2: + del self.dst.ifaces[self.link.iface2.id] + if self.src.is_wireless(): + self.dst.delete_antenna() + if self.dst.is_wireless(): + self.src.delete_antenna() + self.app.core.deleted_canvas_edges([self]) + super().delete() + if self.dst: + self.arc_common_edges() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 9862d2c8..8a66fecc 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -1,79 +1,59 @@ import logging import tkinter as tk from copy import deepcopy -from tkinter import BooleanVar -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage -from core.api.grpc.wrappers import ( - Interface, - Link, - LinkType, - Node, - Session, - ThroughputsEvent, -) +from core.api.grpc.wrappers import Interface, Link +from core.gui import appconfig from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags -from core.gui.graph.edges import ( - EDGE_WIDTH, - CanvasEdge, - CanvasWirelessEdge, - Edge, - arc_edges, - create_edge_token, - create_wireless_token, -) +from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, ShadowNode from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker -from core.gui.images import ImageEnum, TypeToImage -from core.gui.nodeutils import NodeDraw, NodeUtils +from core.gui.images import TypeToImage +from core.gui.nodeutils import NodeUtils if TYPE_CHECKING: from core.gui.app import Application + from core.gui.graph.manager import CanvasManager from core.gui.coreclient import CoreClient -ZOOM_IN = 1.1 -ZOOM_OUT = 0.9 -ICON_SIZE = 48 -MOVE_NODE_MODES = {GraphMode.NODE, GraphMode.SELECT} -MOVE_SHAPE_MODES = {GraphMode.ANNOTATION, GraphMode.SELECT} - - -class ShowVar(BooleanVar): - def __init__(self, canvas: "CanvasGraph", tag: str, value: bool) -> None: - super().__init__(value=value) - self.canvas = canvas - self.tag = tag - - def state(self) -> str: - return tk.NORMAL if self.get() else tk.HIDDEN - - def click_handler(self) -> None: - self.canvas.itemconfigure(self.tag, state=self.state()) +ZOOM_IN: float = 1.1 +ZOOM_OUT: float = 0.9 +ICON_SIZE: int = 48 +MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT} +MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT} +BACKGROUND_COLOR: str = "#cccccc" class CanvasGraph(tk.Canvas): def __init__( - self, master: tk.BaseWidget, app: "Application", core: "CoreClient" + self, + master: tk.BaseWidget, + app: "Application", + manager: "CanvasManager", + core: "CoreClient", + _id: int, + dimensions: Tuple[int, int], ) -> None: - super().__init__(master, highlightthickness=0, background="#cccccc") + super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR) + self.id: int = _id self.app: "Application" = app + self.manager: "CanvasManager" = manager self.core: "CoreClient" = core - self.mode: GraphMode = GraphMode.SELECT - self.annotation_type: Optional[ShapeType] = None self.selection: Dict[int, int] = {} self.select_box: Optional[Shape] = None self.selected: Optional[int] = None - self.node_draw: Optional[NodeDraw] = None self.nodes: Dict[int, CanvasNode] = {} - self.edges: Dict[str, CanvasEdge] = {} + self.shadow_nodes: Dict[int, ShadowNode] = {} self.shapes: Dict[int, Shape] = {} - self.wireless_edges: Dict[str, CanvasWirelessEdge] = {} + self.shadow_core_nodes: Dict[int, ShadowNode] = {} # map wireless/EMANE node to the set of MDRs connected to that node self.wireless_network: Dict[int, Set[int]] = {} @@ -81,10 +61,7 @@ class CanvasGraph(tk.Canvas): self.drawing_edge: Optional[CanvasEdge] = None self.rect: Optional[int] = None self.shape_drawing: bool = False - width = self.app.guiconfig.preferences.width - height = self.app.guiconfig.preferences.height - self.default_dimensions: Tuple[int, int] = (width, height) - self.current_dimensions: Tuple[int, int] = self.default_dimensions + self.current_dimensions: Tuple[int, int] = dimensions self.ratio: float = 1.0 self.offset: Tuple[int, int] = (0, 0) self.cursor: Tuple[int, int] = (0, 0) @@ -98,23 +75,6 @@ class CanvasGraph(tk.Canvas): self.scale_option: tk.IntVar = tk.IntVar(value=1) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) - # throughput related - self.throughput_threshold: float = 250.0 - self.throughput_width: int = 10 - self.throughput_color: str = "#FF0000" - - # drawing related - self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) - self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) - self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True) - self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True) - self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True) - self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) - self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True) - self.show_iface_names: BooleanVar = BooleanVar(value=False) - self.show_ip4s: BooleanVar = BooleanVar(value=True) - self.show_ip6s: BooleanVar = BooleanVar(value=True) - # bindings self.setup_bindings() @@ -126,7 +86,7 @@ class CanvasGraph(tk.Canvas): if self.rect is not None: self.delete(self.rect) if not dimensions: - dimensions = self.default_dimensions + dimensions = self.manager.default_dimensions self.current_dimensions = dimensions self.rect = self.create_rectangle( 0, @@ -139,34 +99,6 @@ class CanvasGraph(tk.Canvas): ) self.configure(scrollregion=self.bbox(tk.ALL)) - def reset_and_redraw(self, session: Session) -> None: - # reset view options to default state - self.show_node_labels.set(True) - self.show_link_labels.set(True) - self.show_grid.set(True) - self.show_annotations.set(True) - self.show_iface_names.set(False) - self.show_ip4s.set(True) - self.show_ip6s.set(True) - self.show_loss_links.set(True) - - # delete any existing drawn items - for tag in tags.RESET_TAGS: - self.delete(tag) - - # set the private variables to default value - self.mode = GraphMode.SELECT - self.annotation_type = None - self.node_draw = None - self.selected = None - self.nodes.clear() - self.edges.clear() - self.shapes.clear() - self.wireless_edges.clear() - self.wireless_network.clear() - self.drawing_edge = None - self.draw_session(session) - def setup_bindings(self) -> None: """ Bind any mouse events or hot keys to the matching action @@ -183,6 +115,12 @@ class CanvasGraph(tk.Canvas): self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y)) self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + def get_shadow(self, node: CanvasNode) -> ShadowNode: + shadow_node = self.shadow_core_nodes.get(node.core_node.id) + if not shadow_node: + shadow_node = ShadowNode(self.app, self, node) + return shadow_node + def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio @@ -204,16 +142,6 @@ class CanvasGraph(tk.Canvas): valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright - def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None: - for iface_throughput in throughputs_event.iface_throughputs: - node_id = iface_throughput.node_id - iface_id = iface_throughput.iface_id - throughput = iface_throughput.throughput - iface_to_edge_id = (node_id, iface_id) - edge = self.core.iface_to_edge.get(iface_to_edge_id) - if edge: - edge.set_throughput(throughput) - def draw_grid(self) -> None: """ Create grid. @@ -228,123 +156,6 @@ class CanvasGraph(tk.Canvas): self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) - def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: - token = create_edge_token(link) - if token in self.edges and link.options.unidirectional: - edge = self.edges[token] - edge.asymmetric_link = link - elif token not in self.edges: - node1 = src.core_node - node2 = dst.core_node - src_pos = (node1.position.x, node1.position.y) - dst_pos = (node2.position.x, node2.position.y) - edge = CanvasEdge(self, src.id, src_pos, dst_pos) - self.complete_edge(src, dst, edge, link) - - def delete_wired_edge(self, link: Link) -> None: - token = create_edge_token(link) - edge = self.edges.get(token) - if edge: - self.delete_edge(edge) - - def update_wired_edge(self, link: Link) -> None: - token = create_edge_token(link) - edge = self.edges.get(token) - if edge: - edge.link.options = deepcopy(link.options) - edge.draw_link_options() - edge.check_options() - - def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: - network_id = link.network_id if link.network_id else None - token = create_wireless_token(src.id, dst.id, network_id) - if token in self.wireless_edges: - logging.warning("ignoring link that already exists: %s", link) - return - src_pos = self.coords(src.id) - dst_pos = self.coords(dst.id) - edge = CanvasWirelessEdge( - self, src.id, dst.id, network_id, token, src_pos, dst_pos, link - ) - self.wireless_edges[token] = edge - src.wireless_edges.add(edge) - dst.wireless_edges.add(edge) - self.tag_raise(src.id) - self.tag_raise(dst.id) - self.arc_common_edges(edge) - - def delete_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: Link - ) -> None: - network_id = link.network_id if link.network_id else None - token = create_wireless_token(src.id, dst.id, network_id) - if token not in self.wireless_edges: - return - edge = self.wireless_edges.pop(token) - edge.delete() - src.wireless_edges.remove(edge) - dst.wireless_edges.remove(edge) - self.arc_common_edges(edge) - - def update_wireless_edge( - self, src: CanvasNode, dst: CanvasNode, link: Link - ) -> None: - if not link.label: - return - network_id = link.network_id if link.network_id else None - token = create_wireless_token(src.id, dst.id, network_id) - if token not in self.wireless_edges: - self.add_wireless_edge(src, dst, link) - else: - edge = self.wireless_edges[token] - edge.middle_label_text(link.label) - - def add_core_node(self, core_node: Node) -> None: - logging.debug("adding node: %s", core_node) - # if the gui can't find node's image, default to the "edit-node" image - image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) - if not image: - image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) - x = core_node.position.x - y = core_node.position.y - node = CanvasNode(self.app, x, y, core_node, image) - self.nodes[node.id] = node - self.core.set_canvas_node(core_node, node) - - def draw_session(self, session: Session) -> None: - """ - Draw existing session. - """ - # draw existing nodes - for core_node in session.nodes.values(): - logging.debug("drawing node: %s", core_node) - # peer to peer node is not drawn on the GUI - if NodeUtils.is_ignore_node(core_node.type): - continue - self.add_core_node(core_node) - # draw existing links - for link in session.links: - logging.debug("drawing link: %s", link) - canvas_node1 = self.core.get_canvas_node(link.node1_id) - canvas_node2 = self.core.get_canvas_node(link.node2_id) - if link.type == LinkType.WIRELESS: - self.add_wireless_edge(canvas_node1, canvas_node2, link) - else: - self.add_wired_edge(canvas_node1, canvas_node2, link) - - def stopped_session(self) -> None: - # clear wireless edges - for edge in self.wireless_edges.values(): - edge.delete() - src_node = self.nodes[edge.src] - src_node.wireless_edges.remove(edge) - dst_node = self.nodes[edge.dst] - dst_node.wireless_edges.remove(edge) - self.wireless_edges.clear() - - # clear throughputs - self.clear_throughputs() - def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate @@ -363,14 +174,12 @@ class CanvasGraph(tk.Canvas): for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: continue - - if _id in self.nodes: + elif _id in self.nodes: selected = _id - break - - if _id in self.shapes: + elif _id in self.shapes: + selected = _id + elif _id in self.shadow_nodes: selected = _id - return selected def click_release(self, event: tk.Event) -> None: @@ -381,13 +190,13 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return - if self.mode == GraphMode.ANNOTATION: + if self.manager.mode == GraphMode.ANNOTATION: self.focus_set() if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False - elif self.mode == GraphMode.SELECT: + elif self.manager.mode == GraphMode.SELECT: self.focus_set() if self.select_box: x0, y0, x1, y1 = self.coords(self.select_box.id) @@ -403,61 +212,35 @@ class CanvasGraph(tk.Canvas): else: self.focus_set() self.selected = self.get_selected(event) - logging.debug(f"click release selected({self.selected}) mode({self.mode})") - if self.mode == GraphMode.EDGE: + logging.debug( + "click release selected(%s) mode(%s)", self.selected, self.manager.mode + ) + if self.manager.mode == GraphMode.EDGE: self.handle_edge_release(event) - elif self.mode == GraphMode.NODE: + elif self.manager.mode == GraphMode.NODE: self.add_node(x, y) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + elif self.manager.mode == GraphMode.PICKNODE: + self.manager.mode = GraphMode.NODE self.selected = None def handle_edge_release(self, _event: tk.Event) -> None: + # not drawing edge return + if not self.drawing_edge: + return edge = self.drawing_edge self.drawing_edge = None - - # not drawing edge return - if edge is None: - return - # edge dst must be a node logging.debug("current selected: %s", self.selected) - src_node = self.nodes.get(edge.src) dst_node = self.nodes.get(self.selected) - if not dst_node or not src_node: + if not dst_node: edge.delete() return - - # edge dst is same as src, delete edge - if edge.src == self.selected: + # check if node can be linked + if not edge.src.is_linkable(dst_node): edge.delete() return - - # rj45 nodes can only support one link - if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges: - edge.delete() - return - if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges: - edge.delete() - return - - # only 1 link between bridge based nodes - is_src_bridge = NodeUtils.is_bridge_node(src_node.core_node) - is_dst_bridge = NodeUtils.is_bridge_node(dst_node.core_node) - common_links = src_node.edges & dst_node.edges - if all([is_src_bridge, is_dst_bridge, common_links]): - edge.delete() - return - # finalize edge creation - self.complete_edge(src_node, dst_node, edge) - - def arc_common_edges(self, edge: Edge) -> None: - src_node = self.nodes[edge.src] - dst_node = self.nodes[edge.dst] - common_edges = list(src_node.edges & dst_node.edges) - common_edges += list(src_node.wireless_edges & dst_node.wireless_edges) - arc_edges(common_edges) + self.manager.complete_edge(edge, dst_node) def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ @@ -504,28 +287,16 @@ class CanvasGraph(tk.Canvas): # delete node and related edges if object_id in self.nodes: canvas_node = self.nodes.pop(object_id) - canvas_node.delete() - nodes.append(canvas_node) - is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) # delete related edges - for edge in canvas_node.edges: + while canvas_node.edges: + edge = canvas_node.edges.pop() if edge in edges: continue edges.add(edge) - del self.edges[edge.token] edge.delete() - # update node connected to edge being deleted - other_id = edge.src - other_iface = edge.link.iface1 - if edge.src == object_id: - other_id = edge.dst - other_iface = edge.link.iface2 - other_node = self.nodes[other_id] - other_node.edges.remove(edge) - if other_iface: - del other_node.ifaces[other_iface.id] - if is_wireless: - other_node.delete_antenna() + # delete node + canvas_node.delete() + nodes.append(canvas_node) # delete shape if object_id in self.shapes: @@ -534,27 +305,21 @@ class CanvasGraph(tk.Canvas): self.selection.clear() self.core.deleted_canvas_nodes(nodes) - self.core.deleted_canvas_edges(edges) - def delete_edge(self, edge: CanvasEdge) -> None: - edge.delete() - del self.edges[edge.token] - src_node = self.nodes[edge.src] - src_node.edges.discard(edge) - if edge.link.iface1: - del src_node.ifaces[edge.link.iface1.id] - dst_node = self.nodes[edge.dst] - dst_node.edges.discard(edge) - if edge.link.iface2: - del dst_node.ifaces[edge.link.iface2.id] - src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) - if src_wireless: - dst_node.delete_antenna() - dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) - if dst_wireless: - src_node.delete_antenna() - self.core.deleted_canvas_edges([edge]) - self.arc_common_edges(edge) + def hide_selected_objects(self) -> None: + for object_id in self.selection: + # delete selection box + selection_id = self.selection[object_id] + self.delete(selection_id) + # hide node and related edges + if object_id in self.nodes: + canvas_node = self.nodes[object_id] + canvas_node.hide() + + def show_hidden(self) -> None: + for node in self.nodes.values(): + if node.hidden: + node.show() def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: @@ -588,13 +353,13 @@ class CanvasGraph(tk.Canvas): y_check = self.cursor[1] - self.offset[1] logging.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes - if self.mode == GraphMode.EDGE and is_node: - pos = self.coords(selected) - self.drawing_edge = CanvasEdge(self, selected, pos, pos) + if self.manager.mode == GraphMode.EDGE and is_node: + node = self.nodes[selected] + self.drawing_edge = CanvasEdge(self.app, node) self.organize() - if self.mode == GraphMode.ANNOTATION: - if is_marker(self.annotation_type): + if self.manager.mode == GraphMode.ANNOTATION: + if is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, @@ -604,11 +369,11 @@ class CanvasGraph(tk.Canvas): fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), - state=self.show_annotations.state(), + state=self.manager.show_annotations.state(), ) return if selected is None: - shape = Shape(self.app, self, self.annotation_type, x, y) + shape = Shape(self.app, self, self.manager.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape @@ -629,8 +394,18 @@ class CanvasGraph(tk.Canvas): node.core_node.position.x, node.core_node.position.y, ) + elif selected in self.shadow_nodes: + shadow_node = self.shadow_nodes[selected] + self.select_object(shadow_node.id) + self.selected = selected + logging.debug( + "selected shadow node(%s), coords: (%s, %s)", + shadow_node.node.core_node.name, + shadow_node.node.core_node.position.x, + shadow_node.node.core_node.position.y, + ) else: - if self.mode == GraphMode.SELECT: + if self.manager.mode == GraphMode.SELECT: shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) self.select_box = shape self.clear_selection() @@ -659,7 +434,7 @@ class CanvasGraph(tk.Canvas): if self.select_box: self.select_box.delete() self.select_box = None - if is_draw_shape(self.annotation_type) and self.shape_drawing: + if is_draw_shape(self.manager.annotation_type) and self.shape_drawing: shape = self.shapes.pop(self.selected) shape.delete() self.shape_drawing = False @@ -669,14 +444,14 @@ class CanvasGraph(tk.Canvas): y_offset = y - self.cursor[1] self.cursor = x, y - if self.mode == GraphMode.EDGE and self.drawing_edge is not None: - self.drawing_edge.move_dst(self.cursor) - if self.mode == GraphMode.ANNOTATION: - if is_draw_shape(self.annotation_type) and self.shape_drawing: + if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None: + self.drawing_edge.drawing(self.cursor) + if self.manager.mode == GraphMode.ANNOTATION: + if is_draw_shape(self.manager.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) return - elif is_marker(self.annotation_type): + elif is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, @@ -689,21 +464,26 @@ class CanvasGraph(tk.Canvas): ) return - if self.mode == GraphMode.EDGE: + if self.manager.mode == GraphMode.EDGE: return # move selected objects if self.selection: for selected_id in self.selection: - if self.mode in MOVE_SHAPE_MODES and selected_id in self.shapes: + if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes: shape = self.shapes[selected_id] shape.motion(x_offset, y_offset) - - if self.mode in MOVE_NODE_MODES and selected_id in self.nodes: + elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes: node = self.nodes[selected_id] node.motion(x_offset, y_offset, update=self.core.is_runtime()) + elif ( + self.manager.mode in MOVE_NODE_MODES + and selected_id in self.shadow_nodes + ): + shadow_node = self.shadow_nodes[selected_id] + shadow_node.motion(x_offset, y_offset) else: - if self.select_box and self.mode == GraphMode.SELECT: + if self.select_box and self.manager.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) def press_delete(self, _event: tk.Event) -> None: @@ -729,17 +509,23 @@ class CanvasGraph(tk.Canvas): return actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( - actual_x, actual_y, self.node_draw.node_type, self.node_draw.model + actual_x, + actual_y, + self.manager.node_draw.node_type, + self.manager.node_draw.model, ) if not core_node: return + core_node.canvas = self.id try: - image_enum = self.node_draw.image_enum - self.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE) + image_enum = self.manager.node_draw.image_enum + self.manager.node_draw.image = self.app.get_icon(image_enum, ICON_SIZE) except AttributeError: - image_file = self.node_draw.image_file - self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE) - node = CanvasNode(self.app, x, y, core_node, self.node_draw.image) + image_file = self.manager.node_draw.image_file + self.manager.node_draw.image = self.app.get_custom_icon( + image_file, ICON_SIZE + ) + node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image) self.nodes[node.id] = node self.core.set_canvas_node(core_node, node) @@ -847,7 +633,7 @@ class CanvasGraph(tk.Canvas): # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) self.draw_grid() - self.app.canvas.show_grid.click_handler() + self.app.manager.show_grid.click_handler() def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): @@ -871,7 +657,7 @@ class CanvasGraph(tk.Canvas): self.tag_raise(tag) def set_wallpaper(self, filename: Optional[str]) -> None: - logging.debug("setting wallpaper: %s", filename) + logging.info("setting canvas(%s) background: %s", self.id, filename) if filename: img = Image.open(filename) self.wallpaper = img @@ -884,44 +670,16 @@ class CanvasGraph(tk.Canvas): self.wallpaper_file = None def is_selection_mode(self) -> bool: - return self.mode == GraphMode.SELECT + return self.manager.mode == GraphMode.SELECT def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ - pos = (src.core_node.position.x, src.core_node.position.y) - edge = CanvasEdge(self, src.id, pos, pos) - self.complete_edge(src, dst, edge) + edge = CanvasEdge(self.app, src) + self.manager.complete_edge(edge, dst) return edge - def complete_edge( - self, - src: CanvasNode, - dst: CanvasNode, - edge: CanvasEdge, - link: Optional[Link] = None, - ) -> None: - linked_wireless = self.is_linked_wireless(src.id, dst.id) - edge.complete(dst.id, linked_wireless) - if link is None: - link = self.core.create_link(edge, src, dst) - edge.link = link - if link.iface1: - iface1 = link.iface1 - src.ifaces[iface1.id] = iface1 - if link.iface2: - iface2 = link.iface2 - dst.ifaces[iface2.id] = iface2 - src.edges.add(edge) - dst.edges.add(edge) - edge.token = create_edge_token(edge.link) - self.arc_common_edges(edge) - edge.draw_labels() - edge.check_options() - self.edges[edge.token] = edge - self.core.save_edge(edge, src, dst) - def copy(self) -> None: if self.core.is_runtime(): logging.debug("copy is disabled during runtime state") @@ -952,7 +710,9 @@ class CanvasGraph(tk.Canvas): ) if not copy: continue - node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) + node = CanvasNode( + self.app, self, scaled_x, scaled_y, copy, canvas_node.image + ) # copy configurations and services node.core_node.services = core_node.services.copy() node.core_node.config_services = core_node.config_services.copy() @@ -1039,30 +799,6 @@ class CanvasGraph(tk.Canvas): ) self.tag_raise(tags.NODE) - def is_linked_wireless(self, src: int, dst: int) -> bool: - src_node = self.nodes[src] - dst_node = self.nodes[dst] - src_node_type = src_node.core_node.type - dst_node_type = dst_node.core_node.type - is_src_wireless = NodeUtils.is_wireless_node(src_node_type) - is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - - # update the wlan/EMANE network - wlan_network = self.wireless_network - if is_src_wireless and not is_dst_wireless: - if src not in wlan_network: - wlan_network[src] = set() - wlan_network[src].add(dst) - elif not is_src_wireless and is_dst_wireless: - if dst not in wlan_network: - wlan_network[dst] = set() - wlan_network[dst].add(src) - return is_src_wireless or is_dst_wireless - - def clear_throughputs(self) -> None: - for edge in self.edges.values(): - edge.clear_throughput() - def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): img = None @@ -1085,3 +821,36 @@ class CanvasGraph(tk.Canvas): for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) + + def get_metadata(self) -> Dict[str, Any]: + wallpaper_path = None + if self.wallpaper_file: + wallpaper = Path(self.wallpaper_file) + if appconfig.BACKGROUNDS_PATH == wallpaper.parent: + wallpaper_path = wallpaper.name + else: + wallpaper_path = str(wallpaper) + return dict( + id=self.id, + wallpaper=wallpaper_path, + wallpaper_style=self.scale_option.get(), + fit_image=self.adjust_to_dim.get(), + ) + + def parse_metadata(self, config: Dict[str, Any]) -> None: + fit_image = config.get("fit_image", False) + self.adjust_to_dim.set(fit_image) + wallpaper_style = config.get("wallpaper_style", 1) + self.scale_option.set(wallpaper_style) + wallpaper = config.get("wallpaper") + if wallpaper: + wallpaper = Path(wallpaper) + if not wallpaper.is_file(): + wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper) + logging.info("canvas(%s), wallpaper: %s", self.id, wallpaper) + if wallpaper.is_file(): + self.set_wallpaper(str(wallpaper)) + else: + self.app.show_error( + "Background Error", f"background file not found: {wallpaper}" + ) diff --git a/daemon/core/gui/graph/manager.py b/daemon/core/gui/graph/manager.py new file mode 100644 index 00000000..00681848 --- /dev/null +++ b/daemon/core/gui/graph/manager.py @@ -0,0 +1,392 @@ +import logging +import tkinter as tk +from copy import deepcopy +from tkinter import BooleanVar, messagebox, ttk +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView + +from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent +from core.gui.graph import tags +from core.gui.graph.edges import ( + CanvasEdge, + CanvasWirelessEdge, + create_edge_token, + create_wireless_token, +) +from core.gui.graph.enums import GraphMode +from core.gui.graph.graph import CanvasGraph +from core.gui.graph.node import CanvasNode +from core.gui.graph.shapeutils import ShapeType +from core.gui.images import ImageEnum +from core.gui.nodeutils import ICON_SIZE, NodeDraw, NodeUtils + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.coreclient import CoreClient + + +class ShowVar(BooleanVar): + def __init__(self, manager: "CanvasManager", tag: str, value: bool) -> None: + super().__init__(value=value) + self.manager: "CanvasManager" = manager + self.tag: str = tag + + def state(self) -> str: + return tk.NORMAL if self.get() else tk.HIDDEN + + def click_handler(self) -> None: + for canvas in self.manager.all(): + canvas.itemconfigure(self.tag, state=self.state()) + + +class CanvasManager: + def __init__( + self, master: tk.BaseWidget, app: "Application", core: "CoreClient" + ) -> None: + self.master: tk.BaseWidget = master + self.app: "Application" = app + self.core: "CoreClient" = core + + # canvas interactions + self.mode: GraphMode = GraphMode.SELECT + self.annotation_type: Optional[ShapeType] = None + self.node_draw: Optional[NodeDraw] = None + self.canvases: Dict[int, CanvasGraph] = {} + + # global edge management + self.edges: Dict[str, CanvasEdge] = {} + self.wireless_edges: Dict[str, CanvasWirelessEdge] = {} + + # global canvas settings + self.default_dimensions: Tuple[int, int] = ( + self.app.guiconfig.preferences.width, + self.app.guiconfig.preferences.height, + ) + self.current_dimensions: Tuple[int, int] = self.default_dimensions + self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) + self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) + self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True) + self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True) + self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True) + self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) + self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True) + self.show_iface_names: BooleanVar = BooleanVar(value=False) + self.show_ip4s: BooleanVar = BooleanVar(value=True) + self.show_ip6s: BooleanVar = BooleanVar(value=True) + + # throughput settings + self.throughput_threshold: float = 250.0 + self.throughput_width: int = 10 + self.throughput_color: str = "#FF0000" + + # widget + self.notebook: Optional[ttk.Notebook] = None + self.canvas_ids: Dict[str, int] = {} + self.unique_ids: Dict[int, str] = {} + self.draw() + + self.setup_bindings() + # start with a single tab by default + self.add_canvas() + + def setup_bindings(self) -> None: + self.notebook.bind("<<NotebookTabChanged>>", self.tab_change) + + def tab_change(self, _event: tk.Event) -> None: + # ignore tab change events before tab data has been setup + unique_id = self.notebook.select() + if not unique_id or unique_id not in self.canvas_ids: + return + canvas = self.current() + self.app.statusbar.set_zoom(canvas.ratio) + + def select(self, tab_id: int): + unique_id = self.unique_ids.get(tab_id) + self.notebook.select(unique_id) + + def draw(self) -> None: + self.notebook = ttk.Notebook(self.master) + self.notebook.grid(sticky=tk.NSEW, pady=1) + + def _next_id(self) -> int: + _id = 1 + canvas_ids = set(self.canvas_ids.values()) + while _id in canvas_ids: + _id += 1 + return _id + + def current(self) -> CanvasGraph: + unique_id = self.notebook.select() + canvas_id = self.canvas_ids[unique_id] + return self.get(canvas_id) + + def all(self) -> ValuesView[CanvasGraph]: + return self.canvases.values() + + def get(self, canvas_id: int) -> CanvasGraph: + canvas = self.canvases.get(canvas_id) + if not canvas: + canvas = self.add_canvas(canvas_id) + return canvas + + def add_canvas(self, canvas_id: int = None) -> CanvasGraph: + # create tab frame + tab = ttk.Frame(self.notebook, padding=0) + tab.grid(sticky=tk.NSEW) + tab.columnconfigure(0, weight=1) + tab.rowconfigure(0, weight=1) + if canvas_id is None: + canvas_id = self._next_id() + self.notebook.add(tab, text=f"Canvas {canvas_id}") + unique_id = self.notebook.tabs()[-1] + logging.info("creating canvas(%s) unique(%s)", canvas_id, unique_id) + self.canvas_ids[unique_id] = canvas_id + self.unique_ids[canvas_id] = unique_id + + # create canvas + canvas = CanvasGraph( + tab, self.app, self, self.core, canvas_id, self.default_dimensions + ) + canvas.grid(sticky=tk.NSEW) + self.canvases[canvas_id] = canvas + + # add scrollbars + scroll_y = ttk.Scrollbar(tab, command=canvas.yview) + scroll_y.grid(row=0, column=1, sticky=tk.NS) + scroll_x = ttk.Scrollbar(tab, orient=tk.HORIZONTAL, command=canvas.xview) + scroll_x.grid(row=1, column=0, sticky=tk.EW) + canvas.configure(xscrollcommand=scroll_x.set) + canvas.configure(yscrollcommand=scroll_y.set) + return canvas + + def delete_canvas(self) -> None: + if len(self.notebook.tabs()) == 1: + messagebox.showinfo("Canvas", "Cannot delete last canvas", parent=self.app) + return + unique_id = self.notebook.select() + self.notebook.forget(unique_id) + canvas_id = self.canvas_ids.pop(unique_id) + canvas = self.canvases.pop(canvas_id) + edges = set() + for node in canvas.nodes.values(): + node.delete() + while node.edges: + edge = node.edges.pop() + if edge in edges: + continue + edges.add(edge) + edge.delete() + + def join(self, session: Session) -> None: + # clear out all canvas + for canvas_id in self.notebook.tabs(): + self.notebook.forget(canvas_id) + self.canvases.clear() + self.canvas_ids.clear() + self.unique_ids.clear() + self.edges.clear() + self.wireless_edges.clear() + logging.info("cleared canvases") + + # reset settings + self.show_node_labels.set(True) + self.show_link_labels.set(True) + self.show_grid.set(True) + self.show_annotations.set(True) + self.show_iface_names.set(False) + self.show_ip4s.set(True) + self.show_ip6s.set(True) + self.show_loss_links.set(True) + self.mode = GraphMode.SELECT + self.annotation_type = None + self.node_draw = None + + # draw session + self.draw_session(session) + + def draw_session(self, session: Session) -> None: + # create session nodes + for core_node in session.nodes.values(): + # add node, avoiding ignored nodes + if NodeUtils.is_ignore_node(core_node.type): + continue + self.add_core_node(core_node) + + # organize canvas tabs + canvas_ids = sorted(self.canvases) + for index, canvas_id in enumerate(canvas_ids): + canvas = self.canvases[canvas_id] + logging.info("sorting canvas index(%s) canvas(%s)", index, canvas_id) + self.notebook.insert(index, canvas.master) + + # draw existing links + for link in session.links: + node1 = self.core.get_canvas_node(link.node1_id) + node2 = self.core.get_canvas_node(link.node2_id) + if link.type == LinkType.WIRELESS: + self.add_wireless_edge(node1, node2, link) + else: + self.add_wired_edge(node1, node2, link) + + # parse metadata and organize canvases + self.core.parse_metadata() + for canvas in self.canvases.values(): + canvas.organize() + + # create a default canvas if none were created prior + if not self.canvases: + self.add_canvas() + + def redraw_canvases(self, dimensions: Tuple[int, int]) -> None: + for canvas in self.canvases.values(): + canvas.redraw_canvas(dimensions) + if canvas.wallpaper: + canvas.redraw_wallpaper() + + def get_metadata(self) -> Dict[str, Any]: + canvases = [x.get_metadata() for x in self.all()] + return dict( + gridlines=self.app.manager.show_grid.get(), + dimensions=self.app.manager.current_dimensions, + canvases=canvases, + ) + + def parse_metadata(self, config: Dict[str, Any]) -> None: + # get configured dimensions and gridlines option + dimensions = self.default_dimensions + dimensions = config.get("dimensions", dimensions) + gridlines = config.get("gridlines", True) + self.show_grid.set(gridlines) + self.redraw_canvases(dimensions) + + # get background configurations + for canvas_config in config.get("canvases", []): + canvas_id = canvas_config.get("id") + if canvas_id is None: + logging.error("canvas config id not provided") + continue + canvas = self.get(canvas_id) + canvas.parse_metadata(canvas_config) + + def add_core_node(self, core_node: Node) -> None: + # get canvas tab for node + canvas_id = core_node.canvas if core_node.canvas > 0 else 1 + logging.info("adding core node canvas(%s): %s", core_node.name, canvas_id) + canvas = self.get(canvas_id) + # if the gui can't find node's image, default to the "edit-node" image + image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale) + if not image: + image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.app, canvas, x, y, core_node, image) + canvas.nodes[node.id] = node + self.core.set_canvas_node(core_node, node) + + def set_throughputs(self, throughputs_event: ThroughputsEvent): + for iface_throughput in throughputs_event.iface_throughputs: + node_id = iface_throughput.node_id + iface_id = iface_throughput.iface_id + throughput = iface_throughput.throughput + iface_to_edge_id = (node_id, iface_id) + edge = self.core.iface_to_edge.get(iface_to_edge_id) + if edge: + edge.set_throughput(throughput) + + def clear_throughputs(self) -> None: + for edge in self.edges.values(): + edge.clear_throughput() + + def stopped_session(self) -> None: + # clear wireless edges + for edge in self.wireless_edges.values(): + edge.delete() + self.wireless_edges.clear() + self.clear_throughputs() + + def update_wired_edge(self, link: Link) -> None: + token = create_edge_token(link) + edge = self.edges.get(token) + if edge: + edge.link.options = deepcopy(link.options) + edge.draw_link_options() + edge.check_options() + + def delete_wired_edge(self, link: Link) -> None: + token = create_edge_token(link) + edge = self.edges.get(token) + if edge: + edge.delete() + + def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: + token = create_edge_token(link) + if token in self.edges and link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + elif token not in self.edges: + edge = CanvasEdge(self.app, src, dst) + self.complete_edge(edge, dst, link) + + def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: + network_id = link.network_id if link.network_id else None + token = create_wireless_token(src.id, dst.id, network_id) + if token in self.wireless_edges: + logging.warning("ignoring link that already exists: %s", link) + return + edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link) + self.wireless_edges[token] = edge + src.wireless_edges.add(edge) + dst.wireless_edges.add(edge) + src.canvas.tag_raise(src.id) + dst.canvas.tag_raise(dst.id) + + def delete_wireless_edge( + self, src: CanvasNode, dst: CanvasNode, link: Link + ) -> None: + network_id = link.network_id if link.network_id else None + token = create_wireless_token(src.id, dst.id, network_id) + if token not in self.wireless_edges: + return + edge = self.wireless_edges.pop(token) + edge.delete() + + def update_wireless_edge( + self, src: CanvasNode, dst: CanvasNode, link: Link + ) -> None: + if not link.label: + return + network_id = link.network_id if link.network_id else None + token = create_wireless_token(src.id, dst.id, network_id) + if token not in self.wireless_edges: + self.add_wireless_edge(src, dst, link) + else: + edge = self.wireless_edges[token] + edge.middle_label_text(link.label) + + # TODO: look into properly moving this into the edge itself and complete when + # the destination is already provided + def complete_edge( + self, edge: CanvasEdge, dst: CanvasNode, link: Optional[Link] = None + ) -> None: + src = edge.src + edge.complete(dst) + if link is None: + link = self.core.create_link(edge, src, dst) + edge.link = link + if link.iface1: + iface1 = link.iface1 + src.ifaces[iface1.id] = iface1 + if link.iface2: + iface2 = link.iface2 + dst.ifaces[iface2.id] = iface2 + src.edges.add(edge) + dst.edges.add(edge) + edge.token = create_edge_token(edge.link) + if not edge.linked_wireless: + edge.arc_common_edges() + edge.draw_labels() + edge.check_options() + self.edges[edge.token] = edge + self.core.save_edge(edge, src, dst) + edge.src.canvas.organize() + if edge.has_shadows(): + edge.dst.canvas.organize() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 2ac4219e..9017db4c 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -2,7 +2,7 @@ import functools import logging import tkinter as tk from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Set +from typing import TYPE_CHECKING, Dict, List, Set, Tuple import grpc from PIL.ImageTk import PhotoImage @@ -31,10 +31,16 @@ NODE_TEXT_OFFSET: int = 5 class CanvasNode: def __init__( - self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage + self, + app: "Application", + canvas: "CanvasGraph", + x: float, + y: float, + core_node: Node, + image: PhotoImage, ): self.app: "Application" = app - self.canvas: "CanvasGraph" = app.canvas + self.canvas: "CanvasGraph" = canvas self.image: PhotoImage = image self.core_node: Node = core_node self.id: int = self.canvas.create_image( @@ -49,7 +55,7 @@ class CanvasNode: tags=tags.NODE_LABEL, font=self.app.icon_text_font, fill="#0000CD", - state=self.canvas.show_node_labels.state(), + state=self.app.manager.show_node_labels.state(), ) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas) self.edges: Set[CanvasEdge] = set() @@ -57,10 +63,14 @@ class CanvasNode: self.wireless_edges: Set[CanvasWirelessEdge] = set() self.antennas: List[int] = [] self.antenna_images: Dict[int, PhotoImage] = {} + self.hidden: bool = False self.setup_bindings() self.context: tk.Menu = tk.Menu(self.canvas) themes.style_menu(self.context) + def position(self) -> Tuple[int, int]: + return self.canvas.coords(self.id) + def next_iface_id(self) -> int: i = 0 while i in self.ifaces: @@ -81,7 +91,7 @@ class CanvasNode: self.delete_antennas() def add_antenna(self) -> None: - x, y = self.canvas.coords(self.id) + x, y = self.position() offset = len(self.antennas) * 8 * self.app.app_scale img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) antenna_id = self.canvas.create_image( @@ -139,15 +149,14 @@ class CanvasNode: def move(self, x: float, y: float) -> None: x, y = self.canvas.get_scaled_coords(x, y) - current_x, current_y = self.canvas.coords(self.id) + current_x, current_y = self.position() x_offset = x - current_x y_offset = y - current_y self.motion(x_offset, y_offset, update=False) def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None: - original_position = self.canvas.coords(self.id) + original_position = self.position() self.canvas.move(self.id, x_offset, y_offset) - pos = self.canvas.coords(self.id) # check new position bbox = self.canvas.bbox(self.id) @@ -165,11 +174,12 @@ class CanvasNode: # move edges for edge in self.edges: - edge.move_node(self.id, pos) + edge.move_node(self) for edge in self.wireless_edges: - edge.move_node(self.id, pos) + edge.move_node(self) # set actual coords for node and update core is running + pos = self.position() real_x, real_y = self.canvas.get_actual_coords(*pos) self.core_node.position.x = real_x self.core_node.position.y = real_y @@ -245,27 +255,44 @@ class CanvasNode: self.context.add_command( label="Link To Selected", command=self.wireless_link_selected ) + + link_menu = tk.Menu(self.context) + for canvas in self.app.manager.all(): + canvas_menu = tk.Menu(link_menu) + themes.style_menu(canvas_menu) + for node in canvas.nodes.values(): + if not self.is_linkable(node): + continue + func_link = functools.partial(self.click_link, node) + canvas_menu.add_command( + label=node.core_node.name, command=func_link + ) + link_menu.add_cascade(label=f"Canvas {canvas.id}", menu=canvas_menu) + themes.style_menu(link_menu) + self.context.add_cascade(label="Link", menu=link_menu) + unlink_menu = tk.Menu(self.context) for edge in self.edges: link = edge.link - if self.id == edge.src: - other_id = edge.dst + if self.id == edge.src.id: + other_node = edge.dst other_iface = link.iface2.name if link.iface2 else None else: - other_id = edge.src + other_node = edge.src other_iface = link.iface1.name if link.iface1 else None - other_node = self.canvas.nodes[other_id] other_name = other_node.core_node.name label = f"{other_name}:{other_iface}" if other_iface else other_name func_unlink = functools.partial(self.click_unlink, edge) unlink_menu.add_command(label=label, command=func_unlink) themes.style_menu(unlink_menu) self.context.add_cascade(label="Unlink", menu=unlink_menu) + edit_menu = tk.Menu(self.context) themes.style_menu(edit_menu) edit_menu.add_command(label="Cut", command=self.click_cut) edit_menu.add_command(label="Copy", command=self.canvas_copy) edit_menu.add_command(label="Delete", command=self.canvas_delete) + edit_menu.add_command(label="Hide", command=self.hide) self.context.add_cascade(label="Edit", menu=edit_menu) self.context.tk_popup(event.x_root, event.y_root) @@ -274,9 +301,13 @@ class CanvasNode: self.canvas_delete() def click_unlink(self, edge: CanvasEdge) -> None: - self.canvas.delete_edge(edge) + edge.delete() self.app.default_info() + def click_link(self, node: "CanvasNode") -> None: + edge = CanvasEdge(self.app, self, node) + self.app.manager.complete_edge(edge, node) + def canvas_delete(self) -> None: self.canvas.clear_selection() self.canvas.select_object(self.id) @@ -320,15 +351,14 @@ class CanvasNode: def has_emane_link(self, iface_id: int) -> Node: result = None for edge in self.edges: - if self.id == edge.src: - other_id = edge.dst + if self.id == edge.src.id: + other_node = edge.dst edge_iface_id = edge.link.iface1.id else: - other_id = edge.src + other_node = edge.src edge_iface_id = edge.link.iface2.id if edge_iface_id != iface_id: continue - other_node = self.canvas.nodes[other_id] if other_node.core_node.type == NodeType.EMANE: result = other_node.core_node break @@ -360,3 +390,43 @@ class CanvasNode: self.core_node.icon = icon_path self.image = Images.create(icon_path, nodeutils.ICON_SIZE) self.canvas.itemconfig(self.id, image=self.image) + + def is_linkable(self, node: "CanvasNode") -> bool: + # cannot link to self + if self == node: + return False + # rj45 nodes can only support one link + if NodeUtils.is_rj45_node(self.core_node.type) and self.edges: + return False + if NodeUtils.is_rj45_node(node.core_node.type) and node.edges: + return False + # only 1 link between bridge based nodes + is_src_bridge = NodeUtils.is_bridge_node(self.core_node) + is_dst_bridge = NodeUtils.is_bridge_node(node.core_node) + common_links = self.edges & node.edges + if all([is_src_bridge, is_dst_bridge, common_links]): + return False + # valid link + return True + + def is_wireless(self) -> bool: + return NodeUtils.is_wireless_node(self.core_node.type) + + def hide(self) -> None: + self.hidden = True + self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self.canvas.itemconfig(self.text_id, state=tk.HIDDEN) + for edge in self.edges: + if not edge.hidden: + edge.hide() + + def show(self) -> None: + self.hidden = False + self.canvas.itemconfig(self.id, state=tk.NORMAL) + self.canvas.itemconfig(self.text_id, state=tk.NORMAL) + for edge in self.edges: + other_node = edge.src + if edge.src == self: + other_node = edge.dst + if edge.hidden and not other_node.hidden: + edge.show() diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 36298655..24786b04 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -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, diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index 3d3c3611..803b969e 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -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, diff --git a/daemon/core/gui/images.py b/daemon/core/gui/images.py index 66d92d30..188bc824 100644 --- a/daemon/core/gui/images.py +++ b/daemon/core/gui/images.py @@ -90,6 +90,7 @@ class ImageEnum(Enum): SHUTDOWN = "shutdown" CANCEL = "cancel" ERROR = "error" + SHADOW = "shadow" class TypeToImage: diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 4c5f5978..a1b03b51 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -188,19 +188,16 @@ class InterfaceManager: self, canvas_node: CanvasNode, visited: Set[int] = None ) -> Optional[IPNetwork]: logging.info("finding subnet for node: %s", canvas_node.core_node.name) - canvas = self.app.canvas subnets = None if not visited: visited = set() visited.add(canvas_node.core_node.id) for edge in canvas_node.edges: - src_node = canvas.nodes[edge.src] - dst_node = canvas.nodes[edge.dst] iface = edge.link.iface1 - check_node = src_node - if src_node == canvas_node: + check_node = edge.src + if edge.src == canvas_node: iface = edge.link.iface2 - check_node = dst_node + check_node = edge.dst if check_node.core_node.id in visited: continue visited.add(check_node.core_node.id) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index fa7853ad..85a8ce01 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -22,7 +22,7 @@ from core.gui.dialogs.servers import ServersDialog from core.gui.dialogs.sessionoptions import SessionOptionsDialog from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.throughput import ThroughputDialog -from core.gui.graph.graph import CanvasGraph +from core.gui.graph.manager import CanvasManager from core.gui.nodeutils import ICON_SIZE from core.gui.observers import ObserversMenu from core.gui.task import ProgressTask @@ -45,9 +45,10 @@ class Menubar(tk.Menu): super().__init__(app) self.app: "Application" = app self.core: CoreClient = app.core - self.canvas: CanvasGraph = app.canvas + self.manager: CanvasManager = app.manager self.recent_menu: Optional[tk.Menu] = None self.edit_menu: Optional[tk.Menu] = None + self.canvas_menu: Optional[tk.Menu] = None self.observers_menu: Optional[ObserversMenu] = None self.draw() @@ -106,6 +107,7 @@ class Menubar(tk.Menu): menu = tk.Menu(self) menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Custom Nodes", command=self.click_custom_nodes) + menu.add_command(label="Show Hidden Nodes", command=self.click_show_hidden) menu.add_separator() menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) @@ -116,11 +118,13 @@ class Menubar(tk.Menu): menu.add_command( label="Delete", accelerator="Ctrl+D", command=self.click_delete ) + menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide) self.add_cascade(label="Edit", menu=menu) self.app.master.bind_all("<Control-x>", self.click_cut) self.app.master.bind_all("<Control-c>", self.click_copy) self.app.master.bind_all("<Control-v>", self.click_paste) self.app.master.bind_all("<Control-d>", self.click_delete) + self.app.master.bind_all("<Control-h>", self.click_hide) self.edit_menu = menu def draw_canvas_menu(self) -> None: @@ -128,9 +132,13 @@ class Menubar(tk.Menu): Create canvas menu """ menu = tk.Menu(self) + menu.add_command(label="New", command=self.click_canvas_add) menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale) + menu.add_separator() + menu.add_command(label="Delete", command=self.click_canvas_delete) menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper) self.add_cascade(label="Canvas", menu=menu) + self.canvas_menu = menu def draw_view_menu(self) -> None: """ @@ -145,52 +153,52 @@ class Menubar(tk.Menu): menu.add_checkbutton( label="Interface Names", command=self.click_edge_label_change, - variable=self.canvas.show_iface_names, + variable=self.manager.show_iface_names, ) menu.add_checkbutton( label="IPv4 Addresses", command=self.click_edge_label_change, - variable=self.canvas.show_ip4s, + variable=self.manager.show_ip4s, ) menu.add_checkbutton( label="IPv6 Addresses", command=self.click_edge_label_change, - variable=self.canvas.show_ip6s, + variable=self.manager.show_ip6s, ) menu.add_checkbutton( label="Node Labels", - command=self.canvas.show_node_labels.click_handler, - variable=self.canvas.show_node_labels, + command=self.manager.show_node_labels.click_handler, + variable=self.manager.show_node_labels, ) menu.add_checkbutton( label="Link Labels", - command=self.canvas.show_link_labels.click_handler, - variable=self.canvas.show_link_labels, + command=self.manager.show_link_labels.click_handler, + variable=self.manager.show_link_labels, ) menu.add_checkbutton( label="Links", - command=self.canvas.show_links.click_handler, - variable=self.canvas.show_links, + command=self.manager.show_links.click_handler, + variable=self.manager.show_links, ) menu.add_checkbutton( label="Loss Links", - command=self.canvas.show_loss_links.click_handler, - variable=self.canvas.show_loss_links, + command=self.manager.show_loss_links.click_handler, + variable=self.manager.show_loss_links, ) menu.add_checkbutton( label="Wireless Links", - command=self.canvas.show_wireless.click_handler, - variable=self.canvas.show_wireless, + command=self.manager.show_wireless.click_handler, + variable=self.manager.show_wireless, ) menu.add_checkbutton( label="Annotations", - command=self.canvas.show_annotations.click_handler, - variable=self.canvas.show_annotations, + command=self.manager.show_annotations.click_handler, + variable=self.manager.show_annotations, ) menu.add_checkbutton( label="Canvas Grid", - command=self.canvas.show_grid.click_handler, - variable=self.canvas.show_grid, + command=self.manager.show_grid.click_handler, + variable=self.manager.show_grid, ) self.add_cascade(label="View", menu=menu) @@ -334,17 +342,12 @@ class Menubar(tk.Menu): self.app.save_config() self.app.menubar.update_recent_files() - def change_menubar_item_state(self, is_runtime: bool) -> None: - labels = {"Copy", "Paste", "Delete", "Cut"} - for i in range(self.edit_menu.index(tk.END) + 1): - try: - label = self.edit_menu.entrycget(i, "label") - if label not in labels: - continue - state = tk.DISABLED if is_runtime else tk.NORMAL - self.edit_menu.entryconfig(i, state=state) - except tk.TclError: - pass + def set_state(self, is_runtime: bool) -> None: + state = tk.DISABLED if is_runtime else tk.NORMAL + for entry in {"Copy", "Paste", "Delete", "Cut"}: + self.edit_menu.entryconfigure(entry, state=state) + for entry in {"Delete"}: + self.canvas_menu.entryconfigure(entry, state=state) def prompt_save_running_session(self, quit_app: bool = False) -> None: """ @@ -372,6 +375,12 @@ class Menubar(tk.Menu): dialog = PreferencesDialog(self.app) dialog.show() + def click_canvas_add(self) -> None: + self.manager.add_canvas() + + def click_canvas_delete(self) -> None: + self.manager.delete_canvas() + def click_canvas_size_and_scale(self) -> None: dialog = SizeAndScaleDialog(self.app) dialog.show() @@ -401,17 +410,29 @@ class Menubar(tk.Menu): dialog.show() def click_copy(self, _event: tk.Event = None) -> None: - self.canvas.copy() + canvas = self.manager.current() + canvas.copy() def click_paste(self, _event: tk.Event = None) -> None: - self.canvas.paste() + canvas = self.manager.current() + canvas.paste() def click_delete(self, _event: tk.Event = None) -> None: - self.canvas.delete_selected_objects() + canvas = self.manager.current() + canvas.delete_selected_objects() + + def click_hide(self, _event: tk.Event = None) -> None: + canvas = self.manager.current() + canvas.hide_selected_objects() def click_cut(self, _event: tk.Event = None) -> None: - self.canvas.copy() - self.canvas.delete_selected_objects() + canvas = self.manager.current() + canvas.copy() + canvas.delete_selected_objects() + + def click_show_hidden(self, _event: tk.Event = None) -> None: + for canvas in self.manager.all(): + canvas.show_hidden() def click_session_options(self) -> None: logging.debug("Click options") @@ -439,14 +460,15 @@ class Menubar(tk.Menu): dialog.show() def click_autogrid(self) -> None: - width, height = self.canvas.current_dimensions + width, height = self.manager.current_dimensions padding = (ICON_SIZE / 2) + 10 layout_size = padding + ICON_SIZE col_count = width // layout_size logging.info( "auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count ) - for i, node in enumerate(self.canvas.nodes.values()): + canvas = self.manager.current() + for i, node in enumerate(canvas.nodes.values()): col = i % col_count row = i // col_count x = (col * layout_size) + padding @@ -460,7 +482,7 @@ class Menubar(tk.Menu): self.app.hide_info() def click_edge_label_change(self) -> None: - for edge in self.canvas.edges.values(): + for edge in self.manager.edges.values(): edge.draw_labels() def click_mac_config(self) -> None: diff --git a/daemon/core/gui/statusbar.py b/daemon/core/gui/statusbar.py index 25f5f972..441213f2 100644 --- a/daemon/core/gui/statusbar.py +++ b/daemon/core/gui/statusbar.py @@ -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 diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 1f5589ba..79a0a2f0 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -144,7 +144,8 @@ class MarkerFrame(ttk.Frame): Tooltip(self.color_frame, "Marker Color") def click_clear(self) -> None: - self.app.canvas.delete(tags.MARKER) + canvas = self.app.manager.current() + canvas.delete(tags.MARKER) def click_color(self, _event: tk.Event) -> None: dialog = ColorPickerDialog(self.app, self.app, self.color) @@ -257,8 +258,8 @@ class Toolbar(ttk.Frame): def draw_node_picker(self) -> None: self.hide_marker() - self.app.canvas.mode = GraphMode.NODE - self.app.canvas.node_draw = self.current_node + self.app.manager.mode = GraphMode.NODE + self.app.manager.node_draw = self.current_node self.design_frame.select_radio(self.node_button) self.picker = PickerFrame(self.app, self.node_button) # draw default nodes @@ -278,12 +279,12 @@ class Toolbar(ttk.Frame): def click_selection(self) -> None: self.design_frame.select_radio(self.select_button) - self.app.canvas.mode = GraphMode.SELECT + self.app.manager.mode = GraphMode.SELECT self.hide_marker() def click_runtime_selection(self) -> None: self.runtime_frame.select_radio(self.runtime_select_button) - self.app.canvas.mode = GraphMode.SELECT + self.app.manager.mode = GraphMode.SELECT self.hide_marker() def click_start(self) -> None: @@ -291,8 +292,8 @@ class Toolbar(ttk.Frame): Start session handler redraw buttons, send node and link messages to grpc server. """ - self.app.menubar.change_menubar_item_state(is_runtime=True) - self.app.canvas.mode = GraphMode.SELECT + self.app.menubar.set_state(is_runtime=True) + self.app.manager.mode = GraphMode.SELECT enable_buttons(self.design_frame, enabled=False) task = ProgressTask( self.app, "Start", self.app.core.start_session, self.start_callback @@ -324,7 +325,7 @@ class Toolbar(ttk.Frame): def click_link(self) -> None: self.design_frame.select_radio(self.link_button) - self.app.canvas.mode = GraphMode.EDGE + self.app.manager.mode = GraphMode.EDGE self.hide_marker() def update_button( @@ -337,7 +338,7 @@ class Toolbar(ttk.Frame): logging.debug("update button(%s): %s", button, node_draw) button.configure(image=image) button.image = image - self.app.canvas.node_draw = node_draw + self.app.manager.node_draw = node_draw if type_enum == NodeTypeEnum.NODE: self.current_node = node_draw elif type_enum == NodeTypeEnum.NETWORK: @@ -348,8 +349,8 @@ class Toolbar(ttk.Frame): Draw the options for link-layer button. """ self.hide_marker() - self.app.canvas.mode = GraphMode.NODE - self.app.canvas.node_draw = self.current_network + self.app.manager.mode = GraphMode.NODE + self.app.manager.node_draw = self.current_network self.design_frame.select_radio(self.network_button) self.picker = PickerFrame(self.app, self.network_button) for node_draw in NodeUtils.NETWORK_NODES: @@ -364,8 +365,8 @@ class Toolbar(ttk.Frame): Draw the options for marker button. """ self.design_frame.select_radio(self.annotation_button) - self.app.canvas.mode = GraphMode.ANNOTATION - self.app.canvas.annotation_type = self.current_annotation + self.app.manager.mode = GraphMode.ANNOTATION + self.app.manager.annotation_type = self.current_annotation if is_marker(self.current_annotation): self.show_marker() self.picker = PickerFrame(self.app, self.annotation_button) @@ -396,7 +397,7 @@ class Toolbar(ttk.Frame): redraw buttons on the toolbar, send node and link messages to grpc server """ logging.info("clicked stop button") - self.app.menubar.change_menubar_item_state(is_runtime=False) + self.app.menubar.set_state(is_runtime=False) self.app.core.close_mobility_players() enable_buttons(self.runtime_frame, enabled=False) task = ProgressTask( @@ -406,7 +407,7 @@ class Toolbar(ttk.Frame): def stop_callback(self, result: bool) -> None: self.set_design() - self.app.canvas.stopped_session() + self.app.manager.stopped_session() def update_annotation( self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage @@ -414,7 +415,7 @@ class Toolbar(ttk.Frame): logging.debug("clicked annotation") self.annotation_button.configure(image=image) self.annotation_button.image = image - self.app.canvas.annotation_type = shape_type + self.app.manager.annotation_type = shape_type self.current_annotation = shape_type self.annotation_enum = image_enum if is_marker(shape_type): @@ -435,8 +436,8 @@ class Toolbar(ttk.Frame): def click_marker_button(self) -> None: self.runtime_frame.select_radio(self.runtime_marker_button) - self.app.canvas.mode = GraphMode.ANNOTATION - self.app.canvas.annotation_type = ShapeType.MARKER + self.app.manager.mode = GraphMode.ANNOTATION + self.app.manager.annotation_type = ShapeType.MARKER self.show_marker() def scale_button( diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index d168afe0..cdc89202 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -752,6 +752,7 @@ message Node { Geo geo = 12; string dir = 13; string channel = 14; + int32 canvas = 15; } message Link {