Merge pull request #477 from coreemu/feature/pygui-sidebar

pygui: added support for a details pane, can be toggled on/off, can b…
This commit is contained in:
bharnden 2020-06-25 10:38:35 -07:00 committed by GitHub
commit d746cfa935
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 226 additions and 17 deletions

View file

@ -3,7 +3,7 @@ import math
import tkinter as tk
from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar
from typing import Dict, Optional
from typing import Any, Dict, Optional, Type
import grpc
@ -11,11 +11,14 @@ from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar
WIDTH: int = 1000
@ -35,6 +38,9 @@ class Application(ttk.Frame):
self.canvas: Optional[CanvasGraph] = None
self.statusbar: Optional[StatusBar] = None
self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None
self.info_frame: Optional[InfoFrameBase] = None
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
# fonts
self.fonts_size: Dict[str, int] = {}
@ -113,16 +119,27 @@ class Application(ttk.Frame):
self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas()
self.draw_infobar()
self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self)
self.master.config(menu=self.menubar)
def draw_infobar(self) -> None:
self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED)
self.infobar.columnconfigure(0, weight=1)
self.infobar.rowconfigure(1, weight=1)
label_font = font.Font(weight=font.BOLD, underline=tk.TRUE)
label = ttk.Label(
self.infobar, text="Details", anchor=tk.CENTER, font=label_font
)
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(sticky="nsew", pady=1)
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -136,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew")
self.statusbar.grid(sticky="ew", columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get():
return
self.clear_info()
self.info_frame = frame_class(self.infobar, **kwargs)
self.info_frame.draw()
self.info_frame.grid(sticky="nsew")
def clear_info(self) -> None:
if self.info_frame:
self.info_frame.destroy()
self.info_frame = None
def default_info(self) -> None:
self.clear_info()
self.display_info(DefaultInfoFrame, app=self)
def show_info(self) -> None:
self.default_info()
self.infobar.grid(row=0, column=1, sticky="nsew")
def hide_info(self) -> None:
self.infobar.grid_forget()
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
logging.exception("app grpc exception", exc_info=e)

View file

View file

@ -0,0 +1,36 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
class InfoFrameBase(ttk.Frame):
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(master, padding=FRAME_PAD)
self.app: "Application" = app
def draw(self) -> None:
raise NotImplementedError
class DetailsFrame(ttk.Frame):
def __init__(self, master: tk.BaseWidget) -> None:
super().__init__(master)
self.columnconfigure(1, weight=1)
self.row = 0
def add_detail(self, label: str, value: str) -> None:
label = ttk.Label(self, text=label, anchor=tk.W)
label.grid(row=self.row, sticky=tk.EW, column=0, padx=PADX)
label = ttk.Label(self, text=value, anchor=tk.W, state=tk.DISABLED)
label.grid(row=self.row, sticky=tk.EW, column=1)
self.row += 1
def add_separator(self) -> None:
separator = ttk.Separator(self)
separator.grid(row=self.row, sticky=tk.EW, columnspan=2, pady=PADY)
self.row += 1

View file

@ -0,0 +1,19 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.frames.base import InfoFrameBase
if TYPE_CHECKING:
from core.gui.app import Application
class DefaultInfoFrame(InfoFrameBase):
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(master, app)
def draw(self) -> None:
label = ttk.Label(self, text="Click a Node/Link", anchor=tk.CENTER)
label.grid(sticky=tk.EW)
label = ttk.Label(self, text="to see details", anchor=tk.CENTER)
label.grid(sticky=tk.EW)

View file

@ -0,0 +1,58 @@
import tkinter as tk
from typing import TYPE_CHECKING
from core.gui.frames.base import DetailsFrame, InfoFrameBase
from core.gui.utils import bandwidth_text
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.edges import CanvasEdge
class EdgeInfoFrame(InfoFrameBase):
def __init__(
self, master: tk.BaseWidget, app: "Application", edge: "CanvasEdge"
) -> None:
super().__init__(master, app)
self.edge: "CanvasEdge" = edge
def draw(self) -> None:
self.columnconfigure(0, weight=1)
link = self.edge.link
options = link.options
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
dst_node = dst_canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.add_detail("Source", src_node.name)
iface1 = link.iface1
if iface1:
mac = iface1.mac if iface1.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else ""
frame.add_detail("IP6", ip6)
frame.add_separator()
frame.add_detail("Destination", dst_node.name)
iface2 = link.iface2
if iface2:
mac = iface2.mac if iface2.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else ""
frame.add_detail("IP6", ip6)
if link.HasField("options"):
frame.add_separator()
bandwidth = bandwidth_text(options.bandwidth)
frame.add_detail("Bandwidth", bandwidth)
frame.add_detail("Delay", f"{options.delay} us")
frame.add_detail("Jitter", f"\u00B1{options.jitter} us")
frame.add_detail("Loss", f"{options.loss}%")
frame.add_detail("Duplicate", f"{options.dup}%")

View file

@ -0,0 +1,33 @@
from typing import TYPE_CHECKING
from core.api.grpc.core_pb2 import NodeType
from core.gui.frames.base import DetailsFrame, InfoFrameBase
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeInfoFrame(InfoFrameBase):
def __init__(self, master, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(master, app)
self.canvas_node: "CanvasNode" = canvas_node
def draw(self) -> None:
self.columnconfigure(0, weight=1)
node = self.canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.add_detail("ID", node.id)
frame.add_detail("Name", node.name)
if NodeUtils.is_model_node(node.type):
frame.add_detail("Type", node.model)
if node.type == NodeType.EMANE:
emane = node.emane.split("_")[1:]
frame.add_detail("EMANE", emane)
if NodeUtils.is_image_node(node.type):
frame.add_detail("Image", node.image)
if NodeUtils.is_container_node(node.type):
server = node.server if node.server else "localhost"
frame.add_detail("Server", server)

View file

@ -7,8 +7,10 @@ from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
@ -57,18 +59,6 @@ def arc_edges(edges) -> None:
edge.redraw()
def bandwidth_label(bandwidth: int) -> str:
size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"}
unit = 1000
i = 0
while bandwidth > unit:
bandwidth /= unit
i += 1
if i == 3:
break
return f"{bandwidth} {size[i]}"
class Edge:
tag: str = tags.EDGE
@ -295,6 +285,7 @@ class CanvasEdge(Edge):
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_link(self, link: Link) -> None:
self.link = link
@ -396,6 +387,9 @@ class CanvasEdge(Edge):
self.middle_label = None
self.canvas.itemconfig(self.id, 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)
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)
@ -413,7 +407,7 @@ class CanvasEdge(Edge):
lines = []
bandwidth = options.bandwidth
if bandwidth > 0:
lines.append(bandwidth_label(bandwidth))
lines.append(bandwidth_text(bandwidth))
delay = options.delay
jitter = options.jitter
if delay > 0 and jitter > 0:

View file

@ -715,6 +715,7 @@ class CanvasGraph(tk.Canvas):
logging.debug("press delete key")
if not self.app.core.is_runtime():
self.delete_selected_objects()
self.app.default_info()
else:
logging.debug("node deletion is disabled during runtime state")

View file

@ -16,6 +16,7 @@ from core.gui.dialogs.nodeconfig import NodeConfigDialog
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip
@ -80,6 +81,7 @@ class CanvasNode:
self.canvas.tag_bind(self.id, "<Enter>", self.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.on_leave)
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def delete(self) -> None:
logging.debug("Delete canvas node for %s", self.core_node)
@ -195,6 +197,9 @@ class CanvasNode:
else:
self.show_config()
def show_info(self, _event: tk.Event) -> None:
self.app.display_info(NodeInfoFrame, app=self.app, canvas_node=self)
def show_context(self, event: tk.Event) -> None:
# clear existing menu
self.context.delete(0, tk.END)
@ -262,6 +267,7 @@ class CanvasNode:
def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge)
self.app.default_info()
def canvas_delete(self) -> None:
self.canvas.clear_selection()

View file

@ -138,6 +138,11 @@ class Menubar(tk.Menu):
Create view menu
"""
menu = tk.Menu(self)
menu.add_checkbutton(
label="Details Panel",
command=self.click_infobar_change,
variable=self.app.show_infobar,
)
menu.add_checkbutton(
label="Interface Names",
command=self.click_edge_label_change,
@ -443,6 +448,12 @@ class Menubar(tk.Menu):
y = (row * layout_size) + padding
node.move(x, y)
def click_infobar_change(self) -> None:
if self.app.show_infobar.get():
self.app.show_info()
else:
self.app.hide_info()
def click_edge_label_change(self) -> None:
for edge in self.canvas.edges.values():
edge.draw_labels()

View file

@ -26,7 +26,7 @@ class ProgressTask:
self.time: Optional[float] = None
def start(self) -> None:
self.app.progress.grid(sticky="ew")
self.app.progress.grid(sticky="ew", columnspan=2)
self.app.progress.start()
self.time = time.perf_counter()
thread = threading.Thread(target=self.run, daemon=True)

10
daemon/core/gui/utils.py Normal file
View file

@ -0,0 +1,10 @@
def bandwidth_text(bandwidth: int) -> str:
size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"}
unit = 1000
i = 0
while bandwidth > unit:
bandwidth /= unit
i += 1
if i == 3:
break
return f"{bandwidth} {size[i]}"