core-extra/daemon/core/gui/graph/manager.py

434 lines
16 KiB
Python
Raw Permalink Normal View History

import json
import logging
import tkinter as tk
from copy import deepcopy
from tkinter import BooleanVar, messagebox, ttk
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView
from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent
from core.gui import nodeutils as nutils
from core.gui.graph import tags
from core.gui.graph.edges import (
CanvasEdge,
CanvasWirelessEdge,
create_edge_token,
create_wireless_token,
)
from core.gui.graph.enums import GraphMode
from core.gui.graph.graph import CanvasGraph
from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType
from core.gui.nodeutils import NodeDraw
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.coreclient import CoreClient
class ShowVar(BooleanVar):
def __init__(self, manager: "CanvasManager", tag: str, value: bool) -> None:
super().__init__(value=value)
self.manager: "CanvasManager" = manager
self.tag: str = tag
def state(self) -> str:
return tk.NORMAL if self.get() else tk.HIDDEN
def click_handler(self) -> None:
for canvas in self.manager.all():
canvas.itemconfigure(self.tag, state=self.state())
class ShowNodeLabels(ShowVar):
def click_handler(self) -> None:
state = self.state()
for canvas in self.manager.all():
for node in canvas.nodes.values():
if not node.hidden:
node.set_label(state)
class ShowLinks(ShowVar):
def click_handler(self) -> None:
for edge in self.manager.edges.values():
if not edge.hidden:
edge.check_visibility()
class ShowLinkLabels(ShowVar):
def click_handler(self) -> None:
state = self.state()
for edge in self.manager.edges.values():
if not edge.hidden:
edge.set_labels(state)
class CanvasManager:
def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
) -> None:
self.master: tk.BaseWidget = master
self.app: "Application" = app
self.core: "CoreClient" = core
# canvas interactions
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.node_draw: Optional[NodeDraw] = None
self.canvases: Dict[int, CanvasGraph] = {}
# global edge management
self.edges: Dict[str, CanvasEdge] = {}
self.wireless_edges: Dict[str, CanvasWirelessEdge] = {}
# global canvas settings
self.default_dimensions: Tuple[int, int] = (
self.app.guiconfig.preferences.width,
self.app.guiconfig.preferences.height,
)
self.show_node_labels: ShowVar = ShowNodeLabels(
self, tags.NODE_LABEL, value=True
)
self.show_link_labels: ShowVar = ShowLinkLabels(
self, tags.LINK_LABEL, value=True
)
self.show_links: ShowVar = ShowLinks(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_loss_links: ShowVar = ShowLinks(self, tags.LOSS_EDGES, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# throughput settings
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# widget
self.notebook: Optional[ttk.Notebook] = None
self.canvas_ids: Dict[str, int] = {}
self.unique_ids: Dict[int, str] = {}
self.draw()
self.setup_bindings()
# start with a single tab by default
self.add_canvas()
def setup_bindings(self) -> None:
self.notebook.bind("<<NotebookTabChanged>>", self.tab_change)
def tab_change(self, _event: tk.Event) -> None:
# ignore tab change events before tab data has been setup
unique_id = self.notebook.select()
if not unique_id or unique_id not in self.canvas_ids:
return
canvas = self.current()
self.app.statusbar.set_zoom(canvas.ratio)
def select(self, tab_id: int):
unique_id = self.unique_ids.get(tab_id)
self.notebook.select(unique_id)
def draw(self) -> None:
self.notebook = ttk.Notebook(self.master)
self.notebook.grid(sticky=tk.NSEW, pady=1)
def _next_id(self) -> int:
_id = 1
canvas_ids = set(self.canvas_ids.values())
while _id in canvas_ids:
_id += 1
return _id
def current(self) -> CanvasGraph:
unique_id = self.notebook.select()
canvas_id = self.canvas_ids[unique_id]
return self.get(canvas_id)
def all(self) -> ValuesView[CanvasGraph]:
return self.canvases.values()
def get(self, canvas_id: int) -> CanvasGraph:
canvas = self.canvases.get(canvas_id)
if not canvas:
canvas = self.add_canvas(canvas_id)
return canvas
def add_canvas(self, canvas_id: int = None) -> CanvasGraph:
# create tab frame
tab = ttk.Frame(self.notebook, padding=0)
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
tab.rowconfigure(0, weight=1)
if canvas_id is None:
canvas_id = self._next_id()
self.notebook.add(tab, text=f"Canvas {canvas_id}")
unique_id = self.notebook.tabs()[-1]
logger.info("creating canvas(%s)", canvas_id)
self.canvas_ids[unique_id] = canvas_id
self.unique_ids[canvas_id] = unique_id
# create canvas
canvas = CanvasGraph(
tab, self.app, self, self.core, canvas_id, self.default_dimensions
)
canvas.grid(sticky=tk.NSEW)
self.canvases[canvas_id] = canvas
# add scrollbars
scroll_y = ttk.Scrollbar(tab, command=canvas.yview)
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(tab, orient=tk.HORIZONTAL, command=canvas.xview)
scroll_x.grid(row=1, column=0, sticky=tk.EW)
canvas.configure(xscrollcommand=scroll_x.set)
canvas.configure(yscrollcommand=scroll_y.set)
return canvas
def delete_canvas(self) -> None:
if len(self.notebook.tabs()) == 1:
messagebox.showinfo("Canvas", "Cannot delete last canvas", parent=self.app)
return
unique_id = self.notebook.select()
self.notebook.forget(unique_id)
canvas_id = self.canvas_ids.pop(unique_id)
canvas = self.canvases.pop(canvas_id)
edges = set()
for node in canvas.nodes.values():
node.delete()
while node.edges:
edge = node.edges.pop()
if edge in edges:
continue
edges.add(edge)
edge.delete()
def join(self, session: Session) -> None:
# clear out all canvases
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()
logger.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:
# draw canvas configurations and shapes
self.parse_metadata_canvas(session.metadata)
self.parse_metadata_shapes(session.metadata)
# create session nodes
for core_node in session.nodes.values():
# add node, avoiding ignored nodes
if nutils.should_ignore(core_node):
continue
self.add_core_node(core_node)
# organize canvas tabs
canvas_ids = sorted(self.canvases)
for index, canvas_id in enumerate(canvas_ids):
canvas = self.canvases[canvas_id]
self.notebook.insert(index, canvas.master)
# draw existing links
for link in session.links:
node1 = self.core.get_canvas_node(link.node1_id)
node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(node1, node2, link)
else:
self.add_wired_edge(node1, node2, link)
# organize canvas order
for canvas in self.canvases.values():
canvas.organize()
# parse metada for edge configs and hidden nodes
self.parse_metadata_edges(session.metadata)
self.parse_metadata_hidden(session.metadata)
# create a default canvas if none were created prior
if not self.canvases:
self.add_canvas()
def redraw_canvas(self, dimensions: Tuple[int, int]) -> None:
canvas = self.current()
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.show_grid.get(), canvases=canvases)
def parse_metadata_canvas(self, metadata: Dict[str, Any]) -> None:
# canvas setting
canvas_config = metadata.get("canvas")
logger.debug("canvas metadata: %s", canvas_config)
if not canvas_config:
return
canvas_config = json.loads(canvas_config)
# get configured dimensions and gridlines option
gridlines = canvas_config.get("gridlines", True)
self.show_grid.set(gridlines)
# get background configurations
for canvas_config in canvas_config.get("canvases", []):
canvas_id = canvas_config.get("id")
if canvas_id is None:
logger.error("canvas config id not provided")
continue
canvas = self.get(canvas_id)
canvas.parse_metadata(canvas_config)
def parse_metadata_shapes(self, metadata: Dict[str, Any]) -> None:
# load saved shapes
shapes_config = metadata.get("shapes")
if not shapes_config:
return
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logger.debug("loading shape: %s", shape_config)
Shape.from_metadata(self.app, shape_config)
def parse_metadata_edges(self, metadata: Dict[str, Any]) -> None:
# load edges config
edges_config = metadata.get("edges")
if not edges_config:
return
edges_config = json.loads(edges_config)
logger.info("edges config: %s", edges_config)
for edge_config in edges_config:
edge_token = edge_config["token"]
edge = self.core.links.get(edge_token)
if edge:
edge.width = edge_config["width"]
edge.color = edge_config["color"]
edge.redraw()
else:
logger.warning("invalid edge token to configure: %s", edge_token)
def parse_metadata_hidden(self, metadata: Dict[str, Any]) -> None:
# read hidden nodes
hidden_config = metadata.get("hidden")
if not hidden_config:
return
hidden_config = json.loads(hidden_config)
for node_id in hidden_config:
canvas_node = self.core.canvas_nodes.get(node_id)
if canvas_node:
canvas_node.hide()
else:
logger.warning("invalid node to hide: %s", node_id)
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
logger.info("adding core node canvas(%s): %s", core_node.name, canvas_id)
canvas = self.get(canvas_id)
image = nutils.get_icon(core_node, self.app)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, canvas, x, y, core_node, image)
canvas.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def set_throughputs(self, throughputs_event: ThroughputsEvent):
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
edge = self.core.iface_to_edge.get(iface_to_edge_id)
if edge:
edge.set_throughput(throughput)
def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_throughput()
def stopped_session(self) -> None:
# clear wireless edges
for edge in self.wireless_edges.values():
edge.delete()
self.wireless_edges.clear()
self.clear_throughputs()
def update_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.link.options = deepcopy(link.options)
edge.draw_link_options()
edge.check_visibility()
def delete_wired_edge(self, link: Link) -> None:
token = create_edge_token(link)
edge = self.edges.get(token)
if edge:
edge.delete()
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
token = create_edge_token(link)
if token in self.edges and link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
edge.redraw()
elif token not in self.edges:
edge = CanvasEdge(self.app, src, dst)
edge.complete(dst, link)
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
logger.warning("ignoring link that already exists: %s", link)
return
edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link)
self.wireless_edges[token] = edge
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
return
edge = self.wireless_edges.pop(token)
edge.delete()
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
network_id = link.network_id if link.network_id else None
token = create_wireless_token(src.id, dst.id, network_id)
if token not in self.wireless_edges:
self.add_wireless_edge(src, dst, link)
else:
edge = self.wireless_edges[token]
edge.middle_label_text(link.label)