removed CustomNode class, added nodeutils and NodeDraw to support defining the current node type to draw and reuse for custom nodes as well
This commit is contained in:
parent
7981340b13
commit
8ad9b7d728
9 changed files with 146 additions and 139 deletions
|
@ -9,12 +9,16 @@ from coretk.graph import CanvasGraph
|
|||
from coretk.images import ImageEnum, Images
|
||||
from coretk.menuaction import MenuAction
|
||||
from coretk.menubar import Menubar
|
||||
from coretk.nodeutils import NodeUtils
|
||||
from coretk.toolbar import Toolbar
|
||||
|
||||
|
||||
class Application(tk.Frame):
|
||||
def __init__(self, master=None):
|
||||
super().__init__(master)
|
||||
# load node icons
|
||||
NodeUtils.setup()
|
||||
|
||||
# widgets
|
||||
self.menubar = None
|
||||
self.toolbar = None
|
||||
|
|
|
@ -7,13 +7,12 @@ import os
|
|||
from core.api.grpc import client, core_pb2
|
||||
from coretk.dialogs.sessions import SessionsDialog
|
||||
from coretk.emaneodelnodeconfig import EmaneModelNodeConfig
|
||||
from coretk.images import NODE_WIDTH, Images
|
||||
from coretk.interface import InterfaceManager
|
||||
from coretk.mobilitynodeconfig import MobilityNodeConfig
|
||||
from coretk.nodeutils import NodeDraw
|
||||
from coretk.servicenodeconfig import ServiceNodeConfig
|
||||
from coretk.wlannodeconfig import WlanNodeConfig
|
||||
|
||||
NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"}
|
||||
DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"}
|
||||
OBSERVERS = {
|
||||
"processes": "ps",
|
||||
|
@ -35,14 +34,6 @@ class CoreServer:
|
|||
self.port = port
|
||||
|
||||
|
||||
class CustomNode:
|
||||
def __init__(self, name, image, image_file, services):
|
||||
self.name = name
|
||||
self.image = image
|
||||
self.image_file = image_file
|
||||
self.services = services
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, name, cmd):
|
||||
self.name = name
|
||||
|
@ -96,12 +87,11 @@ class CoreClient:
|
|||
|
||||
# read custom nodes
|
||||
for config in self.app.config.get("nodes", []):
|
||||
name = config["name"]
|
||||
image_file = config["image"]
|
||||
image = Images.get_custom(image_file, NODE_WIDTH)
|
||||
custom_node = CustomNode(
|
||||
config["name"], image, image_file, set(config["services"])
|
||||
)
|
||||
self.custom_nodes[custom_node.name] = custom_node
|
||||
services = set(config["services"])
|
||||
node_draw = NodeDraw.from_custom(name, image_file, services)
|
||||
self.custom_nodes[name] = node_draw
|
||||
|
||||
# read observers
|
||||
for config in self.app.config.get("observers", []):
|
||||
|
@ -448,6 +438,7 @@ class CoreClient:
|
|||
self.emaneconfig_management.set_default_config(node_id)
|
||||
|
||||
# set default service configurations
|
||||
# TODO: need to deal with this and custom node cases
|
||||
if node_type == core_pb2.NodeType.DEFAULT:
|
||||
self.serviceconfig_manager.node_default_services_configuration(
|
||||
node_id=node_id, node_model=model
|
||||
|
|
|
@ -3,9 +3,9 @@ import tkinter as tk
|
|||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
|
||||
from coretk.coreclient import CustomNode
|
||||
from coretk.dialogs.dialog import Dialog
|
||||
from coretk.dialogs.icondialog import IconDialog
|
||||
from coretk.nodeutils import NodeDraw
|
||||
from coretk.widgets import CheckboxList, ListboxScroll
|
||||
|
||||
|
||||
|
@ -119,7 +119,9 @@ class CustomNodesDialog(Dialog):
|
|||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(sticky="ew")
|
||||
self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon)
|
||||
self.image_button = ttk.Button(
|
||||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
self.image_button.grid(sticky="ew")
|
||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||
button.grid(sticky="ew")
|
||||
|
@ -180,12 +182,12 @@ class CustomNodesDialog(Dialog):
|
|||
def click_save(self):
|
||||
self.app.config["nodes"].clear()
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
custom_node = self.app.core.custom_nodes[name]
|
||||
node_draw = self.app.core.custom_nodes[name]
|
||||
self.app.config["nodes"].append(
|
||||
{
|
||||
"name": custom_node.name,
|
||||
"image": custom_node.image_file,
|
||||
"services": list(custom_node.services),
|
||||
"name": name,
|
||||
"image": node_draw.image_file,
|
||||
"services": list(node_draw.services),
|
||||
}
|
||||
)
|
||||
logging.info("saving custom nodes: %s", self.app.config["nodes"])
|
||||
|
@ -195,10 +197,9 @@ class CustomNodesDialog(Dialog):
|
|||
def click_create(self):
|
||||
name = self.name.get()
|
||||
if name not in self.app.core.custom_nodes:
|
||||
custom_node = CustomNode(
|
||||
name, self.image, Path(self.image_file).name, set(self.services)
|
||||
)
|
||||
self.app.core.custom_nodes[name] = custom_node
|
||||
image_file = Path(self.image_file).stem
|
||||
node_draw = NodeDraw.from_custom(name, image_file, set(self.services))
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
self.reset_values()
|
||||
|
||||
|
@ -207,12 +208,12 @@ class CustomNodesDialog(Dialog):
|
|||
if self.selected:
|
||||
previous_name = self.selected
|
||||
self.selected = name
|
||||
custom_node = self.app.core.custom_nodes.pop(previous_name)
|
||||
custom_node.name = name
|
||||
custom_node.image = self.image
|
||||
custom_node.image_file = Path(self.image_file).stem
|
||||
custom_node.services = self.services
|
||||
self.app.core.custom_nodes[name] = custom_node
|
||||
node_draw = self.app.core.custom_nodes.pop(previous_name)
|
||||
node_draw.model = name
|
||||
node_draw.image_file = Path(self.image_file).stem
|
||||
node_draw.image = self.image
|
||||
node_draw.services = self.services
|
||||
self.app.core.custom_nodes[name] = node_draw
|
||||
self.nodes_list.listbox.delete(self.selected_index)
|
||||
self.nodes_list.listbox.insert(self.selected_index, name)
|
||||
self.nodes_list.listbox.selection_set(self.selected_index)
|
||||
|
@ -230,11 +231,11 @@ class CustomNodesDialog(Dialog):
|
|||
if selection:
|
||||
self.selected_index = selection[0]
|
||||
self.selected = self.nodes_list.listbox.get(self.selected_index)
|
||||
custom_node = self.app.core.custom_nodes[self.selected]
|
||||
self.name.set(custom_node.name)
|
||||
self.services = custom_node.services
|
||||
self.image = custom_node.image
|
||||
self.image_file = custom_node.image_file
|
||||
node_draw = self.app.core.custom_nodes[self.selected]
|
||||
self.name.set(node_draw.model)
|
||||
self.services = node_draw.services
|
||||
self.image = node_draw.image
|
||||
self.image_file = node_draw.image_file
|
||||
self.image_button.config(image=self.image)
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import tkinter as tk
|
||||
from tkinter import filedialog, ttk
|
||||
|
||||
from coretk import nodeutils
|
||||
from coretk.appconfig import ICONS_PATH
|
||||
from coretk.dialogs.dialog import Dialog
|
||||
from coretk.images import Images
|
||||
|
@ -54,7 +55,7 @@ class IconDialog(Dialog):
|
|||
),
|
||||
)
|
||||
if file_path:
|
||||
self.image = Images.create(file_path, 32, 32)
|
||||
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
|
||||
self.image_label.config(image=self.image)
|
||||
self.file_path.set(file_path)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ from coretk.graph_helper import GraphHelper, WlanAntennaManager
|
|||
from coretk.images import Images
|
||||
from coretk.linkinfo import LinkInfo, Throughput
|
||||
from coretk.nodedelete import CanvasComponentManagement
|
||||
from coretk.nodeutils import NodeUtils
|
||||
from coretk.wirelessconnection import WirelessConnection
|
||||
|
||||
|
||||
|
@ -37,9 +38,7 @@ class CanvasGraph(tk.Canvas):
|
|||
kwargs["highlightthickness"] = 0
|
||||
super().__init__(master, cnf, **kwargs)
|
||||
self.mode = GraphMode.SELECT
|
||||
self.draw_node_image = None
|
||||
self.draw_node_type = None
|
||||
self.draw_node_model = None
|
||||
self.node_draw = None
|
||||
self.selected = None
|
||||
self.node_context = None
|
||||
self.nodes = {}
|
||||
|
@ -96,9 +95,7 @@ class CanvasGraph(tk.Canvas):
|
|||
|
||||
# set the private variables to default value
|
||||
self.mode = GraphMode.SELECT
|
||||
self.draw_node_image = None
|
||||
self.draw_node_type = None
|
||||
self.draw_node_model = None
|
||||
self.node_draw = None
|
||||
self.selected = None
|
||||
self.node_context = None
|
||||
self.nodes.clear()
|
||||
|
@ -157,7 +154,7 @@ class CanvasGraph(tk.Canvas):
|
|||
continue
|
||||
|
||||
# draw nodes on the canvas
|
||||
image = Images.node_icon(core_node.type, core_node.model)
|
||||
image = NodeUtils.node_icon(core_node.type, core_node.model)
|
||||
position = core_node.position
|
||||
node = CanvasNode(position.x, position.y, image, self.master, core_node)
|
||||
self.nodes[node.id] = node
|
||||
|
@ -267,13 +264,7 @@ class CanvasGraph(tk.Canvas):
|
|||
self.handle_edge_release(event)
|
||||
elif self.mode == GraphMode.NODE:
|
||||
x, y = self.canvas_xy(event)
|
||||
self.add_node(
|
||||
x,
|
||||
y,
|
||||
self.draw_node_image,
|
||||
self.draw_node_type,
|
||||
self.draw_node_model,
|
||||
)
|
||||
self.add_node(x, y)
|
||||
elif self.mode == GraphMode.PICKNODE:
|
||||
self.mode = GraphMode.NODE
|
||||
|
||||
|
@ -404,12 +395,14 @@ class CanvasGraph(tk.Canvas):
|
|||
# delete the related data from core
|
||||
self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens)
|
||||
|
||||
def add_node(self, x, y, image, node_type, model):
|
||||
def add_node(self, x, y):
|
||||
plot_id = self.find_all()[0]
|
||||
logging.info("add node event: %s - %s", plot_id, self.selected)
|
||||
if self.selected == plot_id:
|
||||
core_node = self.core.create_node(int(x), int(y), node_type, model)
|
||||
node = CanvasNode(x, y, image, self.master, core_node)
|
||||
core_node = self.core.create_node(
|
||||
int(x), int(y), self.node_draw.node_type, self.node_draw.model
|
||||
)
|
||||
node = CanvasNode(x, y, self.node_draw.image, self.master, core_node)
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
self.nodes[node.id] = node
|
||||
return node
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from coretk.appconfig import LOCAL_ICONS_PATH
|
||||
|
||||
NODE_WIDTH = 32
|
||||
|
@ -35,45 +33,6 @@ class Images:
|
|||
file_path = cls.images[name]
|
||||
return cls.create(file_path, width, height)
|
||||
|
||||
@classmethod
|
||||
def node_icon(cls, node_type, node_model):
|
||||
"""
|
||||
Retrieve image based on type and model
|
||||
:param core_pb2.NodeType node_type: core node type
|
||||
:param string node_model: the node model
|
||||
:return: core node icon
|
||||
:rtype: PhotoImage
|
||||
"""
|
||||
image_enum = ImageEnum.ROUTER
|
||||
if node_type == core_pb2.NodeType.SWITCH:
|
||||
image_enum = ImageEnum.SWITCH
|
||||
elif node_type == core_pb2.NodeType.HUB:
|
||||
image_enum = ImageEnum.HUB
|
||||
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
|
||||
image_enum = ImageEnum.WLAN
|
||||
elif node_type == core_pb2.NodeType.EMANE:
|
||||
image_enum = ImageEnum.EMANE
|
||||
elif node_type == core_pb2.NodeType.RJ45:
|
||||
image_enum = ImageEnum.RJ45
|
||||
elif node_type == core_pb2.NodeType.TUNNEL:
|
||||
image_enum = ImageEnum.TUNNEL
|
||||
elif node_type == core_pb2.NodeType.DEFAULT:
|
||||
if node_model == "router":
|
||||
image_enum = ImageEnum.ROUTER
|
||||
elif node_model == "host":
|
||||
image_enum = ImageEnum.HOST
|
||||
elif node_model == "PC":
|
||||
image_enum = ImageEnum.PC
|
||||
elif node_model == "mdr":
|
||||
image_enum = ImageEnum.MDR
|
||||
elif node_model == "prouter":
|
||||
image_enum = ImageEnum.PROUTER
|
||||
else:
|
||||
logging.error("invalid node model: %s", node_model)
|
||||
else:
|
||||
logging.error("invalid node type: %s", node_type)
|
||||
return Images.get(image_enum, NODE_WIDTH)
|
||||
|
||||
|
||||
class ImageEnum(Enum):
|
||||
SWITCH = "lanswitch"
|
||||
|
|
79
coretk/coretk/nodeutils.py
Normal file
79
coretk/coretk/nodeutils.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
from core.api.grpc.core_pb2 import NodeType
|
||||
from coretk.images import ImageEnum, Images
|
||||
|
||||
ICON_SIZE = 32
|
||||
|
||||
|
||||
class NodeDraw:
|
||||
def __init__(self):
|
||||
self.custom = False
|
||||
self.image = None
|
||||
self.image_enum = None
|
||||
self.image_file = None
|
||||
self.node_type = None
|
||||
self.model = None
|
||||
self.tooltip = None
|
||||
self.services = set()
|
||||
|
||||
@classmethod
|
||||
def from_setup(cls, image_enum, node_type, model=None, tooltip=None):
|
||||
node_draw = NodeDraw()
|
||||
node_draw.image_enum = image_enum
|
||||
node_draw.image = Images.get(image_enum, ICON_SIZE)
|
||||
node_draw.node_type = node_type
|
||||
node_draw.model = model
|
||||
if tooltip is None:
|
||||
tooltip = model
|
||||
node_draw.tooltip = tooltip
|
||||
return node_draw
|
||||
|
||||
@classmethod
|
||||
def from_custom(cls, name, image_file, services):
|
||||
node_draw = NodeDraw()
|
||||
node_draw.custom = True
|
||||
node_draw.image_file = image_file
|
||||
node_draw.image = Images.get_custom(image_file, ICON_SIZE)
|
||||
node_draw.node_type = NodeType.DEFAULT
|
||||
node_draw.services = services
|
||||
node_draw.model = name
|
||||
node_draw.tooltip = name
|
||||
return node_draw
|
||||
|
||||
|
||||
class NodeUtils:
|
||||
NODES = []
|
||||
NETWORK_NODES = []
|
||||
NODE_ICONS = {}
|
||||
|
||||
@classmethod
|
||||
def node_icon(cls, node_type, model):
|
||||
return cls.NODE_ICONS[(node_type, model)]
|
||||
|
||||
@classmethod
|
||||
def setup(cls):
|
||||
nodes = [
|
||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "router"),
|
||||
(ImageEnum.HOST, NodeType.DEFAULT, "host"),
|
||||
(ImageEnum.PC, NodeType.DEFAULT, "PC"),
|
||||
(ImageEnum.MDR, NodeType.DEFAULT, "mdr"),
|
||||
(ImageEnum.PROUTER, NodeType.DEFAULT, "prouter"),
|
||||
(ImageEnum.DOCKER, NodeType.DOCKER, "Docker"),
|
||||
(ImageEnum.LXC, NodeType.LXC, "LXC"),
|
||||
]
|
||||
for image_enum, node_type, model in nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, model)
|
||||
cls.NODES.append(node_draw)
|
||||
cls.NODE_ICONS[(node_type, model)] = node_draw.image
|
||||
|
||||
network_nodes = [
|
||||
(ImageEnum.HUB, NodeType.HUB, "ethernet hub"),
|
||||
(ImageEnum.SWITCH, NodeType.SWITCH, "ethernet switch"),
|
||||
(ImageEnum.WLAN, NodeType.WIRELESS_LAN, "wireless LAN"),
|
||||
(ImageEnum.EMANE, NodeType.EMANE, "EMANE"),
|
||||
(ImageEnum.RJ45, NodeType.RJ45, "rj45 physical interface tool"),
|
||||
(ImageEnum.TUNNEL, NodeType.TUNNEL, "tunnel tool"),
|
||||
]
|
||||
for image_enum, node_type, tooltip in network_nodes:
|
||||
node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip)
|
||||
cls.NETWORK_NODES.append(node_draw)
|
||||
cls.NODE_ICONS[(node_type, None)] = node_draw.image
|
|
@ -54,7 +54,12 @@ def load(style):
|
|||
},
|
||||
},
|
||||
"TButton": {
|
||||
"configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED},
|
||||
"configure": {
|
||||
"width": 8,
|
||||
"padding": (5, 1),
|
||||
"relief": tk.RAISED,
|
||||
"anchor": tk.CENTER,
|
||||
},
|
||||
"map": {
|
||||
"relief": [("pressed", tk.SUNKEN)],
|
||||
"shiftrelief": [("pressed", 1)],
|
||||
|
|
|
@ -3,10 +3,10 @@ import tkinter as tk
|
|||
from functools import partial
|
||||
from tkinter import ttk
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from coretk.dialogs.customnodes import CustomNodesDialog
|
||||
from coretk.graph import GraphMode
|
||||
from coretk.images import ImageEnum, Images
|
||||
from coretk.nodeutils import NodeUtils
|
||||
from coretk.tooltip import Tooltip
|
||||
|
||||
WIDTH = 32
|
||||
|
@ -129,33 +129,16 @@ class Toolbar(ttk.Frame):
|
|||
def draw_node_picker(self):
|
||||
self.hide_pickers()
|
||||
self.node_picker = ttk.Frame(self.master)
|
||||
nodes = [
|
||||
(ImageEnum.ROUTER, core_pb2.NodeType.DEFAULT, "router"),
|
||||
(ImageEnum.HOST, core_pb2.NodeType.DEFAULT, "host"),
|
||||
(ImageEnum.PC, core_pb2.NodeType.DEFAULT, "PC"),
|
||||
(ImageEnum.MDR, core_pb2.NodeType.DEFAULT, "mdr"),
|
||||
(ImageEnum.PROUTER, core_pb2.NodeType.DEFAULT, "prouter"),
|
||||
(ImageEnum.DOCKER, core_pb2.NodeType.DOCKER, "Docker"),
|
||||
(ImageEnum.LXC, core_pb2.NodeType.LXC, "LXC"),
|
||||
]
|
||||
# draw default nodes
|
||||
for image_enum, node_type, model in nodes:
|
||||
image = icon(image_enum)
|
||||
func = partial(
|
||||
self.update_button, self.node_button, image, node_type, model
|
||||
)
|
||||
self.create_picker_button(image, func, self.node_picker, model)
|
||||
for node_draw in NodeUtils.NODES:
|
||||
image = icon(node_draw.image_enum)
|
||||
func = partial(self.update_button, self.node_button, image, node_draw)
|
||||
self.create_picker_button(image, func, self.node_picker, node_draw.tooltip)
|
||||
# draw custom nodes
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
custom_node = self.app.core.custom_nodes[name]
|
||||
image = custom_node.image
|
||||
func = partial(
|
||||
self.update_button,
|
||||
self.node_button,
|
||||
image,
|
||||
core_pb2.NodeType.DEFAULT,
|
||||
name,
|
||||
)
|
||||
node_draw = self.app.core.custom_nodes[name]
|
||||
image = Images.get_custom(node_draw.image_file, WIDTH)
|
||||
func = partial(self.update_button, self.node_button, image, node_draw)
|
||||
self.create_picker_button(image, func, self.node_picker, name)
|
||||
# draw edit node
|
||||
image = icon(ImageEnum.EDITNODE)
|
||||
|
@ -227,15 +210,13 @@ class Toolbar(ttk.Frame):
|
|||
dialog = CustomNodesDialog(self.app, self.app)
|
||||
dialog.show()
|
||||
|
||||
def update_button(self, button, image, node_type, model=None):
|
||||
logging.info("update button(%s): %s", button, node_type)
|
||||
def update_button(self, button, image, node_draw):
|
||||
logging.info("update button(%s): %s", button, node_draw)
|
||||
self.hide_pickers()
|
||||
button.configure(image=image)
|
||||
button.image = image
|
||||
self.app.canvas.mode = GraphMode.NODE
|
||||
self.app.canvas.draw_node_image = image
|
||||
self.app.canvas.draw_node_type = node_type
|
||||
self.app.canvas.draw_node_model = model
|
||||
self.app.canvas.node_draw = node_draw
|
||||
|
||||
def hide_pickers(self):
|
||||
logging.info("hiding pickers")
|
||||
|
@ -271,21 +252,13 @@ class Toolbar(ttk.Frame):
|
|||
"""
|
||||
self.hide_pickers()
|
||||
self.network_picker = ttk.Frame(self.master)
|
||||
nodes = [
|
||||
(ImageEnum.HUB, core_pb2.NodeType.HUB, "ethernet hub"),
|
||||
(ImageEnum.SWITCH, core_pb2.NodeType.SWITCH, "ethernet switch"),
|
||||
(ImageEnum.WLAN, core_pb2.NodeType.WIRELESS_LAN, "wireless LAN"),
|
||||
(ImageEnum.EMANE, core_pb2.NodeType.EMANE, "EMANE"),
|
||||
(ImageEnum.RJ45, core_pb2.NodeType.RJ45, "rj45 physical interface tool"),
|
||||
(ImageEnum.TUNNEL, core_pb2.NodeType.TUNNEL, "tunnel tool"),
|
||||
]
|
||||
for image_enum, node_type, tooltip in nodes:
|
||||
image = icon(image_enum)
|
||||
for node_draw in NodeUtils.NETWORK_NODES:
|
||||
image = icon(node_draw.image_enum)
|
||||
self.create_picker_button(
|
||||
image,
|
||||
partial(self.update_button, self.network_button, image, node_type),
|
||||
partial(self.update_button, self.network_button, image, node_draw),
|
||||
self.network_picker,
|
||||
tooltip,
|
||||
node_draw.tooltip,
|
||||
)
|
||||
self.design_select(self.network_button)
|
||||
self.network_button.after(
|
||||
|
@ -294,7 +267,8 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def create_network_button(self):
|
||||
"""
|
||||
Create link-layer node button and the options that represent different link-layer node types
|
||||
Create link-layer node button and the options that represent different
|
||||
link-layer node types.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue