From 62c0011caa0cbaf085d9cc0f4581d787a9952b18 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 28 Apr 2020 09:35:21 -0700 Subject: [PATCH 1/7] avoid configuring links for wireless networks --- daemon/core/emulator/session.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 7d1d3228..95aa7c0b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -357,7 +357,9 @@ class Session: ) interface = create_interface(node_one, net_one, interface_one) node_one_interface = interface - link_config(net_one, interface, link_options) + wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) + if not wireless_net: + link_config(net_one, interface, link_options) # network to node if node_two and net_one: @@ -368,7 +370,8 @@ class Session: ) interface = create_interface(node_two, net_one, interface_two) node_two_interface = interface - if not link_options.unidirectional: + wireless_net = isinstance(net_one, (EmaneNet, WlanNode)) + if not link_options.unidirectional and not wireless_net: link_config(net_one, interface, link_options) # network to network From 275e8f4c30f6f4012e30f776711a213cc7ff25f5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:19:40 -0700 Subject: [PATCH 2/7] finish writing a Find tool that allows find a node based on node name --- daemon/core/gui/dialogs/find.py | 144 ++++++++++++++++++++++++++++++++ daemon/core/gui/menubar.py | 7 ++ 2 files changed, 151 insertions(+) create mode 100644 daemon/core/gui/dialogs/find.py diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py new file mode 100644 index 00000000..73cc459b --- /dev/null +++ b/daemon/core/gui/dialogs/find.py @@ -0,0 +1,144 @@ +import logging +import tkinter as tk +from tkinter import ttk + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY + + +class FindDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Find", modal=True) + + self.find_text = tk.StringVar(value="") + self.tree = None + self.draw() + self.protocol("WM_DELETE_WINDOW", self.close_dialog) + self.bind("", self.find_node) + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=5) + self.top.rowconfigure(2, weight=1) + + # Find node frame + frame = ttk.Frame(self.top, padding=FRAME_PAD) + frame.grid(sticky="nsew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Find:") + label.grid() + entry = ttk.Entry(frame, textvariable=self.find_text) + entry.grid(row=0, column=1, sticky="nsew") + + # node list frame + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", padx=PADX, pady=PADY) + self.tree = ttk.Treeview( + frame, + columns=("nodeid", "name", "location", "detail"), + show="headings", + selectmode=tk.BROWSE, + ) + self.tree.grid(sticky="nsew") + style = ttk.Style() + heading_size = int(self.app.guiconfig["scale"] * 10) + style.configure("Treeview.Heading", font=(None, heading_size, "bold")) + self.tree.column("nodeid", stretch=tk.YES, anchor="center") + self.tree.heading("nodeid", text="Node ID") + self.tree.column("name", stretch=tk.YES, anchor="center") + self.tree.heading("name", text="Name") + self.tree.column("location", stretch=tk.YES, anchor="center") + self.tree.heading("location", text="Location") + self.tree.column("detail", stretch=tk.YES, anchor="center") + self.tree.heading("detail", text="Detail") + + self.tree.bind("<>", self.click_select) + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + + # button frame + frame = ttk.Frame(self.top) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Find", command=self.find_node) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.close_dialog) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + + def clear_treeview_items(self): + """ + clear all items in the treeview + :return: + """ + for i in list(self.tree.get_children("")): + self.tree.delete(i) + + def find_node(self, event=None): + """ + Query nodes that have the same node name as our search key, + display results to tree view + """ + node_name = self.find_text.get().strip() + self.clear_treeview_items() + for node_id, node in sorted( + self.app.core.canvas_nodes.items(), key=lambda x: x[0] + ): + name = node.core_node.name + if not node_name or node_name == name: + location = f"<{node.core_node.position.x}, {node.core_node.position.y}>" + # TODO I am not sure what to insert for Detail column, leaving in blank for now + self.tree.insert( + "", tk.END, text=str(node_id), values=(node_id, name, location, "") + ) + + results = self.tree.get_children("") + if results: + self.tree.selection_set(results[0]) + + def close_dialog(self): + self.app.canvas.delete("find") + self.destroy() + + def click_select(self, _event: tk.Event = None) -> None: + item = self.tree.selection() + if item: + self.app.canvas.delete("find") + node_id = int(self.tree.item(item, "text")) + canvas_node = self.app.core.canvas_nodes[node_id] + + x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id) + dist = 5 * self.app.guiconfig["scale"] + self.app.canvas.create_oval( + x0 - dist, + y0 - dist, + x1 + dist, + y1 + dist, + tags="find", + outline="red", + 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]) + logging.debug("Dist to most left: %s", abs(x0 - _x)) + logging.debug("White canvas width: %s", abs(x0 - x1)) + + # calculate the node's location + # (as fractions of white canvas's width and height) + # and instantly scroll the x and y scrollbar to that location + + xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) + yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) + self.app.canvas.xview_moveto(xscroll_fraction) + self.app.canvas.yview_moveto(yscroll_fraction) diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index 74a93df7..fafe6b5c 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -11,6 +11,7 @@ from core.gui.dialogs.about import AboutDialog from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog from core.gui.dialogs.executepython import ExecutePythonDialog +from core.gui.dialogs.find import FindDialog from core.gui.dialogs.hooks import HooksDialog from core.gui.dialogs.ipdialog import IpConfigDialog from core.gui.dialogs.macdialog import MacConfigDialog @@ -114,6 +115,7 @@ class Menubar(tk.Menu): Create edit menu """ menu = tk.Menu(self) + menu.add_command(label="Find", accelerator="Ctrl+F", command=self.click_find) menu.add_command(label="Preferences", command=self.click_preferences) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) @@ -125,6 +127,7 @@ class Menubar(tk.Menu): label="Delete", accelerator="Ctrl+D", command=self.click_delete ) self.add_cascade(label="Edit", menu=menu) + self.app.master.bind_all("", self.click_find) self.app.master.bind_all("", self.click_cut) self.app.master.bind_all("", self.click_copy) self.app.master.bind_all("", self.click_paste) @@ -397,6 +400,10 @@ class Menubar(tk.Menu): self.core.create_new_session() self.core.xml_file = None + def click_find(self, _event: tk.Event = None) -> None: + dialog = FindDialog(self.app, self.app) + dialog.show() + def click_preferences(self) -> None: dialog = PreferencesDialog(self.app, self.app) dialog.show() From c45202e61b09c2f07734ea47e388b343ed398511 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:36:12 -0700 Subject: [PATCH 3/7] add type checking to class methods --- daemon/core/gui/dialogs/find.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 73cc459b..190d9410 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -7,7 +7,7 @@ from core.gui.themes import FRAME_PAD, PADX, PADY class FindDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app) -> None: super().__init__(master, app, "Find", modal=True) self.find_text = tk.StringVar(value="") @@ -16,7 +16,7 @@ class FindDialog(Dialog): self.protocol("WM_DELETE_WINDOW", self.close_dialog) self.bind("", self.find_node) - def draw(self): + def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.top.rowconfigure(1, weight=5) @@ -75,15 +75,14 @@ class FindDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.close_dialog) button.grid(row=0, column=1, sticky="ew", padx=PADX) - def clear_treeview_items(self): + def clear_treeview_items(self) -> None: """ clear all items in the treeview - :return: """ for i in list(self.tree.get_children("")): self.tree.delete(i) - def find_node(self, event=None): + def find_node(self, _event: tk.Event = None) -> None: """ Query nodes that have the same node name as our search key, display results to tree view @@ -105,11 +104,16 @@ class FindDialog(Dialog): if results: self.tree.selection_set(results[0]) - def close_dialog(self): + def close_dialog(self) -> None: self.app.canvas.delete("find") self.destroy() def click_select(self, _event: tk.Event = None) -> None: + """ + find the node that matches search criteria, circle around that node + and scroll the x and y scrollbar to be able to see the node if + it is out of sight + """ item = self.tree.selection() if item: self.app.canvas.delete("find") @@ -137,7 +141,7 @@ class FindDialog(Dialog): # calculate the node's location # (as fractions of white canvas's width and height) # and instantly scroll the x and y scrollbar to that location - + # looks a bit ugly when zoom too much xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) self.app.canvas.xview_moveto(xscroll_fraction) From e9ca4a5b581c07a8c569806e107b4cd6b52cb806 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 16:53:06 -0700 Subject: [PATCH 4/7] Session dialog: Bold heading text so that it stands out more, allign heading text with column text --- daemon/core/gui/dialogs/sessions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index 6a3cf380..3671b308 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -72,12 +72,15 @@ class SessionsDialog(Dialog): show="headings", selectmode=tk.BROWSE, ) + style = ttk.Style() + heading_size = int(self.app.guiconfig["scale"] * 10) + style.configure("Treeview.Heading", font=(None, heading_size, "bold")) self.tree.grid(sticky="nsew") - self.tree.column("id", stretch=tk.YES) + self.tree.column("id", stretch=tk.YES, anchor="center") self.tree.heading("id", text="ID") - self.tree.column("state", stretch=tk.YES) + self.tree.column("state", stretch=tk.YES, anchor="center") self.tree.heading("state", text="State") - self.tree.column("nodes", stretch=tk.YES) + self.tree.column("nodes", stretch=tk.YES, anchor="center") self.tree.heading("nodes", text="Node Count") for index, session in enumerate(self.sessions): @@ -213,3 +216,5 @@ class SessionsDialog(Dialog): def on_closing(self) -> None: if self.is_start_app and messagebox.askokcancel("Exit", "Quit?", parent=self): self.click_exit() + if not self.is_start_app: + self.destroy() From 64657b20a816e16307b6c4e24996d7e445fa207e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 17:09:17 -0700 Subject: [PATCH 5/7] add more logic to scrolling the scrollbar to get a bit nicer view --- daemon/core/gui/dialogs/find.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 190d9410..e0500da3 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -141,8 +141,14 @@ class FindDialog(Dialog): # calculate the node's location # (as fractions of white canvas's width and height) # and instantly scroll the x and y scrollbar to that location - # looks a bit ugly when zoom too much xscroll_fraction = abs(x0 - _x) / abs(x0 - x1) yscroll_fraction = abs(y0 - _y) / abs(y0 - y1) + # scroll a little more to the left or a little bit up so that the node + # doesn't always fall in the most top-left corner + for i in range(2): + if xscroll_fraction > 0.05: + 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) From 1f8d16df0890fbc9b5bb0482f0309f27294d5140 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 29 Apr 2020 17:17:57 -0700 Subject: [PATCH 6/7] touch up --- daemon/core/gui/dialogs/find.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index e0500da3..4d7bc6bd 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -42,7 +42,7 @@ class FindDialog(Dialog): show="headings", selectmode=tk.BROWSE, ) - self.tree.grid(sticky="nsew") + self.tree.grid(sticky="nsew", pady=PADY) style = ttk.Style() heading_size = int(self.app.guiconfig["scale"] * 10) style.configure("Treeview.Heading", font=(None, heading_size, "bold")) @@ -94,10 +94,14 @@ class FindDialog(Dialog): ): name = node.core_node.name if not node_name or node_name == name: - location = f"<{node.core_node.position.x}, {node.core_node.position.y}>" + pos_x = round(node.core_node.position.x, 1) + pos_y = round(node.core_node.position.y, 1) # TODO I am not sure what to insert for Detail column, leaving in blank for now self.tree.insert( - "", tk.END, text=str(node_id), values=(node_id, name, location, "") + "", + tk.END, + text=str(node_id), + values=(node_id, name, f"<{pos_x}, {pos_y}>", ""), ) results = self.tree.get_children("") From 47ef5ec14dd49e883f413bcac1d1badc0948bb1b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 30 Apr 2020 11:19:23 -0700 Subject: [PATCH 7/7] avoid writing link options to xml for emane/wlan links --- daemon/core/xml/corexml.py | 47 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 6ad210b8..891db1cd 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -13,7 +13,7 @@ from core.errors import CoreXmlError from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode -from core.nodes.network import CtrlNet +from core.nodes.network import CtrlNet, WlanNode from core.services.coreservices import CoreService if TYPE_CHECKING: @@ -559,26 +559,31 @@ class CoreXmlWriter: ) link_element.append(interface_two) - # check for options - options = etree.Element("options") - add_attribute(options, "delay", link_data.delay) - add_attribute(options, "bandwidth", link_data.bandwidth) - add_attribute(options, "per", link_data.per) - add_attribute(options, "dup", link_data.dup) - add_attribute(options, "jitter", link_data.jitter) - add_attribute(options, "mer", link_data.mer) - add_attribute(options, "burst", link_data.burst) - add_attribute(options, "mburst", link_data.mburst) - add_attribute(options, "type", link_data.link_type) - add_attribute(options, "gui_attributes", link_data.gui_attributes) - add_attribute(options, "unidirectional", link_data.unidirectional) - add_attribute(options, "emulation_id", link_data.emulation_id) - add_attribute(options, "network_id", link_data.network_id) - add_attribute(options, "key", link_data.key) - add_attribute(options, "opaque", link_data.opaque) - add_attribute(options, "session", link_data.session) - if options.items(): - link_element.append(options) + # check for options, don't write for emane/wlan links + node_one = self.session.get_node(link_data.node1_id) + node_two = self.session.get_node(link_data.node2_id) + is_node_one_wireless = isinstance(node_one, (WlanNode, EmaneNet)) + is_node_two_wireless = isinstance(node_two, (WlanNode, EmaneNet)) + if not any([is_node_one_wireless, is_node_two_wireless]): + options = etree.Element("options") + add_attribute(options, "delay", link_data.delay) + add_attribute(options, "bandwidth", link_data.bandwidth) + add_attribute(options, "per", link_data.per) + add_attribute(options, "dup", link_data.dup) + add_attribute(options, "jitter", link_data.jitter) + add_attribute(options, "mer", link_data.mer) + add_attribute(options, "burst", link_data.burst) + add_attribute(options, "mburst", link_data.mburst) + add_attribute(options, "type", link_data.link_type) + add_attribute(options, "gui_attributes", link_data.gui_attributes) + add_attribute(options, "unidirectional", link_data.unidirectional) + add_attribute(options, "emulation_id", link_data.emulation_id) + add_attribute(options, "network_id", link_data.network_id) + add_attribute(options, "key", link_data.key) + add_attribute(options, "opaque", link_data.opaque) + add_attribute(options, "session", link_data.session) + if options.items(): + link_element.append(options) return link_element