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 import tkinter as tk
from tkinter import PhotoImage, font, ttk from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar from tkinter.ttk import Progressbar
from typing import Dict, Optional from typing import Any, Dict, Optional, Type
import grpc import grpc
@ -11,11 +11,14 @@ from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog 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.graph import CanvasGraph
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
WIDTH: int = 1000 WIDTH: int = 1000
@ -35,6 +38,9 @@ class Application(ttk.Frame):
self.canvas: Optional[CanvasGraph] = None self.canvas: Optional[CanvasGraph] = None
self.statusbar: Optional[StatusBar] = None self.statusbar: Optional[StatusBar] = None
self.progress: Optional[Progressbar] = 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 # fonts
self.fonts_size: Dict[str, int] = {} self.fonts_size: Dict[str, int] = {}
@ -113,16 +119,27 @@ class Application(ttk.Frame):
self.right_frame.rowconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew") self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas() self.draw_canvas()
self.draw_infobar()
self.draw_status() self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate") self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self) self.menubar = Menubar(self)
self.master.config(menu=self.menubar) 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: def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame) canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1) canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(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 = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew") self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -136,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None: def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self) 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: def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
logging.exception("app grpc exception", exc_info=e) 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.api.grpc.core_pb2 import Interface, Link
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -57,18 +59,6 @@ def arc_edges(edges) -> None:
edge.redraw() 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: class Edge:
tag: str = tags.EDGE tag: str = tags.EDGE
@ -295,6 +285,7 @@ class CanvasEdge(Edge):
def set_binding(self) -> None: def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context) 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: def set_link(self, link: Link) -> None:
self.link = link self.link = link
@ -396,6 +387,9 @@ class CanvasEdge(Edge):
self.middle_label = None self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) 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: def show_context(self, event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state) self.context.entryconfigure(1, state=state)
@ -413,7 +407,7 @@ class CanvasEdge(Edge):
lines = [] lines = []
bandwidth = options.bandwidth bandwidth = options.bandwidth
if bandwidth > 0: if bandwidth > 0:
lines.append(bandwidth_label(bandwidth)) lines.append(bandwidth_text(bandwidth))
delay = options.delay delay = options.delay
jitter = options.jitter jitter = options.jitter
if delay > 0 and jitter > 0: if delay > 0 and jitter > 0:

View file

@ -715,6 +715,7 @@ class CanvasGraph(tk.Canvas):
logging.debug("press delete key") logging.debug("press delete key")
if not self.app.core.is_runtime(): if not self.app.core.is_runtime():
self.delete_selected_objects() self.delete_selected_objects()
self.app.default_info()
else: else:
logging.debug("node deletion is disabled during runtime state") 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.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip 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, "<Enter>", self.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.on_leave) 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, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def delete(self) -> None: def delete(self) -> None:
logging.debug("Delete canvas node for %s", self.core_node) logging.debug("Delete canvas node for %s", self.core_node)
@ -195,6 +197,9 @@ class CanvasNode:
else: else:
self.show_config() 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: def show_context(self, event: tk.Event) -> None:
# clear existing menu # clear existing menu
self.context.delete(0, tk.END) self.context.delete(0, tk.END)
@ -262,6 +267,7 @@ class CanvasNode:
def click_unlink(self, edge: CanvasEdge) -> None: def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge) self.canvas.delete_edge(edge)
self.app.default_info()
def canvas_delete(self) -> None: def canvas_delete(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()

View file

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

View file

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