pygui: added support for a details pane, can be toggled on/off, can be used to quickly view details for nodes or links
This commit is contained in:
parent
bb2ceaf993
commit
f582306bb9
12 changed files with 226 additions and 17 deletions
|
@ -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)
|
||||||
|
|
0
daemon/core/gui/frames/__init__.py
Normal file
0
daemon/core/gui/frames/__init__.py
Normal file
36
daemon/core/gui/frames/base.py
Normal file
36
daemon/core/gui/frames/base.py
Normal 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
|
19
daemon/core/gui/frames/default.py
Normal file
19
daemon/core/gui/frames/default.py
Normal 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)
|
58
daemon/core/gui/frames/link.py
Normal file
58
daemon/core/gui/frames/link.py
Normal 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}%")
|
33
daemon/core/gui/frames/node.py
Normal file
33
daemon/core/gui/frames/node.py
Normal 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)
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
10
daemon/core/gui/utils.py
Normal 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]}"
|
Loading…
Add table
Reference in a new issue