fix merge conflict, add a logging error to temporarily solve issue removing a wireless link when multiple wireless links exist

This commit is contained in:
Huy Pham 2020-02-20 10:47:59 -08:00
commit 08d4bf98c7
19 changed files with 407 additions and 102 deletions

View file

@ -1,3 +1,35 @@
## 2020-02-20 CORE 6.1.0
* New
* config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services
* core-imn-to-xml - IMN to XML utility script
* replaced internal code for determining ip/mac address with netaddr library
* Enhancements
* added distributed package for built packages
* made use of python type hinting for functions and their return values
* updated Quagga zebra service to remove deprecated warning
* Removed
* removed stale ns3 code
* CORETK GUI
* added logging
* improved error dialog
* properly use global ipv6 addresses for nodes
* disable proxy usage by default, flag available to enable
* gRPC API
* add_link - now returns created interface information
* set_node_service - can now set files and directories to properly replicate previous usage
* get_emane_event_channel - return information related to the currently used emane event channel
* Bugfixes
* fixed session SDT functionality back to working order, due to python3 changes
* avoid shutting down services for nodes that are not up
* EMANE bypass model options will now display properly in GUIs
* XML scenarios will now properly read in custom node icons
* \#372 - fixed mobility waypoint comparisons
* \#370 - fixed radvd service
* \#368 - updated frr services to properly start staticd when needed
* \#358 - fixed systemd service install path
* \#350 - fixed frr babel wireless configuration
* \#354 - updated frr to reset interfaces to properly take configurations
## 2020-01-01 CORE 6.0.0 ## 2020-01-01 CORE 6.0.0
* New * New
* beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough * beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough
@ -114,11 +146,11 @@
* Added EMANE prefix configuration when looking for emane model manifest files * Added EMANE prefix configuration when looking for emane model manifest files
* requires configuring **emane_prefix** in /etc/core/core.conf * requires configuring **emane_prefix** in /etc/core/core.conf
* Cleanup * Cleanup
* Refactoring of the core python package structure, trying to help provide better organization and * Refactoring of the core python package structure, trying to help provide better organization and
logical groupings logical groupings
* Issues * Issues
* \#246 - Fixed network to network link handling when reading xml files * \#246 - Fixed network to network link handling when reading xml files
* \#236 - Fixed storing/reading of link configuration values within xml files * \#236 - Fixed storing/reading of link configuration values within xml files
* \#170 - FRR Service * \#170 - FRR Service
* \#155 - EMANE path configuration * \#155 - EMANE path configuration
* \#233 - Python 3 support * \#233 - Python 3 support
@ -152,16 +184,16 @@
## 2018-05-22 CORE 5.1 ## 2018-05-22 CORE 5.1
* DAEMON: * DAEMON:
* removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB * removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB
specific files) specific files)
* default nodes are now set in the node map * default nodes are now set in the node map
* moved ns3 and netns directories to the top of the repo * moved ns3 and netns directories to the top of the repo
* changes to make use of fpm as the tool for building packages * changes to make use of fpm as the tool for building packages
* removed usage of logzero to avoid dependency issues for built packages * removed usage of logzero to avoid dependency issues for built packages
* removed daemon addons directory * removed daemon addons directory
* added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting * added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting
and creating new external APIs out of and creating new external APIs out of
* cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and * cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and
informative informative
* EMANE 1.0.1-1.21 supported * EMANE 1.0.1-1.21 supported
* updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files * updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files
@ -175,7 +207,7 @@
* updated broken help links in GUI Help->About * updated broken help links in GUI Help->About
* Packaging: * Packaging:
* fixed PYTHON_PATH to PYTHONPATH in sysv script * fixed PYTHON_PATH to PYTHONPATH in sysv script
* added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation * added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation
within README.md to try it out within README.md to try it out
* TEST: * TEST:
* fixed some broken tests * fixed some broken tests
@ -184,7 +216,7 @@
* \#142 - duplication of custom services * \#142 - duplication of custom services
* \#136 - sphinx-apidoc command not found * \#136 - sphinx-apidoc command not found
* \#137 - make command fails when using distclean * \#137 - make command fails when using distclean
## 2017-09-01 CORE 5.0 ## 2017-09-01 CORE 5.0
* DEVELOPMENT: * DEVELOPMENT:
* support for editorconfig to help standardize development across IDEs, from the defined configuration file * support for editorconfig to help standardize development across IDEs, from the defined configuration file
@ -339,7 +371,7 @@
* added "--addons" startup mode to pass control to code included from addons dir * added "--addons" startup mode to pass control to code included from addons dir
* added "Locked" entry to View menu to prevent moving items * added "Locked" entry to View menu to prevent moving items
* use currently selected node type when invoking a topology generator * use currently selected node type when invoking a topology generator
* updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot * updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot
configuration with imn file configuration with imn file
* improved session dialog * improved session dialog
* EMANE: * EMANE:
@ -356,11 +388,11 @@
* XML import and export * XML import and export
* renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg" * renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg"
* code reorganization and clean-up * code reorganization and clean-up
* updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new * updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new
"Save As XML..." File menu entry "Save As XML..." File menu entry
* added script_start/pause/stop options to Ns2ScriptedMobility * added script_start/pause/stop options to Ns2ScriptedMobility
* "python" source sub-directory renamed to "daemon" * "python" source sub-directory renamed to "daemon"
* added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for * added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for
GUI connection GUI connection
* support comma-separated list for custom_services_dir in core.conf file * support comma-separated list for custom_services_dir in core.conf file
* updated kernel patches for Linux kernel 3.5 * updated kernel patches for Linux kernel 3.5
@ -369,7 +401,7 @@
* integrate ns-3 node location between CORE and ns-3 simulation * integrate ns-3 node location between CORE and ns-3 simulation
* added ns-3 random walk mobility example * added ns-3 random walk mobility example
* updated ns-3 Wifi example to allow GUI connection and moving of nodes * updated ns-3 Wifi example to allow GUI connection and moving of nodes
* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176, * fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176,
184, 190, 193 184, 190, 193
## 2012-09-25 CORE 4.4 ## 2012-09-25 CORE 4.4
@ -410,7 +442,7 @@
* support /etc/core/environment and ~/.core/environment files * support /etc/core/environment and ~/.core/environment files
* added Ns2ScriptedMobility model to Python, removed from the GUI * added Ns2ScriptedMobility model to Python, removed from the GUI
* namespace nodes mount a private /sys * namespace nodes mount a private /sys
* fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146, * fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146,
147, 151, 154, 155 147, 151, 154, 155
## 2012-03-07 CORE 4.3 ## 2012-03-07 CORE 4.3

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # this defines the CORE version number, must be static for AC_INIT
AC_INIT(core, 6.0.0) AC_INIT(core, 6.1.0)
# autoconf and automake initialization # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -74,6 +74,9 @@ bootfrr()
fi fi
bootdaemon "zebra" bootdaemon "zebra"
if grep -q "^ip route " $FRR_CONF; then
bootdaemon "staticd"
fi
for r in rip ripng ospf6 ospf bgp babel; do for r in rip ripng ospf6 ospf bgp babel; do
if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then
bootdaemon "$${}{r}d" bootdaemon "$${}{r}d"
@ -93,3 +96,10 @@ if [ "$1" != "zebra" ]; then
fi fi
confcheck confcheck
bootfrr bootfrr
# reset interfaces
% for ifc, _, _ , _ in interfaces:
ip link set dev ${ifc.name} down
sleep 1
ip link set dev ${ifc.name} up
% endfor

View file

@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import font, ttk
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -29,8 +29,15 @@ class Application(tk.Frame):
self.statusbar = None self.statusbar = None
self.validation = None self.validation = None
# fonts
self.fonts_size = None
self.icon_text_font = None
self.edge_font = None
# setup # setup
self.guiconfig = appconfig.read() self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig["scale"]
self.setup_scaling()
self.style = ttk.Style() self.style = ttk.Style()
self.setup_theme() self.setup_theme()
self.core = CoreClient(self, proxy) self.core = CoreClient(self, proxy)
@ -38,6 +45,14 @@ class Application(tk.Frame):
self.draw() self.draw()
self.core.set_up() self.core.set_up()
def setup_scaling(self):
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
themes.scale_fonts(self.fonts_size, self.app_scale)
self.icon_text_font = font.Font(
family="TkIconFont", size=int(12 * self.app_scale)
)
self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * self.app_scale))
def setup_theme(self): def setup_theme(self):
themes.load(self.style) themes.load(self.style)
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu) self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
@ -56,9 +71,11 @@ class Application(tk.Frame):
def center(self): def center(self):
screen_width = self.master.winfo_screenwidth() screen_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight() screen_height = self.master.winfo_screenheight()
x = int((screen_width / 2) - (WIDTH / 2)) x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
y = int((screen_height / 2) - (HEIGHT / 2)) y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") self.master.geometry(
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
)
def draw(self): def draw(self):
self.master.option_add("*tearOff", tk.FALSE) self.master.option_add("*tearOff", tk.FALSE)

View file

@ -96,6 +96,7 @@ def check_directory():
"nodes": [], "nodes": [],
"recentfiles": [], "recentfiles": [],
"observers": [{"name": "hello", "cmd": "echo hello"}], "observers": [{"name": "hello", "cmd": "echo hello"}],
"scale": 1.0,
} }
save(config) save(config)

View file

@ -794,7 +794,7 @@ class CoreClient:
image=image, image=image,
emane=emane, emane=emane,
) )
if NodeUtils.is_custom(model): if NodeUtils.is_custom(node_type, model):
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model) services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
node.services[:] = services node.services[:] = services
logging.info( logging.info(

View file

@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog):
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.START, width=ICON_SIZE) image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale))
self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button = ttk.Button(frame, image=image, command=self.click_play)
self.play_button.image = image self.play_button.image = image
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale))
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
self.pause_button.image = image self.pause_button.image = image
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.STOP, width=ICON_SIZE) image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale))
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)

View file

@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog):
if len(services) == 0: if len(services) == 0:
# not custom node type and node's services haven't been modified before # not custom node type and node's services haven't been modified before
if not NodeUtils.is_custom( if not NodeUtils.is_custom(
canvas_node.core_node.model canvas_node.core_node.type, canvas_node.core_node.model
) and not self.app.core.service_been_modified(self.node_id): ) and not self.app.core.service_been_modified(self.node_id):
services = set(self.app.core.default_services[model]) services = set(self.app.core.default_services[model])
# services of default type nodes were modified to be empty # services of default type nodes were modified to be empty

View file

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from core.gui import appconfig from core.gui import appconfig
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -14,6 +14,7 @@ if TYPE_CHECKING:
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Preferences", modal=True) super().__init__(master, app, "Preferences", modal=True)
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
preferences = self.app.guiconfig["preferences"] preferences = self.app.guiconfig["preferences"]
self.editor = tk.StringVar(value=preferences["editor"]) self.editor = tk.StringVar(value=preferences["editor"])
self.theme = tk.StringVar(value=preferences["theme"]) self.theme = tk.StringVar(value=preferences["theme"])
@ -64,6 +65,26 @@ class PreferencesDialog(Dialog):
entry = ttk.Entry(frame, textvariable=self.gui3d) entry = ttk.Entry(frame, textvariable=self.gui3d)
entry.grid(row=3, column=1, sticky="ew") entry.grid(row=3, column=1, sticky="ew")
label = ttk.Label(frame, text="Scaling")
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
scale_frame = ttk.Frame(frame)
scale_frame.grid(row=4, column=1, sticky="ew")
scale_frame.columnconfigure(0, weight=1)
scale = ttk.Scale(
scale_frame,
from_=0.5,
to=5,
value=1,
orient=tk.HORIZONTAL,
variable=self.gui_scale,
)
scale.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry(
scale_frame, textvariable=self.gui_scale, width=4, state="disabled"
)
entry.grid(row=0, column=1)
def draw_buttons(self): def draw_buttons(self):
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
@ -87,5 +108,27 @@ class PreferencesDialog(Dialog):
preferences["editor"] = self.editor.get() preferences["editor"] = self.editor.get()
preferences["gui3d"] = self.gui3d.get() preferences["gui3d"] = self.gui3d.get()
preferences["theme"] = self.theme.get() preferences["theme"] = self.theme.get()
self.gui_scale.set(round(self.gui_scale.get(), 2))
app_scale = self.gui_scale.get()
self.app.guiconfig["scale"] = app_scale
self.app.save_config() self.app.save_config()
self.scale_adjust()
self.destroy() self.destroy()
def scale_adjust(self):
app_scale = self.gui_scale.get()
self.app.app_scale = app_scale
self.app.master.tk.call("tk", "scaling", app_scale)
# scale fonts
scale_fonts(self.app.fonts_size, app_scale)
self.app.icon_text_font.config(size=int(12 * app_scale))
self.app.edge_font.config(size=int(8 * app_scale))
# scale application window
self.app.center()
# scale toolbar and canvas items
self.app.toolbar.scale()
self.app.canvas.scale_graph()

View file

@ -1,6 +1,5 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter.font import Font
from typing import TYPE_CHECKING, Any, Tuple from typing import TYPE_CHECKING, Any, Tuple
from core.gui import themes from core.gui import themes
@ -14,6 +13,7 @@ if TYPE_CHECKING:
TEXT_DISTANCE = 0.30 TEXT_DISTANCE = 0.30
EDGE_WIDTH = 3 EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000" EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933" WIRELESS_COLOR = "#009933"
@ -32,7 +32,10 @@ class CanvasWirelessEdge:
self.dst = dst self.dst = dst
self.canvas = canvas self.canvas = canvas
self.id = self.canvas.create_line( self.id = self.canvas.create_line(
*position, tags=tags.WIRELESS_EDGE, width=EDGE_WIDTH, fill="#009933" *position,
tags=tags.WIRELESS_EDGE,
width=WIRELESS_WIDTH * self.canvas.app.app_scale,
fill=WIRELESS_COLOR,
) )
def delete(self): def delete(self):
@ -62,13 +65,18 @@ class CanvasEdge:
self.dst_interface = None self.dst_interface = None
self.canvas = canvas self.canvas = canvas
self.id = self.canvas.create_line( self.id = self.canvas.create_line(
x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, fill=EDGE_COLOR x1,
y1,
x2,
y2,
tags=tags.EDGE,
width=EDGE_WIDTH * self.canvas.app.app_scale,
fill=EDGE_COLOR,
) )
self.text_src = None self.text_src = None
self.text_dst = None self.text_dst = None
self.text_middle = None self.text_middle = None
self.token = None self.token = None
self.font = Font(size=8)
self.link = None self.link = None
self.asymmetric_link = None self.asymmetric_link = None
self.throughput = None self.throughput = None
@ -118,7 +126,7 @@ class CanvasEdge:
y1, y1,
text=label_one, text=label_one,
justify=tk.CENTER, justify=tk.CENTER,
font=self.font, font=self.canvas.app.edge_font,
tags=tags.LINK_INFO, tags=tags.LINK_INFO,
) )
self.text_dst = self.canvas.create_text( self.text_dst = self.canvas.create_text(
@ -126,7 +134,7 @@ class CanvasEdge:
y2, y2,
text=label_two, text=label_two,
justify=tk.CENTER, justify=tk.CENTER,
font=self.font, font=self.canvas.app.edge_font,
tags=tags.LINK_INFO, tags=tags.LINK_INFO,
) )
@ -147,7 +155,7 @@ class CanvasEdge:
if self.text_middle is None: if self.text_middle is None:
x, y = self.get_midpoint() x, y = self.get_midpoint()
self.text_middle = self.canvas.create_text( self.text_middle = self.canvas.create_text(
x, y, tags=tags.THROUGHPUT, font=self.font, text=value x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value
) )
else: else:
self.canvas.itemconfig(self.text_middle, text=value) self.canvas.itemconfig(self.text_middle, text=value)

View file

@ -7,12 +7,12 @@ from PIL import Image, ImageTk
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
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 EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge
from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.nodeutils import EdgeUtils, NodeUtils from core.gui.nodeutils import EdgeUtils, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
@ -224,10 +224,14 @@ class CanvasGraph(tk.Canvas):
# peer to peer node is not drawn on the GUI # peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type): if NodeUtils.is_ignore_node(core_node.type):
continue continue
image = NodeUtils.node_image(core_node, self.app.guiconfig) image = NodeUtils.node_image(
core_node, self.app.guiconfig, self.app.app_scale
)
# if the gui can't find node's image, default to the "edit-node" image # if the gui can't find node's image, default to the "edit-node" image
if not image: if not image:
image = Images.get(ImageEnum.EDITNODE, ICON_SIZE) image = Images.get(
ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)
)
x = core_node.position.x x = core_node.position.x
y = core_node.position.y y = core_node.position.y
node = CanvasNode(self.master, x, y, core_node, image) node = CanvasNode(self.master, x, y, core_node, image)
@ -667,6 +671,14 @@ class CanvasGraph(tk.Canvas):
core_node = self.core.create_node( core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
) )
try:
self.node_draw.image = Images.get(
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
)
except AttributeError:
self.node_draw.image = Images.get_custom(
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
)
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node self.nodes[node.id] = node
@ -915,3 +927,28 @@ class CanvasGraph(tk.Canvas):
width=self.itemcget(edge.id, "width"), width=self.itemcget(edge.id, "width"),
fill=self.itemcget(edge.id, "fill"), fill=self.itemcget(edge.id, "fill"),
) )
def scale_graph(self):
for nid, canvas_node in self.nodes.items():
img = None
if NodeUtils.is_custom(
canvas_node.core_node.type, canvas_node.core_node.model
):
for custom_node in self.app.guiconfig["nodes"]:
if custom_node["name"] == canvas_node.core_node.model:
img = Images.get_custom(
custom_node["image"], int(ICON_SIZE * self.app.app_scale)
)
else:
image_enum = TypeToImage.get(
canvas_node.core_node.type, canvas_node.core_node.model
)
img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale))
self.itemconfig(nid, image=img)
canvas_node.image = img
canvas_node.scale_text()
canvas_node.scale_antennas()
for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))

View file

@ -1,6 +1,5 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import font
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import grpc import grpc
@ -17,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.errors import show_grpc_error from core.gui.errors import show_grpc_error
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.nodeutils import EdgeUtils, NodeUtils from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, EdgeUtils, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -42,21 +42,21 @@ class CanvasNode:
self.id = self.canvas.create_image( self.id = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
) )
text_font = font.Font(family="TkIconFont", size=12)
label_y = self._get_label_y() label_y = self._get_label_y()
self.text_id = self.canvas.create_text( self.text_id = self.canvas.create_text(
x, x,
label_y, label_y,
text=self.core_node.name, text=self.core_node.name,
tags=tags.NODE_NAME, tags=tags.NODE_NAME,
font=text_font, font=self.app.icon_text_font,
fill="#0000CD", fill="#0000CD",
) )
self.tooltip = CanvasTooltip(self.canvas) self.tooltip = CanvasTooltip(self.canvas)
self.edges = set() self.edges = set()
self.interfaces = [] self.interfaces = []
self.wireless_edges = set() self.wireless_edges = set()
self.antennae = [] self.antennas = []
self.antenna_images = {}
self.setup_bindings() self.setup_bindings()
def setup_bindings(self): def setup_bindings(self):
@ -95,8 +95,13 @@ class CanvasNode:
if other == self.id: if other == self.id:
other = token[1] other = token[1]
self.canvas.nodes[other].wireless_edges.discard(wireless_edge) self.canvas.nodes[other].wireless_edges.discard(wireless_edge)
wlan_edge = self.canvas.wireless_edges.pop(token, None) try:
self.canvas.delete(wlan_edge.id) wlan_edge = self.canvas.wireless_edges.pop(token)
self.canvas.delete(wlan_edge.id)
except KeyError:
logging.error(
"wireless link not found, potentially multiple wireless link issue"
)
self.delete_antennas() self.delete_antennas()
self.wireless_edges.clear() self.wireless_edges.clear()
@ -106,33 +111,37 @@ class CanvasNode:
def add_antenna(self): def add_antenna(self):
x, y = self.canvas.coords(self.id) x, y = self.canvas.coords(self.id)
offset = len(self.antennae) * 8 offset = len(self.antennas) * 8 * self.app.app_scale
img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale))
antenna_id = self.canvas.create_image( antenna_id = self.canvas.create_image(
x - 16 + offset, x - 16 + offset,
y - 23, y - int(23 * self.app.app_scale),
anchor=tk.CENTER, anchor=tk.CENTER,
image=NodeUtils.ANTENNA_ICON, image=img,
tags=tags.ANTENNA, tags=tags.ANTENNA,
) )
self.antennae.append(antenna_id) self.antennas.append(antenna_id)
self.antenna_images[antenna_id] = img
def delete_antenna(self): def delete_antenna(self):
""" """
delete one antenna delete one antenna
""" """
logging.debug("Delete an antenna on %s", self.core_node.name) logging.debug("Delete an antenna on %s", self.core_node.name)
if self.antennae: if self.antennas:
antenna_id = self.antennae.pop() antenna_id = self.antennas.pop()
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antenna_images.pop(antenna_id, None)
def delete_antennas(self): def delete_antennas(self):
""" """
delete all antennas delete all antennas
""" """
logging.debug("Remove all antennas for %s", self.core_node.name) logging.debug("Remove all antennas for %s", self.core_node.name)
for antenna_id in self.antennae: for antenna_id in self.antennas:
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antennae.clear() self.antennas.clear()
self.antenna_images.clear()
def redraw(self): def redraw(self):
self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.id, image=self.image)
@ -142,6 +151,12 @@ class CanvasNode:
image_box = self.canvas.bbox(self.id) image_box = self.canvas.bbox(self.id)
return image_box[3] + NODE_TEXT_OFFSET return image_box[3] + NODE_TEXT_OFFSET
def scale_text(self):
text_bound = self.canvas.bbox(self.text_id)
prev_y = (text_bound[3] + text_bound[1]) / 2
new_y = self._get_label_y()
self.canvas.move(self.text_id, 0, new_y - prev_y)
def move(self, x: int, y: int): def move(self, x: int, y: int):
x, y = self.canvas.get_scaled_coords(x, y) x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id) current_x, current_y = self.canvas.coords(self.id)
@ -165,7 +180,7 @@ class CanvasNode:
self.canvas.move_selection(self.id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset)
# move antennae # move antennae
for antenna_id in self.antennae: for antenna_id in self.antennas:
self.canvas.move(antenna_id, x_offset, y_offset) self.canvas.move(antenna_id, x_offset, y_offset)
# move edges # move edges
@ -355,3 +370,17 @@ class CanvasNode:
self.canvas.delete(wireless_edge.id) self.canvas.delete(wireless_edge.id)
else: else:
logging.debug("%s is not a wireless edge", token) logging.debug("%s is not a wireless edge", token)
def scale_antennas(self):
for i in range(len(self.antennas)):
antenna_id = self.antennas[i]
image = Images.get(
ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)
)
self.canvas.itemconfig(antenna_id, image=image)
self.antenna_images[antenna_id] = image
node_x, node_y = self.canvas.coords(self.id)
x, y = self.canvas.coords(antenna_id)
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
dy = node_y - int(23 * self.app.app_scale) - y
self.canvas.move(antenna_id, dx, dy)

View file

@ -3,6 +3,7 @@ from tkinter import messagebox
from PIL import Image, ImageTk from PIL import Image, ImageTk
from core.api.grpc import core_pb2
from core.gui.appconfig import LOCAL_ICONS_PATH from core.gui.appconfig import LOCAL_ICONS_PATH
@ -90,3 +91,23 @@ class ImageEnum(Enum):
SHUTDOWN = "shutdown" SHUTDOWN = "shutdown"
CANCEL = "cancel" CANCEL = "cancel"
ERROR = "error" ERROR = "error"
class TypeToImage:
type_to_image = {
(core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC,
(core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST,
(core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
(core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
(core_pb2.NodeType.HUB, ""): ImageEnum.HUB,
(core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH,
(core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
}
@classmethod
def get(cls, node_type, model):
return cls.type_to_image.get((node_type, model), None)

View file

@ -2,7 +2,7 @@ import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from core.api.grpc.core_pb2 import NodeType from core.api.grpc.core_pb2 import NodeType
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images, TypeToImage
if TYPE_CHECKING: if TYPE_CHECKING:
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
@ -100,32 +100,35 @@ class NodeUtils:
node_type: NodeType, node_type: NodeType,
model: str, model: str,
gui_config: Dict[str, List[Dict[str, str]]], gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
if model == "":
model = None image_enum = TypeToImage.get(node_type, model)
try: if image_enum:
image = cls.NODE_ICONS[(node_type, model)] return Images.get(image_enum, int(ICON_SIZE * scale))
return image else:
except KeyError:
image_stem = cls.get_image_file(gui_config, model) image_stem = cls.get_image_file(gui_config, model)
if image_stem: if image_stem:
return Images.get_with_image_file(image_stem, ICON_SIZE) return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
@classmethod @classmethod
def node_image( def node_image(
cls, core_node: "core_pb2.Node", gui_config: Dict[str, List[Dict[str, str]]] cls,
core_node: "core_pb2.Node",
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
image = cls.node_icon(core_node.type, core_node.model, gui_config) image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
if core_node.icon: if core_node.icon:
try: try:
image = Images.create(core_node.icon, ICON_SIZE) image = Images.create(core_node.icon, int(ICON_SIZE * scale))
except OSError: except OSError:
logging.error("invalid icon: %s", core_node.icon) logging.error("invalid icon: %s", core_node.icon)
return image return image
@classmethod @classmethod
def is_custom(cls, model: str) -> bool: def is_custom(cls, node_type: NodeType, model: str) -> bool:
return model not in cls.NODE_MODELS return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
@classmethod @classmethod
def get_custom_node_services( def get_custom_node_services(

View file

@ -29,7 +29,7 @@ class StatusBar(ttk.Frame):
def draw(self): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=7) self.columnconfigure(1, weight=5)
self.columnconfigure(2, weight=1) self.columnconfigure(2, weight=1)
self.columnconfigure(3, weight=1) self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1) self.columnconfigure(4, weight=1)

View file

@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import font, ttk
THEME_DARK = "black" THEME_DARK = "black"
PADX = (0, 5) PADX = (0, 5)
@ -176,25 +176,35 @@ def style_listbox(widget: tk.Widget):
def theme_change(event: tk.Event): def theme_change(event: tk.Event):
style = ttk.Style() style = ttk.Style()
style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) style.configure(Styles.picker_button, font="TkSmallCaptionFont")
style.configure( style.configure(
Styles.green_alert, Styles.green_alert,
background="green", background="green",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
style.configure( style.configure(
Styles.yellow_alert, Styles.yellow_alert,
background="yellow", background="yellow",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
style.configure( style.configure(
Styles.red_alert, Styles.red_alert,
background="red", background="red",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
def scale_fonts(fonts_size, scale):
for name in font.names():
f = font.nametofont(name)
if name in fonts_size:
if name == "TkSmallCaptionFont":
f.config(size=int(fonts_size[name] * scale * 8 / 9))
else:
f.config(size=int(fonts_size[name] * scale))

View file

@ -1,9 +1,9 @@
import logging import logging
import time import time
import tkinter as tk import tkinter as tk
from enum import Enum
from functools import partial from functools import partial
from tkinter import ttk from tkinter import ttk
from tkinter.font import Font
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
@ -25,6 +25,12 @@ TOOLBAR_SIZE = 32
PICKER_SIZE = 24 PICKER_SIZE = 24
class NodeTypeEnum(Enum):
NODE = 0
NETWORK = 1
OTHER = 2
def icon(image_enum, width=TOOLBAR_SIZE): def icon(image_enum, width=TOOLBAR_SIZE):
return Images.get(image_enum, width) return Images.get(image_enum, width)
@ -43,10 +49,8 @@ class Toolbar(ttk.Frame):
self.master = app.master self.master = app.master
self.time = None self.time = None
# picker data
self.picker_font = Font(size=8)
# design buttons # design buttons
self.play_button = None
self.select_button = None self.select_button = None
self.link_button = None self.link_button = None
self.node_button = None self.node_button = None
@ -71,9 +75,18 @@ class Toolbar(ttk.Frame):
# dialog # dialog
self.marker_tool = None self.marker_tool = None
# these variables help keep track of what images being drawn so that scaling is possible
# since ImageTk.PhotoImage does not have resize method
self.node_enum = None
self.network_enum = None
self.annotation_enum = None
# draw components # draw components
self.draw() self.draw()
def get_icon(self, image_enum, width=TOOLBAR_SIZE):
return Images.get(image_enum, int(width * self.app.app_scale))
def draw(self): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
@ -85,20 +98,23 @@ class Toolbar(ttk.Frame):
self.design_frame = ttk.Frame(self) self.design_frame = ttk.Frame(self)
self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.grid(row=0, column=0, sticky="nsew")
self.design_frame.columnconfigure(0, weight=1) self.design_frame.columnconfigure(0, weight=1)
self.create_button( self.play_button = self.create_button(
self.design_frame, self.design_frame,
icon(ImageEnum.START), self.get_icon(ImageEnum.START),
self.click_start, self.click_start,
"start the session", "start the session",
) )
self.select_button = self.create_button( self.select_button = self.create_button(
self.design_frame, self.design_frame,
icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT),
self.click_selection, self.click_selection,
"selection tool", "selection tool",
) )
self.link_button = self.create_button( self.link_button = self.create_button(
self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" self.design_frame,
self.get_icon(ImageEnum.LINK),
self.click_link,
"link tool",
) )
self.create_node_button() self.create_node_button()
self.create_network_button() self.create_network_button()
@ -130,18 +146,21 @@ class Toolbar(ttk.Frame):
self.stop_button = self.create_button( self.stop_button = self.create_button(
self.runtime_frame, self.runtime_frame,
icon(ImageEnum.STOP), self.get_icon(ImageEnum.STOP),
self.click_stop, self.click_stop,
"stop the session", "stop the session",
) )
self.runtime_select_button = self.create_button( self.runtime_select_button = self.create_button(
self.runtime_frame, self.runtime_frame,
icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT),
self.click_runtime_selection, self.click_runtime_selection,
"selection tool", "selection tool",
) )
self.plot_button = self.create_button( self.plot_button = self.create_button(
self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" self.runtime_frame,
self.get_icon(ImageEnum.PLOT),
self.click_plot_button,
"plot",
) )
self.runtime_marker_button = self.create_button( self.runtime_marker_button = self.create_button(
self.runtime_frame, self.runtime_frame,
@ -164,23 +183,38 @@ class Toolbar(ttk.Frame):
self.node_picker = ttk.Frame(self.master) self.node_picker = ttk.Frame(self.master)
# draw default nodes # draw default nodes
for node_draw in NodeUtils.NODES: for node_draw in NodeUtils.NODES:
toolbar_image = icon(node_draw.image_enum) toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
image = icon(node_draw.image_enum, PICKER_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
func = partial( func = partial(
self.update_button, self.node_button, toolbar_image, node_draw self.update_button,
self.node_button,
toolbar_image,
node_draw,
NodeTypeEnum.NODE,
node_draw.image_enum,
) )
self.create_picker_button(image, func, self.node_picker, node_draw.label) self.create_picker_button(image, func, self.node_picker, node_draw.label)
# draw custom nodes # draw custom nodes
for name in sorted(self.app.core.custom_nodes): for name in sorted(self.app.core.custom_nodes):
node_draw = self.app.core.custom_nodes[name] node_draw = self.app.core.custom_nodes[name]
toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) toolbar_image = Images.get_custom(
image = Images.get_custom(node_draw.image_file, PICKER_SIZE) node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale)
)
image = Images.get_custom(
node_draw.image_file, int(PICKER_SIZE * self.app.app_scale)
)
func = partial( func = partial(
self.update_button, self.node_button, toolbar_image, node_draw self.update_button,
self.node_button,
toolbar_image,
node_draw,
NodeTypeEnum,
node_draw.image_file,
) )
self.create_picker_button(image, func, self.node_picker, name) self.create_picker_button(image, func, self.node_picker, name)
# draw edit node # draw edit node
image = icon(ImageEnum.EDITNODE, PICKER_SIZE) # image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, self.click_edit_node, self.node_picker, "Custom" image, self.click_edit_node, self.node_picker, "Custom"
) )
@ -281,13 +315,24 @@ class Toolbar(ttk.Frame):
dialog = CustomNodesDialog(self.app, self.app) dialog = CustomNodesDialog(self.app, self.app)
dialog.show() dialog.show()
def update_button(self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw): def update_button(
self,
button: ttk.Button,
image: "ImageTk",
node_draw: NodeDraw,
type_enum,
image_enum,
):
logging.debug("update button(%s): %s", button, node_draw) logging.debug("update button(%s): %s", button, node_draw)
self.hide_pickers() self.hide_pickers()
button.configure(image=image) button.configure(image=image)
button.image = image button.image = image
self.app.canvas.mode = GraphMode.NODE self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = node_draw self.app.canvas.node_draw = node_draw
if type_enum == NodeTypeEnum.NODE:
self.node_enum = image_enum
elif type_enum == NodeTypeEnum.NETWORK:
self.network_enum = image_enum
def hide_pickers(self): def hide_pickers(self):
logging.debug("hiding pickers") logging.debug("hiding pickers")
@ -305,13 +350,14 @@ class Toolbar(ttk.Frame):
""" """
Create network layer button Create network layer button
""" """
image = icon(ImageEnum.ROUTER) image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE)
self.node_button = ttk.Button( self.node_button = ttk.Button(
self.design_frame, image=image, command=self.draw_node_picker self.design_frame, image=image, command=self.draw_node_picker
) )
self.node_button.image = image self.node_button.image = image
self.node_button.grid(sticky="ew") self.node_button.grid(sticky="ew")
Tooltip(self.node_button, "Network-layer virtual nodes") Tooltip(self.node_button, "Network-layer virtual nodes")
self.node_enum = ImageEnum.ROUTER
def draw_network_picker(self): def draw_network_picker(self):
""" """
@ -320,12 +366,17 @@ class Toolbar(ttk.Frame):
self.hide_pickers() self.hide_pickers()
self.network_picker = ttk.Frame(self.master) self.network_picker = ttk.Frame(self.master)
for node_draw in NodeUtils.NETWORK_NODES: for node_draw in NodeUtils.NETWORK_NODES:
toolbar_image = icon(node_draw.image_enum) toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
image = icon(node_draw.image_enum, PICKER_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, image,
partial( partial(
self.update_button, self.network_button, toolbar_image, node_draw self.update_button,
self.network_button,
toolbar_image,
node_draw,
NodeTypeEnum.NETWORK,
node_draw.image_enum,
), ),
self.network_picker, self.network_picker,
node_draw.label, node_draw.label,
@ -340,13 +391,14 @@ class Toolbar(ttk.Frame):
Create link-layer node button and the options that represent different Create link-layer node button and the options that represent different
link-layer node types. link-layer node types.
""" """
image = icon(ImageEnum.HUB) image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE)
self.network_button = ttk.Button( self.network_button = ttk.Button(
self.design_frame, image=image, command=self.draw_network_picker self.design_frame, image=image, command=self.draw_network_picker
) )
self.network_button.image = image self.network_button.image = image
self.network_button.grid(sticky="ew") self.network_button.grid(sticky="ew")
Tooltip(self.network_button, "link-layer nodes") Tooltip(self.network_button, "link-layer nodes")
self.network_enum = ImageEnum.HUB
def draw_annotation_picker(self): def draw_annotation_picker(self):
""" """
@ -361,11 +413,11 @@ class Toolbar(ttk.Frame):
(ImageEnum.TEXT, ShapeType.TEXT), (ImageEnum.TEXT, ShapeType.TEXT),
] ]
for image_enum, shape_type in nodes: for image_enum, shape_type in nodes:
toolbar_image = icon(image_enum) toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE)
image = icon(image_enum, PICKER_SIZE) image = self.get_icon(image_enum, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, image,
partial(self.update_annotation, toolbar_image, shape_type), partial(self.update_annotation, toolbar_image, shape_type, image_enum),
self.annotation_picker, self.annotation_picker,
shape_type.value, shape_type.value,
) )
@ -378,13 +430,14 @@ class Toolbar(ttk.Frame):
""" """
Create marker button and options that represent different marker types Create marker button and options that represent different marker types
""" """
image = icon(ImageEnum.MARKER) image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE)
self.annotation_button = ttk.Button( self.annotation_button = ttk.Button(
self.design_frame, image=image, command=self.draw_annotation_picker self.design_frame, image=image, command=self.draw_annotation_picker
) )
self.annotation_button.image = image self.annotation_button.image = image
self.annotation_button.grid(sticky="ew") self.annotation_button.grid(sticky="ew")
Tooltip(self.annotation_button, "background annotation tools") Tooltip(self.annotation_button, "background annotation tools")
self.annotation_enum = ImageEnum.MARKER
def create_observe_button(self): def create_observe_button(self):
menu_button = ttk.Menubutton( menu_button = ttk.Menubutton(
@ -429,13 +482,16 @@ class Toolbar(ttk.Frame):
self.app.statusbar.set_status(message) self.app.statusbar.set_status(message)
self.app.canvas.stopped_session() self.app.canvas.stopped_session()
def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType): def update_annotation(
self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum
):
logging.debug("clicked annotation: ") logging.debug("clicked annotation: ")
self.hide_pickers() self.hide_pickers()
self.annotation_button.configure(image=image) self.annotation_button.configure(image=image)
self.annotation_button.image = image self.annotation_button.image = image
self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = shape_type self.app.canvas.annotation_type = shape_type
self.annotation_enum = image_enum
if is_marker(shape_type): if is_marker(shape_type):
if self.marker_tool: if self.marker_tool:
self.marker_tool.destroy() self.marker_tool.destroy()
@ -460,3 +516,24 @@ class Toolbar(ttk.Frame):
def click_two_node_button(self): def click_two_node_button(self):
logging.debug("Click TWONODE button") logging.debug("Click TWONODE button")
# def scale_button(cls, button, image_enum, scale):
def scale_button(self, button, image_enum):
image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale))
button.config(image=image)
button.image = image
def scale(self):
self.scale_button(self.play_button, ImageEnum.START)
self.scale_button(self.select_button, ImageEnum.SELECT)
self.scale_button(self.link_button, ImageEnum.LINK)
self.scale_button(self.node_button, self.node_enum)
self.scale_button(self.network_button, self.network_enum)
self.scale_button(self.annotation_button, self.annotation_enum)
self.scale_button(self.runtime_select_button, ImageEnum.SELECT)
self.scale_button(self.stop_button, ImageEnum.STOP)
self.scale_button(self.plot_button, ImageEnum.PLOT)
self.scale_button(self.runtime_marker_button, ImageEnum.MARKER)
self.scale_button(self.node_command_button, ImageEnum.TWONODE)
self.scale_button(self.run_command_button, ImageEnum.RUN)

View file

@ -570,10 +570,10 @@ class WayPoint:
return not self == other return not self == other
def __lt__(self, other: "WayPoint") -> bool: def __lt__(self, other: "WayPoint") -> bool:
result = self.time < other.time if self.time == other.time:
if result: return self.nodenum < other.nodenum
result = self.nodenum < other.nodenum else:
return result return self.time < other.time
class WayPointMobility(WirelessModel): class WayPointMobility(WirelessModel):

View file

@ -0,0 +1,17 @@
import pytest
from core.location.mobility import WayPoint
class TestMobility:
@pytest.mark.parametrize(
"wp1, wp2, expected",
[
(WayPoint(10.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), False),
(WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(10.0, 2, [0, 0], 1.0), True),
(WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), True),
(WayPoint(1.0, 2, [0, 0], 1.0), WayPoint(1.0, 1, [0, 0], 1.0), False),
],
)
def test_waypoint_lessthan(self, wp1, wp2, expected):
assert (wp1 < wp2) == expected