moved coretk under daemon/core/gui

This commit is contained in:
Blake Harnden 2019-12-19 09:30:21 -08:00
parent f13c62a1c9
commit 0b5c94778c
118 changed files with 505 additions and 432 deletions

View file

107
daemon/core/gui/app.py Normal file
View file

@ -0,0 +1,107 @@
import logging
import tkinter as tk
from tkinter import ttk
from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.menuaction import MenuAction
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.toolbar import Toolbar
from core.gui.validation import InputValidation
WIDTH = 1000
HEIGHT = 800
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# load node icons
NodeUtils.setup()
# widgets
self.menubar = None
self.toolbar = None
self.canvas = None
self.statusbar = None
self.validation = None
# setup
self.guiconfig = appconfig.read()
self.style = ttk.Style()
self.setup_theme()
self.core = CoreClient(self)
self.setup_app()
self.draw()
self.core.set_up()
def setup_theme(self):
themes.load(self.style)
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
self.master.bind("<<ThemeChanged>>", themes.theme_change)
self.style.theme_use(self.guiconfig["preferences"]["theme"])
def setup_app(self):
self.master.title("CORE")
self.center()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
image = Images.get(ImageEnum.CORE, 16)
self.master.tk.call("wm", "iconphoto", self.master._w, image)
self.pack(fill=tk.BOTH, expand=True)
self.validation = InputValidation(self)
def center(self):
screen_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight()
x = int((screen_width / 2) - (WIDTH / 2))
y = int((screen_height / 2) - (HEIGHT / 2))
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
def draw(self):
self.master.option_add("*tearOff", tk.FALSE)
self.menubar = Menubar(self.master, self)
self.toolbar = Toolbar(self, self)
self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2)
self.draw_canvas()
self.draw_status()
def draw_canvas(self):
width = self.guiconfig["preferences"]["width"]
height = self.guiconfig["preferences"]["height"]
self.canvas = CanvasGraph(self, self.core, width, height)
self.canvas.pack(fill=tk.BOTH, expand=True)
scroll_x = ttk.Scrollbar(
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview
)
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
scroll_y = ttk.Scrollbar(self.canvas, command=self.canvas.yview)
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set)
def draw_status(self):
self.statusbar = StatusBar(master=self, app=self)
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
def on_closing(self):
menu_action = MenuAction(self, self.master)
menu_action.on_quit()
def save_config(self):
appconfig.save(self.guiconfig)
def close(self):
self.master.destroy()
if __name__ == "__main__":
log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
logging.basicConfig(level=logging.DEBUG, format=log_format)
Images.load_all()
appconfig.check_directory()
app = Application()
app.mainloop()

View file

@ -0,0 +1,111 @@
import logging
import os
import shutil
from pathlib import Path
import yaml
# gui home paths
from core.gui import themes
HOME_PATH = Path.home().joinpath(".coretk")
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
ICONS_PATH = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls")
CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
# local paths
DATA_PATH = Path(__file__).parent.joinpath("data")
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute()
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute()
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
# configuration data
TERMINALS = [
"$TERM",
"gnome-terminal --window --",
"lxterminal -e",
"konsole -e",
"xterm -e",
"aterm -e",
"eterm -e",
"rxvt -e",
"xfce4-terminal -x",
]
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
class IndentDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
return super().increase_indent(flow, False)
def copy_files(current_path, new_path):
for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file)
def check_directory():
if HOME_PATH.exists():
logging.info("~/.coretk exists")
return
logging.info("creating ~/.coretk")
HOME_PATH.mkdir()
BACKGROUNDS_PATH.mkdir()
CUSTOM_EMANE_PATH.mkdir()
CUSTOM_SERVICE_PATH.mkdir()
ICONS_PATH.mkdir()
MOBILITY_PATH.mkdir()
XMLS_PATH.mkdir()
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
if "TERM" in os.environ:
terminal = TERMINALS[0]
else:
terminal = TERMINALS[1]
if "EDITOR" in os.environ:
editor = EDITORS[0]
else:
editor = EDITORS[1]
config = {
"preferences": {
"theme": themes.THEME_DARK,
"editor": editor,
"terminal": terminal,
"gui3d": "/usr/local/bin/std3d.sh",
"width": 1000,
"height": 750,
},
"location": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"lat": 47.5791667,
"lon": -122.132322,
"alt": 2.0,
"scale": 150.0,
},
"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
"nodes": [],
"observers": [{"name": "hello", "cmd": "echo hello"}],
}
save(config)
def read():
with CONFIG_PATH.open("r") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
def save(config):
with CONFIG_PATH.open("w") as f:
yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)

View file

@ -0,0 +1,909 @@
"""
Incorporate grpc into python tkinter GUI
"""
import json
import logging
import os
import time
from pathlib import Path
import grpc
from core.api.grpc import client, core_pb2
from core.gui import appconfig
from core.gui.dialogs.mobilityplayer import MobilityPlayer
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.errors import show_grpc_error
from core.gui.graph import tags
from core.gui.graph.shape import AnnotationData, Shape
from core.gui.graph.shapeutils import ShapeType
from core.gui.interface import InterfaceManager
from core.gui.nodeutils import NodeDraw, NodeUtils
GUI_SOURCE = "gui"
OBSERVERS = {
"processes": "ps",
"ifconfig": "ifconfig",
"IPV4 Routes": "ip -4 ro",
"IPV6 Routes": "ip -6 ro",
"Listening sockets": "netstat -tuwnl",
"IPv4 MFC entries": "ip -4 mroute show",
"IPv6 MFC entries": "ip -6 mroute show",
"firewall rules": "iptables -L",
"IPSec policies": "setkey -DP",
}
class CoreServer:
def __init__(self, name, address, port):
self.name = name
self.address = address
self.port = port
class Observer:
def __init__(self, name, cmd):
self.name = name
self.cmd = cmd
class CoreClient:
def __init__(self, app):
"""
Create a CoreGrpc instance
"""
self.client = client.CoreGrpcClient()
self.session_id = None
self.node_ids = []
self.app = app
self.master = app.master
self.services = {}
self.default_services = {}
self.emane_models = []
self.observer = None
# loaded configuration data
self.servers = {}
self.custom_nodes = {}
self.custom_observers = {}
self.read_config()
# helpers
self.interface_to_edge = {}
self.interfaces_manager = InterfaceManager(self.app)
# session data
self.state = None
self.canvas_nodes = {}
self.location = None
self.links = {}
self.hooks = {}
self.wlan_configs = {}
self.mobility_configs = {}
self.emane_model_configs = {}
self.emane_config = None
self.service_configs = {}
self.file_configs = {}
self.mobility_players = {}
self.handling_throughputs = None
self.handling_events = None
def reset(self):
# helpers
self.interfaces_manager.reset()
self.interface_to_edge.clear()
# session data
self.canvas_nodes.clear()
self.links.clear()
self.hooks.clear()
self.wlan_configs.clear()
self.mobility_configs.clear()
self.emane_model_configs.clear()
self.emane_config = None
self.service_configs.clear()
self.file_configs.clear()
self.mobility_players.clear()
# clear streams
if self.handling_throughputs:
self.handling_throughputs.cancel()
self.handling_throughputs = None
if self.handling_events:
self.handling_events.cancel()
self.handling_events = None
def set_observer(self, value):
self.observer = value
def read_config(self):
# read distributed server
for config in self.app.guiconfig.get("servers", []):
server = CoreServer(config["name"], config["address"], config["port"])
self.servers[server.name] = server
# read custom nodes
for config in self.app.guiconfig.get("nodes", []):
name = config["name"]
image_file = config["image"]
services = set(config["services"])
node_draw = NodeDraw.from_custom(name, image_file, services)
self.custom_nodes[name] = node_draw
# read observers
for config in self.app.guiconfig.get("observers", []):
observer = Observer(config["name"], config["cmd"])
self.custom_observers[observer.name] = observer
def handle_events(self, event):
if event.session_id != self.session_id:
logging.warn(
"ignoring event session(%s) current(%s)",
event.session_id,
self.session_id,
)
return
if event.HasField("link_event"):
logging.info("link event: %s", event)
self.handle_link_event(event.link_event)
elif event.HasField("session_event"):
logging.info("session event: %s", event)
session_event = event.session_event
if session_event.event <= core_pb2.SessionState.SHUTDOWN:
self.state = event.session_event.event
elif session_event.event in {7, 8, 9}:
node_id = session_event.node_id
dialog = self.mobility_players.get(node_id)
if dialog:
if session_event.event == 7:
dialog.set_play()
elif session_event.event == 8:
dialog.set_stop()
else:
dialog.set_pause()
else:
logging.warning("unknown session event: %s", session_event)
elif event.HasField("node_event"):
self.handle_node_event(event.node_event)
elif event.HasField("config_event"):
logging.info("config event: %s", event)
elif event.HasField("exception_event"):
self.handle_exception_event(event.exception_event)
else:
logging.info("unhandled event: %s", event)
def handle_link_event(self, event):
node_one_id = event.link.node_one_id
node_two_id = event.link.node_two_id
canvas_node_one = self.canvas_nodes[node_one_id]
canvas_node_two = self.canvas_nodes[node_two_id]
if event.message_type == core_pb2.MessageType.ADD:
self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two)
elif event.message_type == core_pb2.MessageType.DELETE:
self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two)
else:
logging.warning("unknown link event: %s", event.message_type)
def handle_node_event(self, event):
if event.source == GUI_SOURCE:
return
node_id = event.node.id
x = event.node.position.x
y = event.node.position.y
canvas_node = self.canvas_nodes[node_id]
canvas_node.move(x, y)
def enable_throughputs(self):
self.handling_throughputs = self.client.throughputs(
self.session_id, self.handle_throughputs
)
def cancel_throughputs(self):
self.handling_throughputs.cancel()
self.handling_throughputs = None
def handle_throughputs(self, event):
if event.session_id != self.session_id:
logging.warn(
"ignoring throughput event session(%s) current(%s)",
event.session_id,
self.session_id,
)
return
logging.info("handling throughputs event: %s", event)
self.app.canvas.throughput_draw.process_grpc_throughput_event(
event.interface_throughputs
)
def handle_exception_event(self, event):
logging.info("exception event: %s", event)
self.app.statusbar.core_alarms.append(event)
def join_session(self, session_id, query_location=True):
# update session and title
self.session_id = session_id
self.master.title(f"CORE Session({self.session_id})")
# clear session data
self.reset()
# get session data
try:
response = self.client.get_session(self.session_id)
session = response.session
self.state = session.state
self.handling_events = self.client.events(
self.session_id, self.handle_events
)
# get location
if query_location:
response = self.client.get_session_location(self.session_id)
self.location = response.location
# get emane models
response = self.client.get_emane_models(self.session_id)
self.emane_models = response.models
# get hooks
response = self.client.get_hooks(self.session_id)
for hook in response.hooks:
self.hooks[hook.file] = hook
# get mobility configs
response = self.client.get_mobility_configs(self.session_id)
for node_id in response.configs:
node_config = response.configs[node_id].config
self.mobility_configs[node_id] = node_config
# get emane config
response = self.client.get_emane_config(self.session_id)
self.emane_config = response.config
# get emane model config
response = self.client.get_emane_model_configs(self.session_id)
for config in response.configs:
interface = None
if config.interface != -1:
interface = config.interface
self.set_emane_model_config(
config.node_id, config.model, config.config, interface
)
# get wlan configurations
response = self.client.get_wlan_configs(self.session_id)
for _id in response.configs:
mapped_config = response.configs[_id]
self.wlan_configs[_id] = mapped_config.config
# get service configurations
response = self.client.get_node_service_configs(self.session_id)
for config in response.configs:
service_configs = self.service_configs.setdefault(config.node_id, {})
service_configs[config.service] = config.data
logging.info("service file configs: %s", config.files)
for file_name in config.files:
file_configs = self.file_configs.setdefault(config.node_id, {})
files = file_configs.setdefault(config.service, {})
data = config.files[file_name]
files[file_name] = data
# draw session
self.app.canvas.reset_and_redraw(session)
# get metadata
response = self.client.get_session_metadata(self.session_id)
self.parse_metadata(response.config)
except grpc.RpcError as e:
show_grpc_error(e)
# update ui to represent current state
if self.is_runtime():
self.app.toolbar.runtime_frame.tkraise()
self.app.toolbar.click_runtime_selection()
else:
self.app.toolbar.design_frame.tkraise()
self.app.toolbar.click_selection()
self.app.statusbar.progress_bar.stop()
def is_runtime(self):
return self.state == core_pb2.SessionState.RUNTIME
def parse_metadata(self, config):
# canvas setting
canvas_config = config.get("canvas")
logging.info("canvas metadata: %s", canvas_config)
if canvas_config:
canvas_config = json.loads(canvas_config)
gridlines = canvas_config.get("gridlines", True)
self.app.canvas.show_grid.set(gridlines)
fit_image = canvas_config.get("fit_image", False)
self.app.canvas.adjust_to_dim.set(fit_image)
wallpaper_style = canvas_config.get("wallpaper-style", 1)
self.app.canvas.scale_option.set(wallpaper_style)
width = self.app.guiconfig["preferences"]["width"]
height = self.app.guiconfig["preferences"]["height"]
dimensions = canvas_config.get("dimensions", [width, height])
self.app.canvas.redraw_canvas(dimensions)
wallpaper = canvas_config.get("wallpaper")
if wallpaper:
wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper))
self.app.canvas.set_wallpaper(wallpaper)
else:
self.app.canvas.redraw_canvas()
self.app.canvas.set_wallpaper(None)
# load saved shapes
shapes_config = config.get("shapes")
if shapes_config:
shapes_config = json.loads(shapes_config)
for shape_config in shapes_config:
logging.info("loading shape: %s", shape_config)
shape_type = shape_config["type"]
try:
shape_type = ShapeType(shape_type)
coords = shape_config["iconcoords"]
data = AnnotationData(
shape_config["label"],
shape_config["fontfamily"],
shape_config["fontsize"],
shape_config["labelcolor"],
shape_config["color"],
shape_config["border"],
shape_config["width"],
shape_config["bold"],
shape_config["italic"],
shape_config["underline"],
)
shape = Shape(
self.app, self.app.canvas, shape_type, *coords, data=data
)
self.app.canvas.shapes[shape.id] = shape
except ValueError:
logging.exception("unknown shape: %s", shape_type)
for tag in tags.ABOVE_WALLPAPER_TAGS:
self.app.canvas.tag_raise(tag)
def create_new_session(self):
"""
Create a new session
:return: nothing
"""
try:
response = self.client.create_session()
logging.info("created session: %s", response)
location_config = self.app.guiconfig["location"]
self.location = core_pb2.SessionLocation(
x=location_config["x"],
y=location_config["y"],
z=location_config["z"],
lat=location_config["lat"],
lon=location_config["lon"],
alt=location_config["alt"],
scale=location_config["scale"],
)
self.join_session(response.session_id, query_location=False)
except grpc.RpcError as e:
show_grpc_error(e)
def delete_session(self, session_id=None):
if session_id is None:
session_id = self.session_id
try:
response = self.client.delete_session(session_id)
logging.info("deleted session result: %s", response)
except grpc.RpcError as e:
show_grpc_error(e)
def set_up(self):
"""
Query sessions, if there exist any, prompt whether to join one
:return: existing sessions
"""
try:
self.client.connect()
# get service information
response = self.client.get_services()
for service in response.services:
group_services = self.services.setdefault(service.group, set())
group_services.add(service.name)
# if there are no sessions, create a new session, else join a session
response = self.client.get_sessions()
logging.info("current sessions: %s", response)
sessions = response.sessions
if len(sessions) == 0:
self.create_new_session()
else:
dialog = SessionsDialog(self.app, self.app)
dialog.show()
response = self.client.get_service_defaults(self.session_id)
self.default_services = {
x.node_type: set(x.services) for x in response.defaults
}
except grpc.RpcError as e:
show_grpc_error(e)
self.app.close()
def edit_node(self, core_node):
try:
self.client.edit_node(
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
)
except grpc.RpcError as e:
show_grpc_error(e)
def start_session(self):
nodes = [x.core_node for x in self.canvas_nodes.values()]
links = [x.link for x in self.links.values()]
wlan_configs = self.get_wlan_configs_proto()
mobility_configs = self.get_mobility_configs_proto()
emane_model_configs = self.get_emane_model_configs_proto()
hooks = list(self.hooks.values())
service_configs = self.get_service_configs_proto()
file_configs = self.get_service_file_configs_proto()
asymmetric_links = [
x.asymmetric_link for x in self.links.values() if x.asymmetric_link
]
if self.emane_config:
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
else:
emane_config = None
start = time.perf_counter()
try:
response = self.client.start_session(
self.session_id,
nodes,
links,
self.location,
hooks,
emane_config,
emane_model_configs,
wlan_configs,
mobility_configs,
service_configs,
file_configs,
asymmetric_links,
)
self.set_metadata()
process_time = time.perf_counter() - start
logging.debug(
"start session(%s), result: %s", self.session_id, response.result
)
self.app.statusbar.start_session_callback(process_time)
# display mobility players
for node_id, config in self.mobility_configs.items():
canvas_node = self.canvas_nodes[node_id]
mobility_player = MobilityPlayer(
self.app, self.app, canvas_node, config
)
mobility_player.show()
self.mobility_players[node_id] = mobility_player
except grpc.RpcError as e:
show_grpc_error(e)
def stop_session(self, session_id=None):
if not session_id:
session_id = self.session_id
start = time.perf_counter()
try:
response = self.client.stop_session(session_id)
logging.debug(
"stopped session(%s), result: %s", session_id, response.result
)
process_time = time.perf_counter() - start
self.app.statusbar.stop_session_callback(process_time)
except grpc.RpcError as e:
show_grpc_error(e)
def set_metadata(self):
# create canvas data
wallpaper = None
if self.app.canvas.wallpaper_file:
wallpaper = Path(self.app.canvas.wallpaper_file).name
canvas_config = {
"wallpaper": wallpaper,
"wallpaper-style": self.app.canvas.scale_option.get(),
"gridlines": self.app.canvas.show_grid.get(),
"fit_image": self.app.canvas.adjust_to_dim.get(),
"dimensions": self.app.canvas.current_dimensions,
}
canvas_config = json.dumps(canvas_config)
# create shapes data
shapes = []
for shape in self.app.canvas.shapes.values():
shapes.append(shape.metadata())
shapes = json.dumps(shapes)
metadata = {"canvas": canvas_config, "shapes": shapes}
response = self.client.set_session_metadata(self.session_id, metadata)
logging.info("set session metadata: %s", response)
def launch_terminal(self, node_id):
try:
response = self.client.get_node_terminal(self.session_id, node_id)
logging.info("get terminal %s", response.terminal)
os.system(f"xterm -e {response.terminal} &")
except grpc.RpcError as e:
show_grpc_error(e)
def save_xml(self, file_path):
"""
Save core session as to an xml file
:param str file_path: file path that user pick
:return: nothing
"""
try:
if self.state != core_pb2.SessionState.RUNTIME:
logging.debug(
"session state not runtime, send session data to the daemon..."
)
self.send_data()
response = self.client.save_xml(self.session_id, file_path)
logging.info("saved xml(%s): %s", file_path, response)
except grpc.RpcError as e:
show_grpc_error(e)
def open_xml(self, file_path):
"""
Open core xml
:param str file_path: file to open
:return: session id
"""
try:
response = self.client.open_xml(file_path)
logging.debug("open xml: %s", response)
self.join_session(response.session_id)
except grpc.RpcError as e:
show_grpc_error(e)
def get_node_service(self, node_id, service_name):
response = self.client.get_node_service(self.session_id, node_id, service_name)
logging.debug("get node service %s", response)
return response.service
def set_node_service(self, node_id, service_name, startups, validations, shutdowns):
response = self.client.set_node_service(
self.session_id, node_id, service_name, startups, validations, shutdowns
)
logging.debug("set node service %s", response)
response = self.client.get_node_service(self.session_id, node_id, service_name)
logging.debug("get node service : %s", response)
return response.service
def get_node_service_file(self, node_id, service_name, file_name):
response = self.client.get_node_service_file(
self.session_id, node_id, service_name, file_name
)
logging.debug("get service file %s", response)
return response.data
def set_node_service_file(self, node_id, service_name, file_name, data):
response = self.client.set_node_service_file(
self.session_id, node_id, service_name, file_name, data
)
logging.debug("set node service file %s", response)
def create_nodes_and_links(self):
"""
create nodes and links that have not been created yet
:return: nothing
"""
node_protos = [x.core_node for x in self.canvas_nodes.values()]
link_protos = [x.link for x in self.links.values()]
if self.state != core_pb2.SessionState.DEFINITION:
self.client.set_session_state(
self.session_id, core_pb2.SessionState.DEFINITION
)
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION)
for node_proto in node_protos:
response = self.client.add_node(self.session_id, node_proto)
logging.debug("create node: %s", response)
for link_proto in link_protos:
response = self.client.add_link(
self.session_id,
link_proto.node_one_id,
link_proto.node_two_id,
link_proto.interface_one,
link_proto.interface_two,
link_proto.options,
)
logging.debug("create link: %s", response)
def send_data(self):
"""
send to daemon all session info, but don't start the session
:return: nothing
"""
self.create_nodes_and_links()
for config_proto in self.get_wlan_configs_proto():
self.client.set_wlan_config(
self.session_id, config_proto.node_id, config_proto.config
)
for config_proto in self.get_mobility_configs_proto():
self.client.set_mobility_config(
self.session_id, config_proto.node_id, config_proto.config
)
for config_proto in self.get_service_configs_proto():
self.client.set_node_service(
self.session_id,
config_proto.node_id,
config_proto.service,
config_proto.startup,
config_proto.validate,
config_proto.shutdown,
)
for config_proto in self.get_service_file_configs_proto():
self.client.set_node_service_file(
self.session_id,
config_proto.node_id,
config_proto.service,
config_proto.file,
config_proto.data,
)
for hook in self.hooks.values():
self.client.add_hook(self.session_id, hook.state, hook.file, hook.data)
for config_proto in self.get_emane_model_configs_proto():
self.client.set_emane_model_config(
self.session_id,
config_proto.node_id,
config_proto.model,
config_proto.config,
config_proto.interface_id,
)
if self.emane_config:
config = {x: self.emane_config[x].value for x in self.emane_config}
self.client.set_emane_config(self.session_id, config)
def close(self):
"""
Clean ups when done using grpc
:return: nothing
"""
logging.debug("close grpc")
self.client.close()
def next_node_id(self):
"""
Get the next usable node id.
:return: the next id to be used
:rtype: int
"""
i = 1
while True:
if i not in self.canvas_nodes:
break
i += 1
return i
def create_node(self, x, y, node_type, model):
"""
Add node, with information filled in, to grpc manager
:param int x: x coord
:param int y: y coord
:param core_pb2.NodeType node_type: node type
:param str model: node model
:return: nothing
"""
node_id = self.next_node_id()
position = core_pb2.Position(x=x, y=y)
image = None
if NodeUtils.is_image_node(node_type):
image = "ubuntu:latest"
emane = None
if node_type == core_pb2.NodeType.EMANE:
emane = self.emane_models[0]
node = core_pb2.Node(
id=node_id,
type=node_type,
name=f"n{node_id}",
model=model,
position=position,
image=image,
emane=emane,
)
logging.debug(
"adding node to core session: %s, coords: (%s, %s), name: %s",
self.session_id,
x,
y,
node.name,
)
return node
def delete_graph_nodes(self, canvas_nodes):
"""
remove the nodes selected by the user and anything related to that node
such as link, configurations, interfaces
:param list canvas_nodes: list of nodes to delete
:return: nothing
"""
edges = set()
for canvas_node in canvas_nodes:
node_id = canvas_node.core_node.id
if node_id not in self.canvas_nodes:
logging.error("unknown node: %s", node_id)
continue
del self.canvas_nodes[node_id]
if node_id in self.mobility_configs:
del self.mobility_configs[node_id]
if node_id in self.wlan_configs:
del self.wlan_configs[node_id]
for key in list(self.emane_model_configs):
node_id, _, _ = key
if node_id == node_id:
del self.emane_model_configs[key]
for edge in canvas_node.edges:
if edge in edges:
continue
edges.add(edge)
if edge.token not in self.links:
logging.error("unknown edge: %s", edge.token)
del self.links[edge.token]
def create_interface(self, canvas_node):
node = canvas_node.core_node
ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id)
interface_id = len(canvas_node.interfaces)
name = f"eth{interface_id}"
interface = core_pb2.Interface(
id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix
)
canvas_node.interfaces.append(interface)
logging.debug(
"create node(%s) interface IPv4: %s, name: %s",
node.name,
interface.ip4,
interface.name,
)
return interface
def create_link(self, edge, canvas_src_node, canvas_dst_node):
"""
Create core link for a pair of canvas nodes, with token referencing
the canvas edge.
:param edge: edge for link
:param canvas_src_node: canvas node one
:param canvas_dst_node: canvas node two
:return: nothing
"""
src_node = canvas_src_node.core_node
dst_node = canvas_dst_node.core_node
# determine subnet
self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node)
src_interface = None
if NodeUtils.is_container_node(src_node.type):
src_interface = self.create_interface(canvas_src_node)
edge.src_interface = src_interface
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token
dst_interface = None
if NodeUtils.is_container_node(dst_node.type):
dst_interface = self.create_interface(canvas_dst_node)
edge.dst_interface = dst_interface
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token
link = core_pb2.Link(
type=core_pb2.LinkType.WIRED,
node_one_id=src_node.id,
node_two_id=dst_node.id,
interface_one=src_interface,
interface_two=dst_interface,
)
edge.set_link(link)
self.links[edge.token] = edge
def get_wlan_configs_proto(self):
configs = []
for node_id, config in self.wlan_configs.items():
config = {x: config[x].value for x in config}
wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config)
configs.append(wlan_config)
return configs
def get_mobility_configs_proto(self):
configs = []
for node_id, config in self.mobility_configs.items():
config = {x: config[x].value for x in config}
mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config)
configs.append(mobility_config)
return configs
def get_emane_model_configs_proto(self):
configs = []
for key, config in self.emane_model_configs.items():
node_id, model, interface = key
config = {x: config[x].value for x in config}
if interface is None:
interface = -1
config_proto = core_pb2.EmaneModelConfig(
node_id=node_id, interface_id=interface, model=model, config=config
)
configs.append(config_proto)
return configs
def get_service_configs_proto(self):
configs = []
for node_id, services in self.service_configs.items():
for name, config in services.items():
config_proto = core_pb2.ServiceConfig(
node_id=node_id,
service=name,
startup=config.startup,
validate=config.validate,
shutdown=config.shutdown,
)
configs.append(config_proto)
return configs
def get_service_file_configs_proto(self):
configs = []
for (node_id, file_configs) in self.file_configs.items():
for service, file_config in file_configs.items():
for file, data in file_config.items():
config_proto = core_pb2.ServiceFileConfig(
node_id=node_id, service=service, file=file, data=data
)
configs.append(config_proto)
return configs
def run(self, node_id):
logging.info("running node(%s) cmd: %s", node_id, self.observer)
return self.client.node_command(self.session_id, node_id, self.observer).output
def get_wlan_config(self, node_id):
config = self.wlan_configs.get(node_id)
if not config:
response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config
return config
def get_mobility_config(self, node_id):
config = self.mobility_configs.get(node_id)
if not config:
response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config
return config
def get_emane_model_config(self, node_id, model, interface=None):
logging.info("getting emane model config: %s %s %s", node_id, model, interface)
config = self.emane_model_configs.get((node_id, model, interface))
if not config:
if interface is None:
interface = -1
response = self.client.get_emane_model_config(
self.session_id, node_id, model, interface
)
config = response.config
return config
def set_emane_model_config(self, node_id, model, config, interface=None):
logging.info("setting emane model config: %s %s %s", node_id, model, interface)
self.emane_model_configs[(node_id, model, interface)] = config

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,28 @@
#
# nodes: 4, max time: 27.000000, max x: 600.00, max y: 600.00
# nominal range: 300.00 link bw: 54000000.00
# pause: 30.00, min speed 1.50 max speed: 4.50
$node_(6) set X_ 780.0
$node_(6) set Y_ 228.0
$node_(6) set Z_ 0.00
$node_(7) set X_ 816.0
$node_(7) set Y_ 348.0
$node_(7) set Z_ 0.00
$node_(8) set X_ 672.0
$node_(8) set Y_ 420.0
$node_(8) set Z_ 0.00
$node_(9) set X_ 672.0
$node_(9) set Y_ 96.0
$node_(9) set Z_ 0.00
$ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0"
$ns_ at 2.00 "$node_(7) setdest 400.0 288.0 15.0"
$ns_ at 1.00 "$node_(8) setdest 590.0 520.0 17.0"
$ns_ at 3.00 "$node_(9) setdest 720.0 300.0 20.0"
$ns_ at 8.00 "$node_(7) setdest 600.0 350.0 10.0"
$ns_ at 9.00 "$node_(8) setdest 730.0 300.0 15.0"
$ns_ at 10.00 "$node_(6) setdest 600.0 108.0 10.0"
$ns_ at 16.00 "$node_(9) setdest 672.0 96.0 20.0"
$ns_ at 17.00 "$node_(7) setdest 816.0 348.0 20.0"
$ns_ at 18.00 "$node_(6) setdest 780.0 228.0 25.0"
$ns_ at 22.00 "$node_(8) setdest 672.0 420.0 20.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

File diff suppressed because it is too large Load diff

View file

View file

@ -0,0 +1,44 @@
import tkinter as tk
from core.gui.dialogs.dialog import Dialog
from core.gui.widgets import CodeText
LICENSE = """\
Copyright (c) 2005-2020, the Boeing Company.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.\
"""
class AboutDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "About CORE", modal=True)
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
codetext = CodeText(self.top)
codetext.text.insert("1.0", LICENSE)
codetext.text.config(state=tk.DISABLED)
codetext.grid(sticky="nsew")

View file

@ -0,0 +1,171 @@
"""
check engine light
"""
import tkinter as tk
from tkinter import ttk
from grpc import RpcError
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText
class AlertsDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Alerts", modal=True)
self.app = app
self.tree = None
self.codetext = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
self.tree = ttk.Treeview(
frame,
columns=("time", "level", "session_id", "node", "source"),
show="headings",
)
self.tree.grid(row=0, column=0, sticky="nsew")
self.tree.column("time", stretch=tk.YES)
self.tree.heading("time", text="Time")
self.tree.column("level", stretch=tk.YES)
self.tree.heading("level", text="Level")
self.tree.column("session_id", stretch=tk.YES)
self.tree.heading("session_id", text="Session ID")
self.tree.column("node", stretch=tk.YES)
self.tree.heading("node", text="Node")
self.tree.column("source", stretch=tk.YES)
self.tree.heading("source", text="Source")
self.tree.bind("<<TreeviewSelect>>", self.click_select)
for alarm in self.app.statusbar.core_alarms:
level = self.get_level(alarm.level)
self.tree.insert(
"",
tk.END,
text=str(alarm.date),
values=(
alarm.date,
level + " (%s)" % alarm.level,
alarm.session_id,
alarm.node_id,
alarm.source,
),
tags=(level,),
)
self.tree.tag_configure("ERROR", background="#ff6666")
self.tree.tag_configure("FATAL", background="#d9d9d9")
self.tree.tag_configure("WARNING", background="#ffff99")
self.tree.tag_configure("NOTICE", background="#85e085")
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
self.tree.configure(xscrollcommand=xscrollbar.set)
self.codetext = CodeText(self.top)
self.codetext.text.config(state=tk.DISABLED)
self.codetext.grid(sticky="nsew", pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.columnconfigure(3, weight=1)
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Node Log")
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
def reset_alerts(self):
self.codetext.text.delete("1.0", tk.END)
for item in self.tree.get_children():
self.tree.delete(item)
self.app.statusbar.core_alarms.clear()
def daemon_log(self):
dialog = DaemonLog(self, self.app)
dialog.show()
def get_level(self, level):
if level == core_pb2.ExceptionLevel.ERROR:
return "ERROR"
if level == core_pb2.ExceptionLevel.FATAL:
return "FATAL"
if level == core_pb2.ExceptionLevel.WARNING:
return "WARNING"
if level == core_pb2.ExceptionLevel.NOTICE:
return "NOTICE"
def click_select(self, event):
current = self.tree.selection()
values = self.tree.item(current)["values"]
time = values[0]
level = values[1]
session_id = values[2]
node_id = values[3]
source = values[4]
text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % (
time,
level,
node_id,
self.app.core.canvas_nodes[node_id].core_node.name,
session_id,
source,
)
try:
sid = self.app.core.session_id
self.app.core.client.get_node(sid, node_id)
text = text + "node created"
except RpcError:
text = text + "node not created"
self.codetext.text.delete("1.0", "end")
self.codetext.text.insert("1.0", text)
class DaemonLog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "core-daemon log", modal=True)
self.columnconfigure(0, weight=1)
self.path = tk.StringVar(value="/var/log/core-daemon.log")
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
frame = ttk.Frame(self.top)
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=9)
label = ttk.Label(frame, text="File", anchor="w")
label.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry(frame, textvariable=self.path, state="disabled")
entry.grid(row=0, column=1, sticky="ew")
try:
file = open("/var/log/core-daemon.log", "r")
log = file.readlines()
except FileNotFoundError:
log = "Log file not found"
codetext = CodeText(self.top)
codetext.text.insert("1.0", log)
codetext.text.see("end")
codetext.text.config(state=tk.DISABLED)
codetext.grid(row=1, column=0, sticky="nsew")

View file

@ -0,0 +1,254 @@
"""
size and scale
"""
import tkinter as tk
from tkinter import font, ttk
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
PIXEL_SCALE = 100
class SizeAndScaleDialog(Dialog):
def __init__(self, master, app):
"""
create an instance for size and scale object
:param app: main application
"""
super().__init__(master, app, "Canvas Size and Scale", modal=True)
self.canvas = self.app.canvas
self.validation = app.validation
self.section_font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width)
self.pixel_height = tk.IntVar(value=height)
location = self.app.core.location
self.x = tk.DoubleVar(value=location.x)
self.y = tk.DoubleVar(value=location.y)
self.lat = tk.DoubleVar(value=location.lat)
self.lon = tk.DoubleVar(value=location.lon)
self.alt = tk.DoubleVar(value=location.alt)
self.scale = tk.DoubleVar(value=location.scale)
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale)
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale)
self.save_default = tk.BooleanVar(value=False)
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.draw_size()
self.draw_scale()
self.draw_reference_point()
self.draw_save_as_default()
self.draw_spacer()
self.draw_buttons()
def draw_size(self):
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
# draw size row 1
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.pixel_width,
validate="key",
validatecommand=(self.validation.positive_int, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.pixel_height,
validate="key",
validatecommand=(self.validation.positive_int, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w")
# draw size row 2
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.meters_width,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.meters_height,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=4, sticky="w")
def draw_scale(self):
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.scale,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w")
def draw_reference_point(self):
label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD
)
label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
label = ttk.Label(
label_frame, text="Default is (0, 0), the upper left corner of the canvas"
)
label.grid()
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="X")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.x,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Y")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.y,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(label_frame, text="Translates To")
label.grid()
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
label = ttk.Label(frame, text="Lat")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.lat,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Lon")
label.grid(row=0, column=2, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.lon,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
label = ttk.Label(frame, text="Alt")
label.grid(row=0, column=4, sticky="w", padx=PADX)
entry = ttk.Entry(
frame,
textvariable=self.alt,
validate="key",
validatecommand=(self.validation.positive_float, "%P"),
)
entry.bind("<FocusOut>", lambda event: self.validation.focus_out(event, "0"))
entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self):
button = ttk.Checkbutton(
self.top, text="Save as default?", variable=self.save_default
)
button.grid(sticky="w", pady=PADY)
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(sticky="ew")
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:
self.canvas.redraw_wallpaper()
location = self.app.core.location
location.x = self.x.get()
location.y = self.y.get()
location.lat = self.lat.get()
location.lon = self.lon.get()
location.alt = self.alt.get()
location.scale = self.scale.get()
if self.save_default.get():
location_config = self.app.guiconfig["location"]
location_config["x"] = location.x
location_config["y"] = location.y
location_config["z"] = location.z
location_config["lat"] = location.lat
location_config["lon"] = location.lon
location_config["alt"] = location.alt
location_config["scale"] = location.scale
preferences = self.app.guiconfig["preferences"]
preferences["width"] = width
preferences["height"] = height
self.app.save_config()
self.destroy()

View file

@ -0,0 +1,179 @@
"""
set wallpaper
"""
import logging
import tkinter as tk
from tkinter import ttk
from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.images import Images
from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser
class CanvasBackgroundDialog(Dialog):
def __init__(self, master, app):
"""
create an instance of CanvasWallpaper object
:param coretk.app.Application app: root application
"""
super().__init__(master, app, "Canvas Background", modal=True)
self.canvas = self.app.canvas
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get())
self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get())
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get())
self.filename = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label = None
self.options = []
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.draw_image()
self.draw_image_label()
self.draw_image_selection()
self.draw_options()
self.draw_additional_options()
self.draw_spacer()
self.draw_buttons()
def draw_image(self):
self.image_label = ttk.Label(
self.top, text="(image preview)", width=32, anchor=tk.CENTER
)
self.image_label.grid(pady=PADY)
def draw_image_label(self):
label = ttk.Label(self.top, text="Image filename: ")
label.grid(sticky="ew")
if self.filename.get():
self.draw_preview()
def draw_image_selection(self):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.grid(sticky="ew")
entry = ttk.Entry(frame, textvariable=self.filename)
entry.focus()
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="...", command=self.click_open_image)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Clear", command=self.click_clear)
button.grid(row=0, column=2, sticky="ew")
def draw_options(self):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.columnconfigure(3, weight=1)
frame.grid(sticky="ew")
button = ttk.Radiobutton(
frame, text="upper-left", value=1, variable=self.scale_option
)
button.grid(row=0, column=0, sticky="ew")
self.options.append(button)
button = ttk.Radiobutton(
frame, text="centered", value=2, variable=self.scale_option
)
button.grid(row=0, column=1, sticky="ew")
self.options.append(button)
button = ttk.Radiobutton(
frame, text="scaled", value=3, variable=self.scale_option
)
button.grid(row=0, column=2, sticky="ew")
self.options.append(button)
button = ttk.Radiobutton(
frame, text="titled", value=4, variable=self.scale_option
)
button.grid(row=0, column=3, sticky="ew")
self.options.append(button)
def draw_additional_options(self):
checkbutton = ttk.Checkbutton(
self.top, text="Show grid", variable=self.show_grid
)
checkbutton.grid(sticky="ew", padx=PADX)
checkbutton = ttk.Checkbutton(
self.top,
text="Adjust canvas size to image dimensions",
variable=self.adjust_to_dim,
command=self.click_adjust_canvas,
)
checkbutton.grid(sticky="ew", padx=PADX)
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_open_image(self):
filename = image_chooser(self, BACKGROUNDS_PATH)
if filename:
self.filename.set(filename)
self.draw_preview()
def draw_preview(self):
image = Images.create(self.filename.get(), 250, 135)
self.image_label.config(image=image)
self.image_label.image = image
def click_clear(self):
"""
delete like shown in image link entry if there is any
:return: nothing
"""
# delete entry
self.filename.set("")
# delete display image
self.image_label.config(image="", width=32)
self.image_label.image = None
def click_adjust_canvas(self):
# deselect all radio buttons and grey them out
if self.adjust_to_dim.get():
self.scale_option.set(0)
for option in self.options:
option.config(state=tk.DISABLED)
# turn back the radio button to active state so that user can choose again
else:
self.scale_option.set(1)
for option in self.options:
option.config(state=tk.NORMAL)
def click_apply(self):
self.canvas.scale_option.set(self.scale_option.get())
self.canvas.show_grid.set(self.show_grid.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.update_grid()
filename = self.filename.get()
if not filename:
filename = None
try:
self.canvas.set_wallpaper(filename)
except FileNotFoundError:
logging.error("invalid background: %s", filename)
self.destroy()

View file

@ -0,0 +1,251 @@
"""
custom color picker
"""
import logging
import tkinter as tk
from tkinter import ttk
from core.gui.dialogs.dialog import Dialog
class ColorPicker(Dialog):
def __init__(self, master, app, initcolor="#000000"):
super().__init__(master, app, "color picker", modal=True)
self.red_entry = None
self.blue_entry = None
self.green_entry = None
self.hex_entry = None
self.red_label = None
self.green_label = None
self.blue_label = None
self.display = None
self.color = initcolor
red, green, blue = self.get_rgb(initcolor)
self.red = tk.IntVar(value=red)
self.blue = tk.IntVar(value=blue)
self.green = tk.IntVar(value=green)
self.hex = tk.StringVar(value=initcolor)
self.red_scale = tk.IntVar(value=red)
self.green_scale = tk.IntVar(value=green)
self.blue_scale = tk.IntVar(value=blue)
self.draw()
self.set_bindings()
def askcolor(self):
self.show()
return self.color
def draw(self):
self.top.columnconfigure(0, weight=1)
# rgb frames
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=6)
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="R: ")
label.grid(row=0, column=0)
self.red_entry = ttk.Entry(
frame,
width=4,
textvariable=self.red,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.red_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
from_=0,
to=255,
value=0,
# length=200,
orient=tk.HORIZONTAL,
variable=self.red_scale,
command=lambda x: self.scale_callback(self.red_scale, self.red),
)
scale.grid(row=0, column=2, sticky="nsew")
self.red_label = ttk.Label(
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
)
self.red_label.grid(row=0, column=3, sticky="nsew")
frame.grid(row=0, column=0, sticky="nsew")
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=6)
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="G: ")
label.grid(row=0, column=0)
self.green_entry = ttk.Entry(
frame,
width=4,
textvariable=self.green,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.green_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
from_=0,
to=255,
value=0,
# length=200,
orient=tk.HORIZONTAL,
variable=self.green_scale,
command=lambda x: self.scale_callback(self.green_scale, self.green),
)
scale.grid(row=0, column=2, sticky="nsew")
self.green_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
)
self.green_label.grid(row=0, column=3, sticky="nsew")
frame.grid(row=1, column=0, sticky="nsew")
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=6)
frame.columnconfigure(3, weight=2)
label = ttk.Label(frame, text="B: ")
label.grid(row=0, column=0)
self.blue_entry = ttk.Entry(
frame,
width=4,
textvariable=self.blue,
validate="key",
validatecommand=(self.app.validation.rgb, "%P"),
)
self.blue_entry.grid(row=0, column=1, sticky="nsew")
scale = ttk.Scale(
frame,
from_=0,
to=255,
value=0,
# length=200,
orient=tk.HORIZONTAL,
variable=self.blue_scale,
command=lambda x: self.scale_callback(self.blue_scale, self.blue),
)
scale.grid(row=0, column=2, sticky="nsew")
self.blue_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
)
self.blue_label.grid(row=0, column=3, sticky="nsew")
frame.grid(row=2, column=0, sticky="nsew")
# hex code and color display
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
label = ttk.Label(frame, text="Selection: ")
label.grid(row=0, column=0, sticky="nsew")
self.hex_entry = ttk.Entry(
frame,
textvariable=self.hex,
validate="key",
validatecommand=(self.app.validation.hex, "%P"),
)
self.hex_entry.grid(row=1, column=0, sticky="nsew")
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
self.display.grid(row=2, column=0)
frame.grid(row=3, column=0, sticky="nsew")
# button frame
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="OK", command=self.button_ok)
button.grid(row=0, column=0, sticky="nsew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="nsew")
frame.grid(row=4, column=0, sticky="nsew")
def set_bindings(self):
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.hex_entry.bind("<FocusIn>", lambda x: self.current_focus("hex"))
self.red.trace_add("write", self.update_color)
self.green.trace_add("write", self.update_color)
self.blue.trace_add("write", self.update_color)
self.hex.trace_add("write", self.update_color)
def button_ok(self):
logging.debug("not implemented")
self.color = self.hex.get()
self.destroy()
def get_hex(self):
"""
convert current RGB values into hex color
:rtype: str
:return: hex color
"""
red = self.red_entry.get()
blue = self.blue_entry.get()
green = self.green_entry.get()
return "#%02x%02x%02x" % (int(red), int(green), int(blue))
def current_focus(self, focus):
self.focus = focus
def update_color(self, arg1=None, arg2=None, arg3=None):
if self.focus == "rgb":
red = self.red_entry.get()
blue = self.blue_entry.get()
green = self.green_entry.get()
self.set_scale(red, green, blue)
if red and blue and green:
hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue))
self.hex.set(hex_code)
self.display.config(background=hex_code)
self.set_label(red, green, blue)
elif self.focus == "hex":
hex_code = self.hex.get()
if len(hex_code) == 4 or len(hex_code) == 7:
red, green, blue = self.get_rgb(hex_code)
else:
return
self.set_entry(red, green, blue)
self.set_scale(red, green, blue)
self.display.config(background=hex_code)
self.set_label(red, green, blue)
def scale_callback(self, var, color_var):
color_var.set(var.get())
self.focus = "rgb"
self.update_color()
def set_scale(self, red, green, blue):
self.red_scale.set(red)
self.green_scale.set(green)
self.blue_scale.set(blue)
def set_entry(self, red, green, blue):
self.red.set(red)
self.green.set(green)
self.blue.set(blue)
def set_label(self, red, green, blue):
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
def get_rgb(self, hex_code):
"""
convert a valid hex code to RGB values
:param string hex_code: color in hex
:rtype: tuple(int, int, int)
:return: the RGB values
"""
if len(hex_code) == 4:
red = hex_code[1]
green = hex_code[2]
blue = hex_code[3]
else:
red = hex_code[1:3]
green = hex_code[3:5]
blue = hex_code[5:]
return int(red, 16), int(green, 16), int(blue, 16)

View file

@ -0,0 +1,261 @@
import logging
import tkinter as tk
from pathlib import Path
from tkinter import ttk
from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.images import Images
from core.gui.nodeutils import NodeDraw
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
class ServicesSelectDialog(Dialog):
def __init__(self, master, app, current_services):
super().__init__(master, app, "Node Services", modal=True)
self.groups = None
self.services = None
self.current = None
self.current_services = set(current_services)
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
frame = ttk.LabelFrame(self.top)
frame.grid(stick="nsew", pady=PADY)
frame.rowconfigure(0, weight=1)
for i in range(3):
frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
label_frame.grid(row=0, column=0, sticky="nsew")
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew")
for group in sorted(self.app.core.services):
self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services")
label_frame.grid(row=0, column=1, sticky="nsew")
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
)
self.services.grid(sticky="nsew")
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service)
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.destroy)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button.grid(row=0, column=1, sticky="ew")
# trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>")
def handle_group_change(self, event):
selection = self.groups.listbox.curselection()
if selection:
index = selection[0]
group = self.groups.listbox.get(index)
self.services.clear()
for name in sorted(self.app.core.services[group]):
checked = name in self.current_services
self.services.add(name, checked)
def service_clicked(self, name, var):
if var.get() and name not in self.current_services:
self.current_services.add(name)
elif not var.get() and name in self.current_services:
self.current_services.remove(name)
self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
def click_cancel(self):
self.current_services = None
self.destroy()
class CustomNodesDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Custom Nodes", modal=True)
self.edit_button = None
self.delete_button = None
self.nodes_list = None
self.name = tk.StringVar()
self.image_button = None
self.image = None
self.image_file = None
self.services = set()
self.selected = None
self.selected_index = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_node_config()
self.draw_node_buttons()
self.draw_buttons()
def draw_node_config(self):
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
self.nodes_list = ListboxScroll(frame)
self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX)
self.nodes_list.listbox.bind("<<ListboxSelect>>", self.handle_node_select)
for name in sorted(self.app.core.custom_nodes):
self.nodes_list.listbox.insert(tk.END, name)
frame = ttk.Frame(frame)
frame.grid(row=0, column=2, sticky="nsew")
frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(sticky="ew")
self.image_button = ttk.Button(
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
)
self.image_button.grid(sticky="ew")
button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew")
def draw_node_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
self.edit_button = ttk.Button(
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
)
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def reset_values(self):
self.name.set("")
self.image = None
self.image_file = None
self.services = set()
self.image_button.config(image="")
def click_icon(self):
file_path = image_chooser(self, ICONS_PATH)
if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE)
self.image = image
self.image_file = file_path
self.image_button.config(image=self.image)
def click_services(self):
dialog = ServicesSelectDialog(self, self.app, self.services)
dialog.show()
if dialog.current_services is not None:
self.services.clear()
self.services.update(dialog.current_services)
def click_save(self):
self.app.guiconfig["nodes"].clear()
for name in sorted(self.app.core.custom_nodes):
node_draw = self.app.core.custom_nodes[name]
self.app.guiconfig["nodes"].append(
{
"name": name,
"image": node_draw.image_file,
"services": list(node_draw.services),
}
)
logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"])
self.app.save_config()
self.destroy()
def click_create(self):
name = self.name.get()
if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem
node_draw = NodeDraw.from_custom(name, image_file, set(self.services))
self.app.core.custom_nodes[name] = node_draw
self.nodes_list.listbox.insert(tk.END, name)
self.reset_values()
def click_edit(self):
name = self.name.get()
if self.selected:
previous_name = self.selected
self.selected = name
node_draw = self.app.core.custom_nodes.pop(previous_name)
node_draw.model = name
node_draw.image_file = Path(self.image_file).stem
node_draw.image = self.image
node_draw.services = self.services
self.app.core.custom_nodes[name] = node_draw
self.nodes_list.listbox.delete(self.selected_index)
self.nodes_list.listbox.insert(self.selected_index, name)
self.nodes_list.listbox.selection_set(self.selected_index)
def click_delete(self):
if self.selected and self.selected in self.app.core.custom_nodes:
self.nodes_list.listbox.delete(self.selected_index)
del self.app.core.custom_nodes[self.selected]
self.reset_values()
self.nodes_list.listbox.selection_clear(0, tk.END)
self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
def handle_node_select(self, event):
selection = self.nodes_list.listbox.curselection()
if selection:
self.selected_index = selection[0]
self.selected = self.nodes_list.listbox.get(self.selected_index)
node_draw = self.app.core.custom_nodes[self.selected]
self.name.set(node_draw.model)
self.services = node_draw.services
self.image = node_draw.image
self.image_file = node_draw.image_file
self.image_button.config(image=self.image)
self.edit_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:
self.selected = None
self.selected_index = None
self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)

View file

@ -0,0 +1,37 @@
import tkinter as tk
from tkinter import ttk
from core.gui.images import ImageEnum, Images
from core.gui.themes import DIALOG_PAD
class Dialog(tk.Toplevel):
def __init__(self, master, app, title, modal=False):
super().__init__(master)
self.withdraw()
self.app = app
self.modal = modal
self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy)
image = Images.get(ImageEnum.CORE, 16)
self.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.top = ttk.Frame(self, padding=DIALOG_PAD)
self.top.grid(sticky="nsew")
def show(self):
self.transient(self.master)
self.focus_force()
self.update()
self.deiconify()
if self.modal:
self.wait_visibility()
self.grab_set()
self.wait_window()
def draw_spacer(self, row=None):
frame = ttk.Frame(self.top)
frame.grid(row=row, sticky="nsew")
frame.rowconfigure(0, weight=1)
self.top.rowconfigure(frame.grid_info()["row"], weight=1)

View file

@ -0,0 +1,235 @@
"""
emane configuration
"""
import logging
import tkinter as tk
import webbrowser
from tkinter import ttk
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
class GlobalEmaneDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "EMANE Configuration", modal=True)
self.config_frame = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer()
self.draw_buttons()
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
self.config_frame.parse_config()
self.destroy()
class EmaneModelDialog(Dialog):
def __init__(self, master, app, node, model, interface=None):
super().__init__(master, app, f"{node.name} {model} Configuration", modal=True)
self.node = node
self.model = f"emane_{model}"
self.interface = interface
self.config_frame = None
try:
self.config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface
)
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer()
self.draw_buttons()
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
self.config_frame.parse_config()
self.app.core.set_emane_model_config(
self.node.id, self.model, self.config, self.interface
)
self.destroy()
class EmaneConfigDialog(Dialog):
def __init__(self, master, app, canvas_node):
super().__init__(
master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True
)
self.app = app
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.radiovar = tk.IntVar()
self.radiovar.set(1)
self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models]
self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1])
self.emane_model_button = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.draw_emane_configuration()
self.draw_emane_models()
self.draw_emane_buttons()
self.draw_spacer()
self.draw_apply_and_cancel()
def draw_emane_configuration(self):
"""
draw the main frame for emane configuration
:return: nothing
"""
label = ttk.Label(
self.top,
text="The EMANE emulation system provides more complex wireless radio emulation "
"\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details",
justify=tk.CENTER,
)
label.grid(pady=PADY)
image = Images.get(ImageEnum.EDITNODE, 16)
button = ttk.Button(
self.top,
image=image,
text="EMANE Wiki",
compound=tk.RIGHT,
command=lambda: webbrowser.open_new(
"https://github.com/adjacentlink/emane/wiki"
),
)
button.image = image
button.grid(sticky="ew", pady=PADY)
def draw_emane_models(self):
"""
create a combobox that has all the known emane models
:return: nothing
"""
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Model")
label.grid(row=0, column=0, sticky="w")
# create combo box and its binding
combobox = ttk.Combobox(
frame,
textvariable=self.emane_model,
values=self.emane_models,
state="readonly",
)
combobox.grid(row=0, column=1, sticky="ew")
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
def draw_emane_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(2):
frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.EDITNODE, 16)
self.emane_model_button = ttk.Button(
frame,
text=f"{self.emane_model.get()} options",
image=image,
compound=tk.RIGHT,
command=self.click_model_config,
)
self.emane_model_button.image = image
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.EDITNODE, 16)
button = ttk.Button(
frame,
text="EMANE options",
image=image,
compound=tk.RIGHT,
command=self.click_emane_config,
)
button.image = image
button.grid(row=0, column=1, sticky="ew")
def draw_apply_and_cancel(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self):
dialog = GlobalEmaneDialog(self, self.app)
dialog.show()
def click_model_config(self):
"""
draw emane model configuration
:return: nothing
"""
model_name = self.emane_model.get()
logging.info("configuring emane model: %s", model_name)
dialog = EmaneModelDialog(
self, self.app, self.canvas_node.core_node, model_name
)
dialog.show()
def emane_model_change(self, event):
"""
update emane model options button
:param event:
:return: nothing
"""
model_name = self.emane_model.get()
self.emane_model_button.config(text=f"{model_name} options")
def click_apply(self):
self.node.emane = f"emane_{self.emane_model.get()}"
self.destroy()

View file

@ -0,0 +1,152 @@
import tkinter as tk
from tkinter import ttk
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
class HookDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Hook", modal=True)
self.name = tk.StringVar()
self.codetext = None
self.hook = core_pb2.Hook()
self.state = tk.StringVar()
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
# name and states
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=7)
frame.columnconfigure(2, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE")
initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME)
self.state.set(initial_state)
self.name.set(f"{initial_state.lower()}_hook.sh")
combobox = ttk.Combobox(
frame, textvariable=self.state, values=values, state="readonly"
)
combobox.grid(row=0, column=2, sticky="ew")
combobox.bind("<<ComboboxSelected>>", self.state_change)
# data
self.codetext = CodeText(self.top)
self.codetext.text.insert(
1.0,
(
"#!/bin/sh\n"
"# session hook script; write commands here to execute on the host at the\n"
"# specified state\n"
),
)
self.codetext.grid(sticky="nsew", pady=PADY)
# button row
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=lambda: self.save())
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=1, sticky="ew")
def state_change(self, event):
state_name = self.state.get()
self.name.set(f"{state_name.lower()}_hook.sh")
def set(self, hook):
self.hook = hook
self.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END)
self.codetext.text.insert(tk.END, hook.data)
state_name = core_pb2.SessionState.Enum.Name(hook.state)
self.state.set(state_name)
def save(self):
data = self.codetext.text.get("1.0", tk.END).strip()
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
self.hook.file = self.name.get()
self.hook.data = data
self.hook.state = state_value
self.destroy()
class HooksDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Hooks", modal=True)
self.listbox = None
self.edit_button = None
self.delete_button = None
self.selected = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
self.listbox = listbox_scroll.listbox
self.listbox.bind("<<ListboxSelect>>", self.select)
for hook_file in self.app.core.hooks:
self.listbox.insert(tk.END, hook_file)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
self.edit_button = ttk.Button(
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
)
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=3, sticky="ew")
def click_create(self):
dialog = HookDialog(self, self.app)
dialog.show()
hook = dialog.hook
if hook:
self.app.core.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file)
def click_edit(self):
hook = self.app.core.hooks[self.selected]
dialog = HookDialog(self, self.app)
dialog.set(hook)
dialog.show()
def click_delete(self):
del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR)
self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
def select(self, event):
if self.listbox.curselection():
index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index)
self.edit_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:
self.selected = None
self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)

View file

@ -0,0 +1,362 @@
"""
link configuration
"""
import logging
import tkinter as tk
from tkinter import ttk
from core.api.grpc import core_pb2
from core.gui.dialogs.colorpicker import ColorPicker
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
def get_int(var):
value = var.get()
if value != "":
return int(value)
else:
return None
def get_float(var):
value = var.get()
if value != "":
return float(value)
else:
return None
class LinkConfiguration(Dialog):
def __init__(self, master, app, edge):
super().__init__(master, app, "Link Configuration", modal=True)
self.app = app
self.edge = edge
self.is_symmetric = edge.link.options.unidirectional is False
if self.is_symmetric:
self.symmetry_var = tk.StringVar(value=">>")
else:
self.symmetry_var = tk.StringVar(value="<<")
self.bandwidth = tk.StringVar()
self.delay = tk.StringVar()
self.jitter = tk.StringVar()
self.loss = tk.StringVar()
self.duplicate = tk.StringVar()
self.down_bandwidth = tk.StringVar()
self.down_delay = tk.StringVar()
self.down_jitter = tk.StringVar()
self.down_loss = tk.StringVar()
self.down_duplicate = tk.StringVar()
self.color = tk.StringVar(value="#000000")
self.color_button = None
self.width = tk.DoubleVar()
self.load_link_config()
self.symmetric_frame = None
self.asymmetric_frame = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
source_name = self.app.canvas.nodes[self.edge.src].core_node.name
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
label = ttk.Label(
self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER
)
label.grid(row=0, column=0, sticky="ew", pady=PADY)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
button = ttk.Button(frame, text="Unlimited")
button.grid(row=0, column=0, sticky="ew", padx=PADX)
if self.is_symmetric:
button = ttk.Button(
frame, textvariable=self.symmetry_var, command=self.change_symmetry
)
else:
button = ttk.Button(
frame, textvariable=self.symmetry_var, command=self.change_symmetry
)
button.grid(row=0, column=1, sticky="ew")
if self.is_symmetric:
self.symmetric_frame = self.get_frame()
self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
else:
self.asymmetric_frame = self.get_frame()
self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
self.draw_spacer(row=3)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=4, column=0, sticky="ew")
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def get_frame(self):
frame = ttk.Frame(self.top)
frame.columnconfigure(1, weight=1)
if self.is_symmetric:
label_name = "Symmetric Link Effects"
else:
label_name = "Asymmetric Effects: Downstream / Upstream "
row = 0
label = ttk.Label(frame, text=label_name, anchor=tk.CENTER)
label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Bandwidth (bps)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_bandwidth,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Delay (us)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_delay,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Jitter (us)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_jitter,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Loss (%)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_loss,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Duplicate (%)")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
if not self.is_symmetric:
entry = ttk.Entry(
frame,
textvariable=self.down_duplicate,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Color")
label.grid(row=row, column=0, sticky="ew")
self.color_button = tk.Button(
frame,
textvariable=self.color,
background=self.color.get(),
bd=0,
relief=tk.FLAT,
highlightthickness=0,
command=self.click_color,
)
self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Width")
label.grid(row=row, column=0, sticky="ew")
entry = ttk.Entry(
frame,
textvariable=self.width,
validate="key",
validatecommand=(self.app.validation.positive_float, "%P"),
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
return frame
def click_color(self):
dialog = ColorPicker(self, self.app, self.color.get())
color = dialog.askcolor()
self.color.set(color)
self.color_button.config(background=color)
def click_apply(self):
logging.debug("click apply")
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
link = self.edge.link
bandwidth = get_int(self.bandwidth)
jitter = get_int(self.jitter)
delay = get_int(self.delay)
duplicate = get_int(self.duplicate)
loss = get_float(self.loss)
options = core_pb2.LinkOptions(
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss
)
link.options.CopyFrom(options)
interface_one = None
if link.HasField("interface_one"):
interface_one = link.interface_one.id
interface_two = None
if link.HasField("interface_two"):
interface_two = link.interface_two.id
if not self.is_symmetric:
link.options.unidirectional = True
asym_interface_one = None
if interface_one:
asym_interface_one = core_pb2.Interface(id=interface_one)
asym_interface_two = None
if interface_two:
asym_interface_two = core_pb2.Interface(id=interface_two)
down_bandwidth = get_int(self.down_bandwidth)
down_jitter = get_int(self.down_jitter)
down_delay = get_int(self.down_delay)
down_duplicate = get_int(self.down_duplicate)
down_loss = get_float(self.down_loss)
options = core_pb2.LinkOptions(
bandwidth=down_bandwidth,
jitter=down_jitter,
delay=down_delay,
dup=down_duplicate,
per=down_loss,
unidirectional=True,
)
self.edge.asymmetric_link = core_pb2.Link(
node_one_id=link.node_two_id,
node_two_id=link.node_one_id,
interface_one=asym_interface_one,
interface_two=asym_interface_two,
options=options,
)
else:
link.options.unidirectional = False
self.edge.asymmetric_link = None
if self.app.core.is_runtime() and link.HasField("options"):
session_id = self.app.core.session_id
self.app.core.client.edit_link(
session_id,
link.node_one_id,
link.node_two_id,
link.options,
interface_one,
interface_two,
)
if self.edge.asymmetric_link:
self.app.core.client.edit_link(
session_id,
link.node_two_id,
link.node_one_id,
self.edge.asymmetric_link.options,
interface_one,
interface_two,
)
self.destroy()
def change_symmetry(self):
logging.debug("change symmetry")
if self.is_symmetric:
self.is_symmetric = False
self.symmetry_var.set("<<")
if not self.asymmetric_frame:
self.asymmetric_frame = self.get_frame()
self.symmetric_frame.grid_forget()
self.asymmetric_frame.grid(row=2, column=0)
else:
self.is_symmetric = True
self.symmetry_var.set(">>")
if not self.symmetric_frame:
self.symmetric_frame = self.get_frame()
self.asymmetric_frame.grid_forget()
self.symmetric_frame.grid(row=2, column=0)
def load_link_config(self):
"""
populate link config to the table
:return: nothing
"""
width = self.app.canvas.itemcget(self.edge.id, "width")
self.width.set(width)
color = self.app.canvas.itemcget(self.edge.id, "fill")
self.color.set(color)
link = self.edge.link
if link.HasField("options"):
self.bandwidth.set(str(link.options.bandwidth))
self.jitter.set(str(link.options.jitter))
self.duplicate.set(str(link.options.dup))
self.loss.set(str(link.options.per))
self.delay.set(str(link.options.delay))
if not self.is_symmetric:
asym_link = self.edge.asymmetric_link
self.down_bandwidth.set(str(asym_link.options.bandwidth))
self.down_jitter.set(str(asym_link.options.jitter))
self.down_duplicate.set(str(asym_link.options.dup))
self.down_loss.set(str(asym_link.options.per))
self.down_delay.set(str(asym_link.options.delay))

View file

@ -0,0 +1,73 @@
"""
marker dialog
"""
import logging
import tkinter as tk
from tkinter import ttk
from core.gui.dialogs.colorpicker import ColorPicker
from core.gui.dialogs.dialog import Dialog
MARKER_THICKNESS = [3, 5, 8, 10]
class Marker(Dialog):
def __init__(self, master, app, initcolor="#000000"):
super().__init__(master, app, "marker tool", modal=False)
self.app = app
self.color = initcolor
self.radius = MARKER_THICKNESS[0]
self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0])
self.draw()
self.top.bind("<Destroy>", self.close_marker)
def draw(self):
button = ttk.Button(self.top, text="clear", command=self.clear_marker)
button.grid(row=0, column=0, sticky="nsew")
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=4)
frame.grid(row=1, column=0, sticky="nsew")
label = ttk.Label(frame, text="Thickness: ")
label.grid(row=0, column=0, sticky="nsew")
combobox = ttk.Combobox(
frame,
textvariable=self.marker_thickness,
values=MARKER_THICKNESS,
state="readonly",
)
combobox.grid(row=0, column=1, sticky="nsew")
combobox.bind("<<ComboboxSelected>>", self.change_thickness)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=4)
frame.grid(row=2, column=0, sticky="nsew")
label = ttk.Label(frame, text="Color: ")
label.grid(row=0, column=0, sticky="nsew")
label = ttk.Label(frame, background=self.color)
label.grid(row=0, column=1, sticky="nsew")
label.bind("<Button-1>", self.change_color)
def clear_marker(self):
canvas = self.app.canvas
for i in canvas.find_withtag("marker"):
canvas.delete(i)
def change_color(self, event):
color_picker = ColorPicker(self, self.app, self.color)
color = color_picker.askcolor()
event.widget.configure(background=color)
self.color = color
def change_thickness(self, event):
self.radius = self.marker_thickness.get()
def close_marker(self, event):
logging.debug("destroy marker dialog")
self.app.toolbar.marker_tool = None
def position(self):
print(self.winfo_width(), self.winfo_height())
self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y))

View file

@ -0,0 +1,55 @@
"""
mobility configuration
"""
from tkinter import ttk
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
class MobilityConfigDialog(Dialog):
def __init__(self, master, app, canvas_node):
super().__init__(
master,
app,
f"{canvas_node.core_node.name} Mobility Configuration",
modal=True,
)
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
try:
self.config = self.app.core.get_mobility_config(self.node.id)
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_apply_buttons()
def draw_apply_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
self.config_frame.parse_config()
self.app.core.mobility_configs[self.node.id] = self.config
self.destroy()

View file

@ -0,0 +1,163 @@
import tkinter as tk
from tkinter import ttk
import grpc
from core.api.grpc.core_pb2 import MobilityAction
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY
ICON_SIZE = 16
class MobilityPlayer:
def __init__(self, master, app, canvas_node, config):
self.master = master
self.app = app
self.canvas_node = canvas_node
self.config = config
self.dialog = None
self.state = None
def show(self):
if self.dialog:
self.dialog.destroy()
self.dialog = MobilityPlayerDialog(
self.master, self.app, self.canvas_node, self.config
)
self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close)
if self.state == MobilityAction.START:
self.set_play()
elif self.state == MobilityAction.PAUSE:
self.set_pause()
else:
self.set_stop()
self.dialog.show()
def handle_close(self):
self.dialog.destroy()
self.dialog = None
def set_play(self):
self.state = MobilityAction.START
if self.dialog:
self.dialog.set_play()
def set_pause(self):
self.state = MobilityAction.PAUSE
if self.dialog:
self.dialog.set_pause()
def set_stop(self):
self.state = MobilityAction.STOP
if self.dialog:
self.dialog.set_stop()
class MobilityPlayerDialog(Dialog):
def __init__(self, master, app, canvas_node, config):
super().__init__(
master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False
)
self.resizable(False, False)
self.geometry("")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config = config
self.play_button = None
self.pause_button = None
self.stop_button = None
self.progressbar = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
file_name = self.config["file"].value
label = ttk.Label(self.top, text=file_name)
label.grid(sticky="ew", pady=PADY)
self.progressbar = ttk.Progressbar(self.top, mode="indeterminate")
self.progressbar.grid(sticky="ew", pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.START, width=ICON_SIZE)
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
self.play_button.image = image
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE)
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
self.pause_button.image = image
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.STOP, width=ICON_SIZE)
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
loop = tk.IntVar(value=int(self.config["loop"].value == "1"))
checkbutton = ttk.Checkbutton(
frame, text="Loop?", variable=loop, state=tk.DISABLED
)
checkbutton.grid(row=0, column=3, padx=PADX)
rate = self.config["refresh_ms"].value
label = ttk.Label(frame, text=f"rate {rate} ms")
label.grid(row=0, column=4)
def clear_buttons(self):
self.play_button.state(["!pressed"])
self.pause_button.state(["!pressed"])
self.stop_button.state(["!pressed"])
def set_play(self):
self.clear_buttons()
self.play_button.state(["pressed"])
self.progressbar.start()
def set_pause(self):
self.clear_buttons()
self.pause_button.state(["pressed"])
self.progressbar.stop()
def set_stop(self):
self.clear_buttons()
self.stop_button.state(["pressed"])
self.progressbar.stop()
def click_play(self):
self.set_play()
session_id = self.app.core.session_id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.START
)
except grpc.RpcError as e:
show_grpc_error(e)
def click_pause(self):
self.set_pause()
session_id = self.app.core.session_id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.PAUSE
)
except grpc.RpcError as e:
show_grpc_error(e)
def click_stop(self):
self.set_stop()
session_id = self.app.core.session_id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.STOP
)
except grpc.RpcError as e:
show_grpc_error(e)

View file

@ -0,0 +1,237 @@
import logging
import tkinter as tk
from functools import partial
from tkinter import ttk
from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.emaneconfig import EmaneModelDialog
from core.gui.images import Images
from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import image_chooser
def mac_auto(is_auto, entry):
logging.info("mac auto clicked")
if is_auto.get():
logging.info("disabling mac")
entry.var.set("")
entry.config(state=tk.DISABLED)
else:
entry.var.set("00:00:00:00:00:00")
entry.config(state=tk.NORMAL)
class InterfaceData:
def __init__(self, is_auto, mac, ip4, ip6):
self.is_auto = is_auto
self.mac = mac
self.ip4 = ip4
self.ip6 = ip6
class NodeConfigDialog(Dialog):
def __init__(self, master, app, canvas_node):
"""
create an instance of node configuration
:param master: dialog master
:param coretk.app.Application: main app
:param coretk.graph.CanvasNode canvas_node: canvas node object
"""
super().__init__(
master, app, f"{canvas_node.core_node.name} Configuration", modal=True
)
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.image = canvas_node.image
self.image_file = None
self.image_button = None
self.name = tk.StringVar(value=self.node.name)
self.type = tk.StringVar(value=self.node.model)
self.container_image = tk.StringVar(value=self.node.image)
server = "localhost"
if self.node.server:
server = self.node.server
self.server = tk.StringVar(value=server)
self.interfaces = {}
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
row = 0
# field frame
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.columnconfigure(1, weight=1)
# icon field
label = ttk.Label(frame, text="Icon")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
self.image_button = ttk.Button(
frame,
text="Icon",
image=self.image,
compound=tk.NONE,
command=self.click_icon,
)
self.image_button.grid(row=row, column=1, sticky="ew")
row += 1
# name field
label = ttk.Label(frame, text="Name")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
entry = ttk.Entry(
frame,
textvariable=self.name,
validate="key",
validatecommand=(self.app.validation.name, "%P"),
)
entry.bind(
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "noname")
)
entry.grid(row=row, column=1, sticky="ew")
row += 1
# node type field
if NodeUtils.is_model_node(self.node.type):
label = ttk.Label(frame, text="Type")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
combobox = ttk.Combobox(
frame,
textvariable=self.type,
values=list(NodeUtils.NODE_MODELS),
state="readonly",
)
combobox.grid(row=row, column=1, sticky="ew")
row += 1
# container image field
if NodeUtils.is_image_node(self.node.type):
label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image)
entry.grid(row=row, column=1, sticky="ew")
row += 1
if NodeUtils.is_container_node(self.node.type):
# server
frame.grid(sticky="ew")
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Server")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
servers = ["localhost"]
servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox(
frame, textvariable=self.server, values=servers, state="readonly"
)
combobox.grid(row=row, column=1, sticky="ew")
row += 1
# interfaces
if self.canvas_node.interfaces:
self.draw_interfaces()
self.draw_spacer()
self.draw_buttons()
def draw_interfaces(self):
notebook = ttk.Notebook(self.top)
notebook.grid(sticky="nsew", pady=PADY)
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
for interface in self.canvas_node.interfaces:
logging.info("interface: %s", interface)
tab = ttk.Frame(notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew", pady=PADY)
tab.columnconfigure(1, weight=1)
tab.columnconfigure(2, weight=1)
notebook.add(tab, text=interface.name)
row = 0
emane_node = self.canvas_node.has_emane_link(interface.id)
if emane_node:
emane_model = emane_node.emane.split("_")[1]
button = ttk.Button(
tab,
text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, interface.id),
)
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
row += 1
label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY)
is_auto = tk.BooleanVar(value=True)
checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto)
checkbutton.var = is_auto
checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=interface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED)
entry.grid(row=row, column=2, sticky="ew")
func = partial(mac_auto, is_auto, entry)
checkbutton.config(command=func)
row += 1
label = ttk.Label(tab, text="IPv4")
label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}")
entry = ttk.Entry(tab, textvariable=ip4)
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
row += 1
label = ttk.Label(tab, text="IPv6")
label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}")
entry = ttk.Entry(tab, textvariable=ip6)
entry.bind("<FocusOut>", self.app.validation.ip_focus_out)
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.config_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_emane_config(self, emane_model, interface_id):
dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id)
dialog.show()
def click_icon(self):
file_path = image_chooser(self, ICONS_PATH)
if file_path:
self.image = Images.create(file_path, nodeutils.ICON_SIZE)
self.image_button.config(image=self.image)
self.image_file = file_path
def config_apply(self):
# update core node
self.node.name = self.name.get()
if NodeUtils.is_image_node(self.node.type):
self.node.image = self.container_image.get()
server = self.server.get()
if NodeUtils.is_container_node(self.node.type) and server != "localhost":
self.node.server = server
# set custom icon
if self.image_file:
self.node.icon = self.image_file
# update canvas node
self.canvas_node.image = self.image
# redraw
self.canvas_node.redraw()
self.destroy()

View file

@ -0,0 +1,134 @@
"""
core node services
"""
import tkinter as tk
from tkinter import messagebox, ttk
from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.serviceconfiguration import ServiceConfiguration
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll
class NodeService(Dialog):
def __init__(self, master, app, canvas_node, services=None):
title = f"{canvas_node.core_node.name} Services"
super().__init__(master, app, title, modal=True)
self.app = app
self.canvas_node = canvas_node
self.node_id = canvas_node.core_node.id
self.groups = None
self.services = None
self.current = None
if services is None:
services = canvas_node.core_node.services
model = canvas_node.core_node.model
if len(services) == 0:
services = set(self.app.core.default_services[model])
else:
services = set(services)
self.current_services = services
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
frame = ttk.Frame(self.top)
frame.grid(stick="nsew", pady=PADY)
frame.rowconfigure(0, weight=1)
for i in range(3):
frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
label_frame.grid(row=0, column=0, sticky="nsew")
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew")
for group in sorted(self.app.core.services):
self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services")
label_frame.grid(row=0, column=1, sticky="nsew")
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
)
self.services.grid(sticky="nsew")
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service)
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Configure", command=self.click_configure)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button.grid(row=0, column=2, sticky="ew")
# trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>")
def handle_group_change(self, event):
selection = self.groups.listbox.curselection()
if selection:
index = selection[0]
group = self.groups.listbox.get(index)
self.services.clear()
for name in sorted(self.app.core.services[group]):
checked = name in self.current_services
self.services.add(name, checked)
def service_clicked(self, name, var):
if var.get() and name not in self.current_services:
self.current_services.add(name)
elif not var.get() and name in self.current_services:
self.current_services.remove(name)
self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
self.canvas_node.core_node.services[:] = self.current_services
def click_configure(self):
current_selection = self.current.listbox.curselection()
if len(current_selection):
dialog = ServiceConfiguration(
master=self,
app=self.app,
service_name=self.current.listbox.get(current_selection[0]),
node_id=self.node_id,
)
dialog.show()
else:
messagebox.showinfo(
"Node service configuration", "Select a service to configure"
)
def click_save(self):
if (
self.current_services
!= self.app.core.default_services[self.canvas_node.core_node.model]
):
self.canvas_node.core_node.services[:] = self.current_services
else:
if len(self.canvas_node.core_node.services) > 0:
self.canvas_node.core_node.services[:] = []
self.destroy()
def click_cancel(self):
self.current_services = None
self.destroy()

View file

@ -0,0 +1,143 @@
import tkinter as tk
from tkinter import ttk
from core.gui.coreclient import Observer
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import ListboxScroll
class ObserverDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Observer Widgets", modal=True)
self.observers = None
self.save_button = None
self.delete_button = None
self.selected = None
self.selected_index = None
self.name = tk.StringVar()
self.cmd = tk.StringVar()
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_listbox()
self.draw_form_fields()
self.draw_config_buttons()
self.draw_apply_buttons()
def draw_listbox(self):
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.columnconfigure(0, weight=1)
listbox_scroll.rowconfigure(0, weight=1)
self.observers = listbox_scroll.listbox
self.observers.grid(row=0, column=0, sticky="nsew")
self.observers.bind("<<ListboxSelect>>", self.handle_observer_change)
for name in sorted(self.app.core.custom_observers):
self.observers.insert(tk.END, name)
def draw_form_fields(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew")
label = ttk.Label(frame, text="Command")
label.grid(row=1, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.cmd)
entry.grid(row=1, column=1, sticky="ew")
def draw_config_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
self.save_button = ttk.Button(
frame, text="Save", state=tk.DISABLED, command=self.click_save
)
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save_config)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_save_config(self):
observers = []
for name in sorted(self.app.core.custom_observers):
observer = self.app.core.custom_observers[name]
observers.append({"name": observer.name, "cmd": observer.cmd})
self.app.guiconfig["observers"] = observers
self.app.save_config()
self.destroy()
def click_create(self):
name = self.name.get()
if name not in self.app.core.custom_observers:
cmd = self.cmd.get()
observer = Observer(name, cmd)
self.app.core.custom_observers[name] = observer
self.observers.insert(tk.END, name)
def click_save(self):
name = self.name.get()
if self.selected:
previous_name = self.selected
self.selected = name
observer = self.app.core.custom_observers.pop(previous_name)
observer.name = name
observer.cmd = self.cmd.get()
self.app.core.custom_observers[name] = observer
self.observers.delete(self.selected_index)
self.observers.insert(self.selected_index, name)
self.observers.selection_set(self.selected_index)
def click_delete(self):
if self.selected:
self.observers.delete(self.selected_index)
del self.app.core.custom_observers[self.selected]
self.selected = None
self.selected_index = None
self.name.set("")
self.cmd.set("")
self.observers.selection_clear(0, tk.END)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
def handle_observer_change(self, event):
selection = self.observers.curselection()
if selection:
self.selected_index = selection[0]
self.selected = self.observers.get(self.selected_index)
observer = self.app.core.custom_observers[self.selected]
self.name.set(observer.name)
self.cmd.set(observer.cmd)
self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:
self.selected_index = None
self.selected = None
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)

View file

@ -0,0 +1,87 @@
import logging
import tkinter as tk
from tkinter import ttk
from core.gui import appconfig
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
class PreferencesDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Preferences", modal=True)
preferences = self.app.guiconfig["preferences"]
self.editor = tk.StringVar(value=preferences["editor"])
self.theme = tk.StringVar(value=preferences["theme"])
self.terminal = tk.StringVar(value=preferences["terminal"])
self.gui3d = tk.StringVar(value=preferences["gui3d"])
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_preferences()
self.draw_buttons()
def draw_preferences(self):
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Theme")
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w")
themes = self.app.style.theme_names()
combobox = ttk.Combobox(
frame, textvariable=self.theme, values=themes, state="readonly"
)
combobox.set(self.theme.get())
combobox.grid(row=0, column=1, sticky="ew")
combobox.bind("<<ComboboxSelected>>", self.theme_change)
label = ttk.Label(frame, text="Editor")
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w")
combobox = ttk.Combobox(
frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly"
)
combobox.grid(row=1, column=1, sticky="ew")
label = ttk.Label(frame, text="Terminal")
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
combobox = ttk.Combobox(
frame,
textvariable=self.terminal,
values=appconfig.TERMINALS,
state="readonly",
)
combobox.grid(row=2, column=1, sticky="ew")
label = ttk.Label(frame, text="3D GUI")
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w")
entry = ttk.Entry(frame, textvariable=self.gui3d)
entry.grid(row=3, column=1, sticky="ew")
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def theme_change(self, event):
theme = self.theme.get()
logging.info("changing theme: %s", theme)
self.app.style.theme_use(theme)
def click_save(self):
preferences = self.app.guiconfig["preferences"]
preferences["terminal"] = self.terminal.get()
preferences["editor"] = self.editor.get()
preferences["gui3d"] = self.gui3d.get()
preferences["theme"] = self.theme.get()
self.app.save_config()
self.destroy()

View file

@ -0,0 +1,173 @@
import tkinter as tk
from tkinter import ttk
from core.gui.coreclient import CoreServer
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll
DEFAULT_NAME = "example"
DEFAULT_ADDRESS = "127.0.0.1"
DEFAULT_PORT = 50051
class ServersDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "CORE Servers", modal=True)
self.name = tk.StringVar(value=DEFAULT_NAME)
self.address = tk.StringVar(value=DEFAULT_ADDRESS)
self.port = tk.IntVar(value=DEFAULT_PORT)
self.servers = None
self.selected_index = None
self.selected = None
self.save_button = None
self.delete_button = None
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_servers()
self.draw_servers_buttons()
self.draw_server_configuration()
self.draw_apply_buttons()
def draw_servers(self):
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(pady=PADY, sticky="nsew")
listbox_scroll.columnconfigure(0, weight=1)
listbox_scroll.rowconfigure(0, weight=1)
self.servers = listbox_scroll.listbox
self.servers.grid(row=0, column=0, sticky="nsew")
self.servers.bind("<<ListboxSelect>>", self.handle_server_change)
for server in self.app.core.servers:
self.servers.insert(tk.END, server)
def draw_server_configuration(self):
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew")
label = ttk.Label(frame, text="Address")
label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.address)
entry.grid(row=0, column=3, sticky="ew")
label = ttk.Label(frame, text="Port")
label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY)
entry = ttk.Entry(
frame,
textvariable=self.port,
validate="key",
validatecommand=(self.app.validation.positive_int, "%P"),
)
entry.bind(
"<FocusOut>", lambda event: self.app.validation.focus_out(event, "50051")
)
entry.grid(row=0, column=5, sticky="ew")
def draw_servers_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew")
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
self.save_button = ttk.Button(
frame, text="Save", state=tk.DISABLED, command=self.click_save
)
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(
frame, text="Save Configuration", command=self.click_save_configuration
)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_save_configuration(self):
servers = []
for name in sorted(self.app.core.servers):
server = self.app.core.servers[name]
servers.append(
{"name": server.name, "address": server.address, "port": server.port}
)
self.app.guiconfig["servers"] = servers
self.app.save_config()
self.destroy()
def click_create(self):
name = self.name.get()
if name not in self.app.core.servers:
address = self.address.get()
port = self.port.get()
server = CoreServer(name, address, port)
self.app.core.servers[name] = server
self.servers.insert(tk.END, name)
def click_save(self):
name = self.name.get()
if self.selected:
previous_name = self.selected
self.selected = name
server = self.app.core.servers.pop(previous_name)
server.name = name
server.address = self.address.get()
server.port = self.port.get()
self.app.core.servers[name] = server
self.servers.delete(self.selected_index)
self.servers.insert(self.selected_index, name)
self.servers.selection_set(self.selected_index)
def click_delete(self):
if self.selected:
self.servers.delete(self.selected_index)
del self.app.core.servers[self.selected]
self.selected = None
self.selected_index = None
self.name.set(DEFAULT_NAME)
self.address.set(DEFAULT_ADDRESS)
self.port.set(DEFAULT_PORT)
self.servers.selection_clear(0, tk.END)
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
def handle_server_change(self, event):
selection = self.servers.curselection()
if selection:
self.selected_index = selection[0]
self.selected = self.servers.get(self.selected_index)
server = self.app.core.servers[self.selected]
self.name.set(server.name)
self.address.set(server.address)
self.port.set(server.port)
self.save_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:
self.selected_index = None
self.selected = None
self.save_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)

View file

@ -0,0 +1,453 @@
"Service configuration dialog"
import logging
import tkinter as tk
from tkinter import ttk
import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
class ServiceConfiguration(Dialog):
def __init__(self, master, app, service_name, node_id):
title = f"{service_name} Service"
super().__init__(master, app, title, modal=True)
self.app = app
self.core = app.core
self.node_id = node_id
self.service_name = service_name
self.radiovar = tk.IntVar()
self.radiovar.set(2)
self.metadata = ""
self.filenames = []
self.dependencies = []
self.executables = []
self.startup_commands = []
self.validation_commands = []
self.shutdown_commands = []
self.validation_mode = None
self.validation_time = None
self.validation_period = None
self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16)
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16)
self.notebook = None
self.metadata_entry = None
self.filename_combobox = None
self.startup_commands_listbox = None
self.shutdown_commands_listbox = None
self.validate_commands_listbox = None
self.validation_time_entry = None
self.validation_mode_entry = None
self.service_file_data = None
self.validation_period_entry = None
self.original_service_files = {}
self.temp_service_files = {}
self.modified_files = set()
self.load()
self.draw()
def load(self):
try:
self.app.core.create_nodes_and_links()
service_configs = self.app.core.service_configs
if (
self.node_id in service_configs
and self.service_name in service_configs[self.node_id]
):
service_config = self.app.core.service_configs[self.node_id][
self.service_name
]
else:
service_config = self.app.core.get_node_service(
self.node_id, self.service_name
)
self.dependencies = [x for x in service_config.dependencies]
self.executables = [x for x in service_config.executables]
self.metadata = service_config.meta
self.filenames = [x for x in service_config.configs]
self.startup_commands = [x for x in service_config.startup]
self.validation_commands = [x for x in service_config.validate]
self.shutdown_commands = [x for x in service_config.shutdown]
self.validation_mode = service_config.validation_mode
self.validation_time = service_config.validation_timer
self.original_service_files = {
x: self.app.core.get_node_service_file(
self.node_id, self.service_name, x
)
for x in self.filenames
}
self.temp_service_files = {
x: self.original_service_files[x] for x in self.original_service_files
}
file_configs = self.app.core.file_configs
if (
self.node_id in file_configs
and self.service_name in file_configs[self.node_id]
):
for file, data in file_configs[self.node_id][self.service_name].items():
self.temp_service_files[file] = data
except grpc.RpcError as e:
show_grpc_error(e)
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
# draw metadata
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Meta-data")
label.grid(row=0, column=0, sticky="w", padx=PADX)
self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata)
self.metadata_entry.grid(row=0, column=1, sticky="ew")
# draw notebook
self.notebook = ttk.Notebook(self.top)
self.notebook.grid(sticky="nsew", pady=PADY)
self.draw_tab_files()
self.draw_tab_directories()
self.draw_tab_startstop()
self.draw_tab_configuration()
button = ttk.Button(self.top, text="Only Save Changes")
button.grid(sticky="ew", pady=PADY)
self.draw_buttons()
def draw_tab_files(self):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Files")
label = ttk.Label(
tab, text="Config files and scripts that are generated for this service."
)
label.grid()
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="File Name")
label.grid(row=0, column=0, padx=PADX, sticky="w")
self.filename_combobox = ttk.Combobox(
frame, values=self.filenames, state="readonly"
)
self.filename_combobox.bind(
"<<ComboboxSelected>>", self.display_service_file_data
)
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, image=self.documentnew_img, state="disabled")
button.bind("<Button-1>", self.add_filename)
button.grid(row=0, column=2, padx=PADX)
button = ttk.Button(frame, image=self.editdelete_img, state="disabled")
button.bind("<Button-1>", self.delete_filename)
button.grid(row=0, column=3)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
button = ttk.Radiobutton(
frame,
variable=self.radiovar,
text="Copy Source File",
value=1,
state=tk.DISABLED,
)
button.grid(row=0, column=0, sticky="w", padx=PADX)
entry = ttk.Entry(frame, state=tk.DISABLED)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.FILEOPEN, 16)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(0, weight=1)
button = ttk.Radiobutton(
frame,
variable=self.radiovar,
text="Use text below for file contents",
value=2,
)
button.grid(row=0, column=0, sticky="ew")
image = Images.get(ImageEnum.FILEOPEN, 16)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=1)
image = Images.get(ImageEnum.DOCUMENTSAVE, 16)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)
self.service_file_data = CodeText(tab)
self.service_file_data.grid(sticky="nsew")
tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1)
if len(self.filenames) > 0:
self.filename_combobox.current(0)
self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert(
"end", self.temp_service_files[self.filenames[0]]
)
self.service_file_data.text.bind(
"<FocusOut>", self.update_temp_service_file_data
)
def draw_tab_directories(self):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Directories")
label = ttk.Label(
tab,
text="Directories required by this service that are unique for each node.",
)
label.grid()
def draw_tab_startstop(self):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
for i in range(3):
tab.rowconfigure(i, weight=1)
self.notebook.add(tab, text="Startup/Shutdown")
# tab 3
for i in range(3):
label_frame = None
if i == 0:
label_frame = ttk.LabelFrame(
tab, text="Startup Commands", padding=FRAME_PAD
)
commands = self.startup_commands
elif i == 1:
label_frame = ttk.LabelFrame(
tab, text="Shutdown Commands", padding=FRAME_PAD
)
commands = self.shutdown_commands
elif i == 2:
label_frame = ttk.LabelFrame(
tab, text="Validation Commands", padding=FRAME_PAD
)
commands = self.validation_commands
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(1, weight=1)
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
frame = ttk.Frame(label_frame)
frame.grid(row=0, column=0, sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=tk.StringVar())
entry.grid(row=0, column=0, stick="ew", padx=PADX)
button = ttk.Button(frame, image=self.documentnew_img)
button.bind("<Button-1>", self.add_command)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, image=self.editdelete_img)
button.grid(row=0, column=2, sticky="ew")
button.bind("<Button-1>", self.delete_command)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.listbox.bind("<<ListboxSelect>>", self.update_entry)
for command in commands:
listbox_scroll.listbox.insert("end", command)
listbox_scroll.listbox.config(height=4)
listbox_scroll.grid(row=1, column=0, sticky="nsew")
if i == 0:
self.startup_commands_listbox = listbox_scroll.listbox
elif i == 1:
self.shutdown_commands_listbox = listbox_scroll.listbox
elif i == 2:
self.validate_commands_listbox = listbox_scroll.listbox
def draw_tab_configuration(self):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Configuration", sticky="nsew")
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Validation Time")
label.grid(row=0, column=0, sticky="w", padx=PADX)
self.validation_time_entry = ttk.Entry(frame)
self.validation_time_entry.insert("end", self.validation_time)
self.validation_time_entry.config(state=tk.DISABLED)
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
label = ttk.Label(frame, text="Validation Mode")
label.grid(row=1, column=0, sticky="w", padx=PADX)
if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING:
mode = "BLOCKING"
elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING:
mode = "NON_BLOCKING"
else:
mode = "TIMER"
self.validation_mode_entry = ttk.Entry(
frame, textvariable=tk.StringVar(value=mode)
)
self.validation_mode_entry.insert("end", mode)
self.validation_mode_entry.config(state=tk.DISABLED)
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
label = ttk.Label(frame, text="Validation Period")
label.grid(row=2, column=0, sticky="w", padx=PADX)
self.validation_period_entry = ttk.Entry(
frame, state=tk.DISABLED, textvariable=tk.StringVar()
)
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for executable in self.executables:
listbox_scroll.listbox.insert("end", executable)
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(
frame, text="Defaults", command=self.click_defaults, state="disabled"
)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(
frame, text="Copy...", command=self.click_copy, state="disabled"
)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
def add_filename(self, event):
# not worry about it for now
return
frame_contains_button = event.widget.master
combobox = frame_contains_button.grid_slaves(row=0, column=1)[0]
filename = combobox.get()
if filename not in combobox["values"]:
combobox["values"] += (filename,)
def delete_filename(self, event):
# not worry about it for now
return
frame_comntains_button = event.widget.master
combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0]
filename = combobox.get()
if filename in combobox["values"]:
combobox["values"] = tuple([x for x in combobox["values"] if x != filename])
combobox.set("")
def add_command(self, event):
frame_contains_button = event.widget.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
if command_to_add == "":
return
for cmd in listbox.get(0, tk.END):
if cmd == command_to_add:
return
listbox.insert(tk.END, command_to_add)
def update_entry(self, event):
listbox = event.widget
current_selection = listbox.curselection()
if len(current_selection) > 0:
cmd = listbox.get(current_selection[0])
entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves(
row=0, column=0
)[0]
entry.delete(0, "end")
entry.insert(0, cmd)
def delete_command(self, event):
button = event.widget
frame_contains_button = button.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
current_selection = listbox.curselection()
if len(current_selection) > 0:
listbox.delete(current_selection[0])
entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
entry.delete(0, tk.END)
def click_apply(self):
service_configs = self.app.core.service_configs
startup_commands = self.startup_commands_listbox.get(0, "end")
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
validate_commands = self.validate_commands_listbox.get(0, "end")
try:
config = self.core.set_node_service(
self.node_id,
self.service_name,
startup_commands,
validate_commands,
shutdown_commands,
)
if self.node_id not in service_configs:
service_configs[self.node_id] = {}
if self.service_name not in service_configs[self.node_id]:
self.app.core.service_configs[self.node_id][self.service_name] = config
for file in self.modified_files:
file_configs = self.app.core.file_configs
if self.node_id not in file_configs:
file_configs[self.node_id] = {}
if self.service_name not in file_configs[self.node_id]:
file_configs[self.node_id][self.service_name] = {}
file_configs[self.node_id][self.service_name][
file
] = self.temp_service_files[file]
self.app.core.set_node_service_file(
self.node_id, self.service_name, file, self.temp_service_files[file]
)
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
def display_service_file_data(self, event):
combobox = event.widget
filename = combobox.get()
self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert("end", self.temp_service_files[filename])
def update_temp_service_file_data(self, event):
scrolledtext = event.widget
filename = self.filename_combobox.get()
self.temp_service_files[filename] = scrolledtext.get(1.0, "end")
if self.temp_service_files[filename] != self.original_service_files[filename]:
self.modified_files.add(filename)
else:
self.modified_files.discard(filename)
def click_defaults(self):
logging.info("not implemented")
def click_copy(self):
logging.info("not implemented")
def click_cancel(self):
logging.info("not implemented")

View file

@ -0,0 +1,53 @@
import logging
from tkinter import ttk
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
class SessionOptionsDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Session Options", modal=True)
self.config_frame = None
self.config = self.get_config()
self.draw()
def get_config(self):
try:
session_id = self.app.core.session_id
response = self.app.core.client.get_session_options(session_id)
return response.config
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, config=self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.save)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, padx=PADX, sticky="ew")
def save(self):
config = self.config_frame.parse_config()
try:
session_id = self.app.core.session_id
response = self.app.core.client.set_session_options(session_id, config)
logging.info("saved session config: %s", response)
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()

View file

@ -0,0 +1,181 @@
import logging
import threading
import tkinter as tk
from tkinter import ttk
import grpc
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images
from core.gui.themes import PADX, PADY
class SessionsDialog(Dialog):
def __init__(self, master, app):
super().__init__(master, app, "Sessions", modal=True)
self.selected = False
self.selected_id = None
self.tree = None
self.sessions = self.get_sessions()
self.draw()
def get_sessions(self):
try:
response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response)
return response.sessions
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
self.draw_description()
self.draw_tree()
self.draw_buttons()
def draw_description(self):
"""
write a short description
:return: nothing
"""
label = ttk.Label(
self.top,
text="Below is a list of active CORE sessions. Double-click to \n"
"connect to an existing session. Usually, only sessions in \n"
"the RUNTIME state persist in the daemon, except for the \n"
"one you might be concurrently editting.",
justify=tk.CENTER,
)
label.grid(pady=PADY)
def draw_tree(self):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
self.tree = ttk.Treeview(
frame, columns=("id", "state", "nodes"), show="headings"
)
self.tree.grid(sticky="nsew")
self.tree.column("id", stretch=tk.YES)
self.tree.heading("id", text="ID")
self.tree.column("state", stretch=tk.YES)
self.tree.heading("state", text="State")
self.tree.column("nodes", stretch=tk.YES)
self.tree.heading("nodes", text="Node Count")
for index, session in enumerate(self.sessions):
state_name = core_pb2.SessionState.Enum.Name(session.state)
self.tree.insert(
"",
tk.END,
text=str(session.id),
values=(session.id, state_name, session.nodes),
)
self.tree.bind("<Double-1>", self.on_selected)
self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
self.tree.configure(xscrollcommand=xscrollbar.set)
def draw_buttons(self):
frame = ttk.Frame(self.top)
for i in range(4):
frame.columnconfigure(i, weight=1)
frame.grid(sticky="ew")
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
b = ttk.Button(
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
)
b.image = image
b.grid(row=0, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.FILEOPEN, 16)
b = ttk.Button(
frame,
image=image,
text="Connect",
compound=tk.LEFT,
command=self.click_connect,
)
b.image = image
b.grid(row=0, column=1, padx=PADX, sticky="ew")
image = Images.get(ImageEnum.EDITDELETE, 16)
b = ttk.Button(
frame,
image=image,
text="Shutdown",
compound=tk.LEFT,
command=self.click_shutdown,
)
b.image = image
b.grid(row=0, column=2, padx=PADX, sticky="ew")
b = ttk.Button(frame, text="Cancel", command=self.click_new)
b.grid(row=0, column=3, sticky="ew")
def click_new(self):
self.app.core.create_new_session()
self.destroy()
def click_select(self, event):
item = self.tree.selection()
session_id = int(self.tree.item(item, "text"))
self.selected = True
self.selected_id = session_id
def click_connect(self):
"""
if no session is selected yet, create a new one else join that session
:return: nothing
"""
if self.selected and self.selected_id is not None:
self.join_session(self.selected_id)
elif not self.selected and self.selected_id is None:
self.click_new()
else:
logging.error("sessions invalid state")
def click_shutdown(self):
"""
if no session is currently selected create a new session else shut the selected
session down.
:return: nothing
"""
if self.selected and self.selected_id is not None:
self.shutdown_session(self.selected_id)
elif not self.selected and self.selected_id is None:
self.click_new()
else:
logging.error("querysessiondrawing.py invalid state")
def join_session(self, session_id):
self.app.statusbar.progress_bar.start(5)
thread = threading.Thread(
target=self.app.core.join_session, args=([session_id])
)
thread.start()
self.destroy()
def on_selected(self, event):
item = self.tree.selection()
sid = int(self.tree.item(item, "text"))
self.join_session(sid)
def shutdown_session(self, sid):
self.app.core.stop_session(sid)
self.click_new()
self.destroy()

View file

@ -0,0 +1,252 @@
"""
shape input dialog
"""
import tkinter as tk
from tkinter import font, ttk
from core.gui.dialogs.colorpicker import ColorPicker
from core.gui.dialogs.dialog import Dialog
from core.gui.graph import tags
from core.gui.graph.shapeutils import is_draw_shape, is_shape_text
from core.gui.themes import FRAME_PAD, PADX, PADY
FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ShapeDialog(Dialog):
def __init__(self, master, app, shape):
if is_draw_shape(shape.shape_type):
title = "Add Shape"
else:
title = "Add Text"
super().__init__(master, app, title, modal=True)
self.canvas = app.canvas
self.fill = None
self.border = None
self.shape = shape
data = shape.shape_data
self.shape_text = tk.StringVar(value=data.text)
self.font = tk.StringVar(value=data.font)
self.font_size = tk.IntVar(value=data.font_size)
self.text_color = data.text_color
fill_color = data.fill_color
if not fill_color:
fill_color = "#CFCFFF"
self.fill_color = fill_color
self.border_color = data.border_color
self.border_width = tk.IntVar(value=0)
self.bold = tk.BooleanVar(value=data.bold)
self.italic = tk.BooleanVar(value=data.italic)
self.underline = tk.BooleanVar(value=data.underline)
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.draw_label_options()
if is_draw_shape(self.shape.shape_type):
self.draw_shape_options()
self.draw_spacer()
self.draw_buttons()
def draw_label_options(self):
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(label_frame, textvariable=self.shape_text)
entry.grid(sticky="ew", pady=PADY)
# font options
frame = ttk.Frame(label_frame)
frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
combobox = ttk.Combobox(
frame,
textvariable=self.font,
values=sorted(font.families()),
state="readonly",
)
combobox.grid(row=0, column=0, sticky="nsew")
combobox = ttk.Combobox(
frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly"
)
combobox.grid(row=0, column=1, padx=PADX, sticky="nsew")
button = ttk.Button(frame, text="Color", command=self.choose_text_color)
button.grid(row=0, column=2, sticky="nsew")
# style options
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Checkbutton(frame, variable=self.bold, text="Bold")
button.grid(row=0, column=0, sticky="ew")
button = ttk.Checkbutton(frame, variable=self.italic, text="Italic")
button.grid(row=0, column=1, padx=PADX, sticky="ew")
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
button.grid(row=0, column=2, sticky="ew")
def draw_shape_options(self):
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
label_frame.grid(sticky="ew", pady=PADY)
label_frame.columnconfigure(0, weight=1)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
for i in range(1, 3):
frame.columnconfigure(i, weight=1)
label = ttk.Label(frame, text="Fill Color")
label.grid(row=0, column=0, padx=PADX, sticky="w")
self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color)
self.fill.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Color", command=self.choose_fill_color)
button.grid(row=0, column=2, sticky="ew")
label = ttk.Label(frame, text="Border Color")
label.grid(row=1, column=0, sticky="w", padx=PADX)
self.border = ttk.Label(
frame, text=self.border_color, background=self.border_color
)
self.border.grid(row=1, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Color", command=self.choose_border_color)
button.grid(row=1, column=2, sticky="ew")
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Border Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
combobox = ttk.Combobox(
frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly"
)
combobox.grid(row=0, column=1, sticky="nsew")
def draw_buttons(self):
frame = ttk.Frame(self.top)
frame.grid(sticky="nsew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Add shape", command=self.click_add)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.cancel)
button.grid(row=0, column=1, sticky="ew")
def choose_text_color(self):
color_picker = ColorPicker(self, self.app, "#000000")
color = color_picker.askcolor()
self.text_color = color
def choose_fill_color(self):
color_picker = ColorPicker(self, self.app, self.fill_color)
color = color_picker.askcolor()
self.fill_color = color
self.fill.config(background=color, text=color)
def choose_border_color(self):
color_picker = ColorPicker(self, self.app, self.border_color)
color = color_picker.askcolor()
self.border_color = color
self.border.config(background=color, text=color)
def cancel(self):
self.shape.delete()
self.canvas.shapes.pop(self.shape.id)
self.destroy()
def click_add(self):
if is_draw_shape(self.shape.shape_type):
self.add_shape()
elif is_shape_text(self.shape.shape_type):
self.add_text()
self.destroy()
def make_font(self):
"""
create font for text or shape label
:return: list(font specifications)
"""
size = int(self.font_size.get())
text_font = [self.font.get(), size]
if self.bold.get():
text_font.append("bold")
if self.italic.get():
text_font.append("italic")
if self.underline.get():
text_font.append("underline")
return text_font
def save_text(self):
"""
save info related to text or shape label
:return: nothing
"""
data = self.shape.shape_data
data.text = self.shape_text.get()
data.font = self.font.get()
data.font_size = int(self.font_size.get())
data.text_color = self.text_color
data.bold = self.bold.get()
data.italic = self.italic.get()
data.underline = self.underline.get()
def save_shape(self):
"""
save info related to shape
:return: nothing
"""
data = self.shape.shape_data
data.fill_color = self.fill_color
data.border_color = self.border_color
data.border_width = int(self.border_width.get())
def add_text(self):
"""
add text to canvas
:return: nothing
"""
text = self.shape_text.get()
text_font = self.make_font()
self.canvas.itemconfig(
self.shape.id, text=text, fill=self.text_color, font=text_font
)
self.save_text()
def add_shape(self):
self.canvas.itemconfig(
self.shape.id,
fill=self.fill_color,
dash="",
outline=self.border_color,
width=int(self.border_width.get()),
)
shape_text = self.shape_text.get()
size = int(self.font_size.get())
x0, y0, x1, y1 = self.canvas.bbox(self.shape.id)
_y = y0 + 1.5 * size
_x = (x0 + x1) / 2
text_font = self.make_font()
if self.shape.text_id is None:
self.shape.text_id = self.canvas.create_text(
_x,
_y,
text=shape_text,
fill=self.text_color,
font=text_font,
tags=tags.SHAPE_TEXT,
)
self.shape.created = True
else:
self.canvas.itemconfig(
self.shape.text_id,
text=shape_text,
fill=self.text_color,
font=text_font,
)
self.save_text()
self.save_shape()

View file

@ -0,0 +1,66 @@
"""
wlan configuration
"""
from tkinter import ttk
import grpc
from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
class WlanConfigDialog(Dialog):
def __init__(self, master, app, canvas_node):
super().__init__(
master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True
)
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
try:
self.config = self.app.core.get_wlan_config(self.node.id)
except grpc.RpcError as e:
show_grpc_error(e)
self.destroy()
self.draw()
def draw(self):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_apply_buttons()
def draw_apply_buttons(self):
"""
create node configuration options
:return: nothing
"""
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
"""
retrieve user's wlan configuration and store the new configuration values
:return: nothing
"""
config = self.config_frame.parse_config()
self.app.core.wlan_configs[self.node.id] = self.config
if self.app.core.is_runtime():
session_id = self.app.core.session_id
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
self.destroy()

View file

@ -0,0 +1,8 @@
from tkinter import messagebox
def show_grpc_error(e):
title = [x.capitalize() for x in e.code().name.lower().split("_")]
title = " ".join(title)
title = f"GRPC {title}"
messagebox.showerror(title, e.details())

View file

View file

@ -0,0 +1,181 @@
import logging
import tkinter as tk
from tkinter.font import Font
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfiguration
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
TEXT_DISTANCE = 0.30
class CanvasWirelessEdge:
def __init__(self, token, position, src, dst, canvas):
self.token = token
self.src = src
self.dst = dst
self.canvas = canvas
self.id = self.canvas.create_line(
*position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933"
)
def delete(self):
self.canvas.delete(self.id)
class CanvasEdge:
"""
Canvas edge class
"""
width = 3
def __init__(self, x1, y1, x2, y2, src, canvas):
"""
Create an instance of canvas edge object
:param int x1: source x-coord
:param int y1: source y-coord
:param int x2: destination x-coord
:param int y2: destination y-coord
:param int src: source id
:param coretk.graph.graph.GraphCanvas canvas: canvas object
"""
self.src = src
self.dst = None
self.src_interface = None
self.dst_interface = None
self.canvas = canvas
self.id = self.canvas.create_line(
x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000"
)
self.text_src = None
self.text_dst = None
self.token = None
self.font = Font(size=8)
self.link = None
self.asymmetric_link = None
self.throughput = None
self.set_binding()
def set_binding(self):
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.create_context)
def set_link(self, link):
self.link = link
self.draw_labels()
def get_coordinates(self):
x1, y1, x2, y2 = self.canvas.coords(self.id)
v1 = x2 - x1
v2 = y2 - y1
ux = TEXT_DISTANCE * v1
uy = TEXT_DISTANCE * v2
x1 = x1 + ux
y1 = y1 + uy
x2 = x2 - ux
y2 = y2 - uy
return x1, y1, x2, y2
def draw_labels(self):
x1, y1, x2, y2 = self.get_coordinates()
label_one = None
if self.link.HasField("interface_one"):
label_one = (
f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n"
f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n"
)
label_two = None
if self.link.HasField("interface_two"):
label_two = (
f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n"
f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n"
)
self.text_src = self.canvas.create_text(
x1,
y1,
text=label_one,
justify=tk.CENTER,
font=self.font,
tags=tags.LINK_INFO,
)
self.text_dst = self.canvas.create_text(
x2,
y2,
text=label_two,
justify=tk.CENTER,
font=self.font,
tags=tags.LINK_INFO,
)
def update_labels(self):
"""
Move edge labels based on current position.
:return: nothing
"""
x1, y1, x2, y2 = self.get_coordinates()
self.canvas.coords(self.text_src, x1, y1)
self.canvas.coords(self.text_dst, x2, y2)
def complete(self, dst):
self.dst = dst
self.token = tuple(sorted((self.src, self.dst)))
x, y = self.canvas.coords(self.dst)
x1, y1, _, _ = self.canvas.coords(self.id)
self.canvas.coords(self.id, x1, y1, x, y)
self.check_wireless()
self.canvas.tag_raise(self.src)
self.canvas.tag_raise(self.dst)
def check_wireless(self):
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
if is_src_wireless or is_dst_wireless:
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self._check_antenna()
def _check_antenna(self):
src_node = self.canvas.nodes[self.src]
dst_node = self.canvas.nodes[self.dst]
src_node_type = src_node.core_node.type
dst_node_type = dst_node.core_node.type
is_src_wireless = NodeUtils.is_wireless_node(src_node_type)
is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type)
if is_src_wireless or is_dst_wireless:
if is_src_wireless and not is_dst_wireless:
dst_node.add_antenna()
elif not is_src_wireless and is_dst_wireless:
src_node.add_antenna()
# TODO: remove this? dont allow linking wireless nodes?
else:
src_node.add_antenna()
def delete(self):
self.canvas.delete(self.id)
if self.link:
self.canvas.delete(self.text_src)
self.canvas.delete(self.text_dst)
def create_context(self, event):
logging.debug("create link context")
context = tk.Menu(self.canvas)
themes.style_menu(context)
context.add_command(label="Configure", command=self.configure)
context.add_command(label="Delete")
context.add_command(label="Split")
context.add_command(label="Merge")
if self.canvas.app.core.is_runtime():
context.entryconfigure(1, state="disabled")
context.entryconfigure(2, state="disabled")
context.entryconfigure(3, state="disabled")
context.post(event.x_root, event.y_root)
def configure(self):
logging.debug("link configuration")
dialog = LinkConfiguration(self.canvas, self.canvas.app, self)
dialog.show()

View file

@ -0,0 +1,18 @@
import enum
class GraphMode(enum.Enum):
SELECT = 0
EDGE = 1
PICKNODE = 2
NODE = 3
ANNOTATION = 4
OTHER = 5
class ScaleOption(enum.Enum):
NONE = 0
UPPER_LEFT = 1
CENTERED = 2
SCALED = 3
TILED = 4

View file

@ -0,0 +1,839 @@
import logging
import tkinter as tk
from PIL import Image, ImageTk
from core.api.grpc import core_pb2
from core.gui import nodeutils
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.linkinfo import Throughput
from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import Images
from core.gui.nodeutils import NodeUtils
ZOOM_IN = 1.1
ZOOM_OUT = 0.9
class CanvasGraph(tk.Canvas):
def __init__(self, master, core, width, height):
super().__init__(master, highlightthickness=0, background="#cccccc")
self.app = master
self.core = core
self.mode = GraphMode.SELECT
self.annotation_type = None
self.selection = {}
self.select_box = None
self.selected = None
self.node_draw = None
self.context = None
self.nodes = {}
self.edges = {}
self.shapes = {}
self.wireless_edges = {}
self.drawing_edge = None
self.grid = None
self.throughput_draw = Throughput(self, core)
self.shape_drawing = False
self.default_dimensions = (width, height)
self.current_dimensions = self.default_dimensions
self.ratio = 1.0
self.offset = (0, 0)
self.cursor = (0, 0)
self.marker_tool = None
# background related
self.wallpaper_id = None
self.wallpaper = None
self.wallpaper_drawn = None
self.wallpaper_file = ""
self.scale_option = tk.IntVar(value=1)
self.show_grid = tk.BooleanVar(value=True)
self.adjust_to_dim = tk.BooleanVar(value=False)
# bindings
self.setup_bindings()
# draw base canvas
self.draw_canvas()
self.draw_grid()
def draw_canvas(self, dimensions=None):
if self.grid is not None:
self.delete(self.grid)
if not dimensions:
dimensions = self.default_dimensions
self.current_dimensions = dimensions
self.grid = self.create_rectangle(
0,
0,
*dimensions,
outline="#000000",
fill="#ffffff",
width=1,
tags="rectangle",
)
self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session):
"""
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
client.
:param core.api.grpc.core_pb2.Session session: session to draw
:return: nothing
"""
# hide context
self.hide_context()
# delete any existing drawn items
for tag in tags.COMPONENT_TAGS:
self.delete(tag)
# set the private variables to default value
self.mode = GraphMode.SELECT
self.annotation_type = None
self.node_draw = None
self.selected = None
self.nodes.clear()
self.edges.clear()
self.shapes.clear()
self.wireless_edges.clear()
self.drawing_edge = None
self.draw_session(session)
def setup_bindings(self):
"""
Bind any mouse events or hot keys to the matching action
:return: nothing
"""
self.bind("<ButtonPress-1>", self.click_press)
self.bind("<ButtonRelease-1>", self.click_release)
self.bind("<B1-Motion>", self.click_motion)
self.bind("<ButtonRelease-3>", self.click_context)
self.bind("<Delete>", self.press_delete)
self.bind("<Control-1>", self.ctrl_click)
self.bind("<Double-Button-1>", self.double_click)
self.bind("<MouseWheel>", self.zoom)
self.bind("<Button-4>", lambda e: self.zoom(e, ZOOM_IN))
self.bind("<Button-5>", lambda e: self.zoom(e, ZOOM_OUT))
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
def hide_context(self):
if self.context:
self.context.unpost()
self.context = None
def get_actual_coords(self, x, y):
actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / self.ratio
return actual_x, actual_y
def get_scaled_coords(self, x, y):
scaled_x = (x * self.ratio) + self.offset[0]
scaled_y = (y * self.ratio) + self.offset[1]
return scaled_x, scaled_y
def inside_canvas(self, x, y):
x1, y1, x2, y2 = self.bbox(self.grid)
valid_x = x1 <= x <= x2
valid_y = y1 <= y <= y2
return valid_x and valid_y
def valid_position(self, x1, y1, x2, y2):
valid_topleft = self.inside_canvas(x1, y1)
valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright
def draw_grid(self):
"""
Create grid.
:return: nothing
"""
width, height = self.width_and_height()
width = int(width)
height = int(height)
for i in range(0, width, 27):
self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE)
for i in range(0, height, 27):
self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE)
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.grid)
def add_wireless_edge(self, src, dst):
"""
add a wireless edge between 2 canvas nodes
:param CanvasNode src: source node
:param CanvasNode dst: destination node
:return: nothing
"""
token = tuple(sorted((src.id, dst.id)))
x1, y1 = self.coords(src.id)
x2, y2 = self.coords(dst.id)
position = (x1, y1, x2, y2)
edge = CanvasWirelessEdge(token, position, src.id, dst.id, self)
self.wireless_edges[token] = edge
src.wireless_edges.add(edge)
dst.wireless_edges.add(edge)
self.tag_raise(src.id)
self.tag_raise(dst.id)
def delete_wireless_edge(self, src, dst):
token = tuple(sorted((src.id, dst.id)))
edge = self.wireless_edges.pop(token)
edge.delete()
src.wireless_edges.remove(edge)
dst.wireless_edges.remove(edge)
def draw_session(self, session):
"""
Draw existing session.
:return: nothing
"""
# draw existing nodes
for core_node in session.nodes:
# peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type):
continue
# draw nodes on the canvas
logging.info("drawing core node: %s", core_node)
image = NodeUtils.node_icon(core_node.type, core_node.model)
if core_node.icon:
try:
image = Images.create(core_node.icon, nodeutils.ICON_SIZE)
except OSError:
logging.error("invalid icon: %s", core_node.icon)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.master, x, y, core_node, image)
self.nodes[node.id] = node
self.core.canvas_nodes[core_node.id] = node
# draw existing links
for link in session.links:
logging.info("drawing link: %s", link)
canvas_node_one = self.core.canvas_nodes[link.node_one_id]
node_one = canvas_node_one.core_node
canvas_node_two = self.core.canvas_nodes[link.node_two_id]
node_two = canvas_node_two.core_node
token = tuple(sorted((canvas_node_one.id, canvas_node_two.id)))
if link.type == core_pb2.LinkType.WIRELESS:
self.add_wireless_edge(canvas_node_one, canvas_node_two)
else:
if token not in self.edges:
edge = CanvasEdge(
node_one.position.x,
node_one.position.y,
node_two.position.x,
node_two.position.y,
canvas_node_one.id,
self,
)
edge.token = token
edge.dst = canvas_node_two.id
edge.set_link(link)
edge.check_wireless()
canvas_node_one.edges.add(edge)
canvas_node_two.edges.add(edge)
self.edges[edge.token] = edge
self.core.links[edge.token] = edge
if link.HasField("interface_one"):
canvas_node_one.interfaces.append(link.interface_one)
if link.HasField("interface_two"):
canvas_node_two.interfaces.append(link.interface_two)
elif link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
else:
logging.error("duplicate link received: %s", link)
# raise the nodes so they on top of the links
self.tag_raise(tags.NODE)
def canvas_xy(self, event):
"""
Convert window coordinate to canvas coordinate
:param event:
:rtype: (int, int)
:return: x, y canvas coordinate
"""
x = self.canvasx(event.x)
y = self.canvasy(event.y)
return x, y
def get_selected(self, event):
"""
Retrieve the item id that is on the mouse position
:param event: mouse event
:rtype: int
:return: the item that the mouse point to
"""
x, y = self.canvas_xy(event)
overlapping = self.find_overlapping(x, y, x, y)
selected = None
for _id in overlapping:
if self.drawing_edge and self.drawing_edge.id == _id:
continue
if _id in self.nodes:
selected = _id
break
if _id in self.shapes:
selected = _id
return selected
def click_release(self, event):
"""
Draw a node or finish drawing an edge according to the current graph mode
:param event: mouse event
:return: nothing
"""
logging.debug("click release")
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
if self.context:
self.hide_context()
else:
if self.mode == GraphMode.ANNOTATION:
self.focus_set()
if self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_complete(x, y)
self.shape_drawing = False
elif self.mode == GraphMode.SELECT:
self.focus_set()
if self.select_box:
x0, y0, x1, y1 = self.coords(self.select_box.id)
inside = [
x
for x in self.find_enclosed(x0, y0, x1, y1)
if "node" in self.gettags(x) or "shape" in self.gettags(x)
]
for i in inside:
self.select_object(i, True)
self.select_box.disappear()
self.select_box = None
else:
self.focus_set()
self.selected = self.get_selected(event)
logging.debug(
f"click release selected({self.selected}) mode({self.mode})"
)
if self.mode == GraphMode.EDGE:
self.handle_edge_release(event)
elif self.mode == GraphMode.NODE:
self.add_node(x, y)
elif self.mode == GraphMode.PICKNODE:
self.mode = GraphMode.NODE
self.selected = None
def handle_edge_release(self, event):
edge = self.drawing_edge
self.drawing_edge = None
# not drawing edge return
if edge is None:
return
# edge dst must be a node
logging.debug(f"current selected: {self.selected}")
dst_node = self.nodes.get(self.selected)
if not dst_node:
edge.delete()
return
# edge dst is same as src, delete edge
if edge.src == self.selected:
edge.delete()
return
# ignore repeated edges
token = tuple(sorted((edge.src, self.selected)))
if token in self.edges:
edge.delete()
return
# set dst node and snap edge to center
edge.complete(self.selected)
logging.debug("drawing edge token: %s", edge.token)
self.edges[edge.token] = edge
node_src = self.nodes[edge.src]
node_src.edges.add(edge)
node_dst = self.nodes[edge.dst]
node_dst.edges.add(edge)
self.core.create_link(edge, node_src, node_dst)
def select_object(self, object_id, choose_multiple=False):
"""
create a bounding box when a node is selected
"""
if not choose_multiple:
self.clear_selection()
# draw a bounding box if node hasn't been selected yet
if object_id not in self.selection:
x0, y0, x1, y1 = self.bbox(object_id)
selection_id = self.create_rectangle(
(x0 - 6, y0 - 6, x1 + 6, y1 + 6),
activedash=True,
dash="-",
tags=tags.SELECTION,
)
self.selection[object_id] = selection_id
else:
selection_id = self.selection.pop(object_id)
self.delete(selection_id)
def clear_selection(self):
"""
Clear current selection boxes.
:return: nothing
"""
for _id in self.selection.values():
self.delete(_id)
self.selection.clear()
def move_selection(self, object_id, x_offset, y_offset):
select_id = self.selection.get(object_id)
if select_id is not None:
self.move(select_id, x_offset, y_offset)
def delete_selection_objects(self):
edges = set()
nodes = []
for object_id in self.selection:
# delete selection box
selection_id = self.selection[object_id]
self.delete(selection_id)
# delete node and related edges
if object_id in self.nodes:
canvas_node = self.nodes.pop(object_id)
canvas_node.delete()
nodes.append(canvas_node)
is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type)
# delete related edges
for edge in canvas_node.edges:
if edge in edges:
continue
edges.add(edge)
self.throughput_draw.delete(edge)
del self.edges[edge.token]
edge.delete()
# update node connected to edge being deleted
other_id = edge.src
other_interface = edge.src_interface
if edge.src == object_id:
other_id = edge.dst
other_interface = edge.dst_interface
other_node = self.nodes[other_id]
other_node.edges.remove(edge)
try:
other_node.interfaces.remove(other_interface)
except ValueError:
pass
if is_wireless:
other_node.delete_antenna()
# delete shape
if object_id in self.shapes:
shape = self.shapes.pop(object_id)
shape.delete()
self.selection.clear()
return nodes
def zoom(self, event, factor=None):
if not factor:
factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
self.scale(tk.ALL, event.x, event.y, factor, factor)
self.configure(scrollregion=self.bbox(tk.ALL))
self.ratio *= float(factor)
self.offset = (
self.offset[0] * factor + event.x * (1 - factor),
self.offset[1] * factor + event.y * (1 - factor),
)
logging.info("ratio: %s", self.ratio)
logging.info("offset: %s", self.offset)
self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%")
if self.wallpaper:
self.redraw_wallpaper()
def click_press(self, event):
"""
Start drawing an edge if mouse click is on a node
:param event: mouse event
:return: nothing
"""
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
self.cursor = x, y
selected = self.get_selected(event)
logging.debug("click press(%s): %s", self.cursor, selected)
x_check = self.cursor[0] - self.offset[0]
y_check = self.cursor[1] - self.offset[1]
logging.debug("clock press ofset(%s, %s)", x_check, y_check)
is_node = selected in self.nodes
if self.mode == GraphMode.EDGE and is_node:
x, y = self.coords(selected)
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
if self.mode == GraphMode.ANNOTATION:
if is_marker(self.annotation_type):
r = self.app.toolbar.marker_tool.radius
self.create_oval(
x - r,
y - r,
x + r,
y + r,
fill=self.app.toolbar.marker_tool.color,
outline="",
tags="marker",
)
return
if selected is None:
shape = Shape(self.app, self, self.annotation_type, x, y)
self.selected = shape.id
self.shape_drawing = True
self.shapes[shape.id] = shape
if selected is not None:
if selected not in self.selection:
if selected in self.shapes:
shape = self.shapes[selected]
self.select_object(shape.id)
self.selected = selected
elif selected in self.nodes:
node = self.nodes[selected]
self.select_object(node.id)
self.selected = selected
logging.info(
"selected coords: (%s, %s)",
node.core_node.position.x,
node.core_node.position.y,
)
else:
logging.debug("create selection box")
if self.mode == GraphMode.SELECT:
shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y)
self.select_box = shape
self.clear_selection()
def ctrl_click(self, event):
# update cursor location
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
return
self.cursor = x, y
# handle multiple selections
logging.debug("control left click: %s", event)
selected = self.get_selected(event)
if (
selected not in self.selection
and selected in self.shapes
or selected in self.nodes
):
self.select_object(selected, choose_multiple=True)
def click_motion(self, event):
"""
Redraw drawing edge according to the current position of the mouse
:param event: mouse event
:return: nothing
"""
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
if self.select_box:
self.select_box.delete()
self.select_box = None
if is_draw_shape(self.annotation_type) and self.shape_drawing:
shape = self.shapes.pop(self.selected)
shape.delete()
self.shape_drawing = False
return
x_offset = x - self.cursor[0]
y_offset = y - self.cursor[1]
self.cursor = x, y
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
x1, y1, _, _ = self.coords(self.drawing_edge.id)
self.coords(self.drawing_edge.id, x1, y1, x, y)
if self.mode == GraphMode.ANNOTATION:
if is_draw_shape(self.annotation_type) and self.shape_drawing:
shape = self.shapes[self.selected]
shape.shape_motion(x, y)
elif is_marker(self.annotation_type):
r = self.app.toolbar.marker_tool.radius
self.create_oval(
x - r,
y - r,
x + r,
y + r,
fill=self.app.toolbar.marker_tool.color,
outline="",
tags="marker",
)
return
if self.mode == GraphMode.EDGE:
return
# move selected objects
if self.selection:
for selected_id in self.selection:
if selected_id in self.shapes:
shape = self.shapes[selected_id]
shape.motion(x_offset, y_offset)
if selected_id in self.nodes:
node = self.nodes[selected_id]
node.motion(x_offset, y_offset, update=self.core.is_runtime())
else:
if self.select_box and self.mode == GraphMode.SELECT:
self.select_box.shape_motion(x, y)
def click_context(self, event):
logging.info("context event: %s", self.context)
if not self.context:
selected = self.get_selected(event)
canvas_node = self.nodes.get(selected)
if canvas_node:
logging.debug(f"node context: {selected}")
self.context = canvas_node.create_context()
self.context.post(event.x_root, event.y_root)
else:
self.hide_context()
def press_delete(self, event):
"""
delete selected nodes and any data that relates to it
:param event:
:return:
"""
logging.debug("press delete key")
nodes = self.delete_selection_objects()
self.core.delete_graph_nodes(nodes)
def double_click(self, event):
selected = self.get_selected(event)
if selected is not None and selected in self.shapes:
shape = self.shapes[selected]
dialog = ShapeDialog(self.app, self.app, shape)
dialog.show()
def add_node(self, x, y):
if self.selected is None or self.selected in self.shapes:
actual_x, actual_y = self.get_actual_coords(x, y)
core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
)
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node
return node
def width_and_height(self):
"""
retrieve canvas width and height in pixels
:return: nothing
"""
x0, y0, x1, y1 = self.coords(self.grid)
canvas_w = abs(x0 - x1)
canvas_h = abs(y0 - y1)
return canvas_w, canvas_h
def get_wallpaper_image(self):
width = int(self.wallpaper.width * self.ratio)
height = int(self.wallpaper.height * self.ratio)
image = self.wallpaper.resize((width, height), Image.ANTIALIAS)
return image
def draw_wallpaper(self, image, x=None, y=None):
if x is None and y is None:
x1, y1, x2, y2 = self.bbox(self.grid)
x = (x1 + x2) / 2
y = (y1 + y2) / 2
self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER)
self.wallpaper_drawn = image
def wallpaper_upper_left(self):
self.delete(self.wallpaper_id)
# create new scaled image, cropped if needed
width, height = self.width_and_height()
image = self.get_wallpaper_image()
cropx = image.width
cropy = image.height
if image.width > width:
cropx = image.width
if image.height > height:
cropy = image.height
cropped = image.crop((0, 0, cropx, cropy))
image = ImageTk.PhotoImage(cropped)
# draw on canvas
x1, y1, _, _ = self.bbox(self.grid)
x = (cropx / 2) + x1
y = (cropy / 2) + y1
self.draw_wallpaper(image, x, y)
def wallpaper_center(self):
"""
place the image at the center of canvas
:return: nothing
"""
self.delete(self.wallpaper_id)
# dimension of the cropped image
width, height = self.width_and_height()
image = self.get_wallpaper_image()
cropx = 0
if image.width > width:
cropx = (image.width - width) / 2
cropy = 0
if image.height > height:
cropy = (image.height - height) / 2
x1 = 0 + cropx
y1 = 0 + cropy
x2 = image.width - cropx
y2 = image.height - cropy
cropped = image.crop((x1, y1, x2, y2))
image = ImageTk.PhotoImage(cropped)
self.draw_wallpaper(image)
def wallpaper_scaled(self):
"""
scale image based on canvas dimension
:return: nothing
"""
self.delete(self.wallpaper_id)
canvas_w, canvas_h = self.width_and_height()
image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS)
image = ImageTk.PhotoImage(image)
self.draw_wallpaper(image)
def resize_to_wallpaper(self):
self.delete(self.wallpaper_id)
image = ImageTk.PhotoImage(self.wallpaper)
self.redraw_canvas((image.width(), image.height()))
self.draw_wallpaper(image)
def redraw_canvas(self, dimensions=None):
logging.info("redrawing canvas to dimensions: %s", dimensions)
# reset scale and move back to original position
logging.info("resetting scaling: %s %s", self.ratio, self.offset)
factor = 1 / self.ratio
self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor)
self.move(tk.ALL, -self.offset[0], -self.offset[1])
# reset ratio and offset
self.ratio = 1.0
self.offset = (0, 0)
# redraw canvas rectangle
self.draw_canvas(dimensions)
# redraw gridlines to new canvas size
self.delete(tags.GRIDLINE)
self.draw_grid()
self.update_grid()
def redraw_wallpaper(self):
if self.adjust_to_dim.get():
logging.info("drawing wallpaper to canvas dimensions")
self.resize_to_wallpaper()
else:
option = ScaleOption(self.scale_option.get())
logging.info("drawing canvas using scaling option: %s", option)
if option == ScaleOption.UPPER_LEFT:
self.wallpaper_upper_left()
elif option == ScaleOption.CENTERED:
self.wallpaper_center()
elif option == ScaleOption.SCALED:
self.wallpaper_scaled()
elif option == ScaleOption.TILED:
logging.warning("tiled background not implemented yet")
# raise items above wallpaper
for component in tags.ABOVE_WALLPAPER_TAGS:
self.tag_raise(component)
def update_grid(self):
logging.info("updating grid show: %s", self.show_grid.get())
if self.show_grid.get():
self.itemconfig(tags.GRIDLINE, state=tk.NORMAL)
else:
self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN)
def set_wallpaper(self, filename):
logging.info("setting wallpaper: %s", filename)
if filename:
img = Image.open(filename)
self.wallpaper = img
self.wallpaper_file = filename
self.redraw_wallpaper()
else:
if self.wallpaper_id is not None:
self.delete(self.wallpaper_id)
self.wallpaper = None
self.wallpaper_file = None
def is_selection_mode(self):
return self.mode == GraphMode.SELECT
def create_edge(self, source, dest):
"""
create an edge between source node and destination node
:param CanvasNode source: source node
:param CanvasNode dest: destination node
:return: nothing
"""
if (source.id, dest.id) not in self.edges:
pos0 = source.core_node.position
x0 = pos0.x
y0 = pos0.y
edge = CanvasEdge(x0, y0, x0, y0, source.id, self)
edge.complete(dest.id)
self.edges[edge.token] = edge
self.nodes[source.id].edges.add(edge)
self.nodes[dest.id].edges.add(edge)
self.core.create_link(edge, source, dest)

View file

@ -0,0 +1,152 @@
"""
Link information, such as IPv4, IPv6 and throughput drawn in the canvas
"""
from core.api.grpc import core_pb2
class Throughput:
def __init__(self, canvas, core):
self.canvas = canvas
self.core = core
# edge canvas id mapped to throughput value
self.tracker = {}
# map an edge canvas id to a throughput canvas id
self.map = {}
# map edge canvas id to token
self.edge_id_to_token = {}
def load_throughput_info(self, interface_throughputs):
"""
load all interface throughouts from an event
:param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface
throughputs
:return: nothing
"""
for throughput in interface_throughputs:
nid = throughput.node_id
iid = throughput.interface_id
tp = throughput.throughput
token = self.core.interface_to_edge.get((nid, iid))
if token:
edge = self.canvas.edges.get(token)
if edge:
edge_id = edge.id
self.edge_id_to_token[edge_id] = token
if edge_id not in self.tracker:
self.tracker[edge_id] = tp
else:
temp = self.tracker[edge_id]
self.tracker[edge_id] = (temp + tp) / 2
else:
self.core.interface_to_edge.pop((nid, iid), None)
def edge_is_wired(self, token):
"""
determine whether link is a WIRED link
:param token:
:return:
"""
canvas_edge = self.canvas.edges[token]
canvas_src_id = canvas_edge.src
canvas_dst_id = canvas_edge.dst
src = self.canvas.nodes[canvas_src_id].core_node
dst = self.canvas.nodes[canvas_dst_id].core_node
return not (
src.type == core_pb2.NodeType.WIRELESS_LAN
and dst.model == "mdr"
or src.model == "mdr"
and dst.type == core_pb2.NodeType.WIRELESS_LAN
)
def draw_wired_throughput(self, edge_id):
x0, y0, x1, y1 = self.canvas.coords(edge_id)
x = (x0 + x1) / 2
y = (y0 + y1) / 2
if edge_id not in self.map:
tpid = self.canvas.create_text(
x,
y,
tags="throughput",
font=("Arial", 8),
text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]),
)
self.map[edge_id] = tpid
else:
tpid = self.map[edge_id]
self.canvas.coords(tpid, x, y)
self.canvas.itemconfig(
tpid, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id])
)
def draw_wireless_throughput(self, edge_id):
token = self.edge_id_to_token[edge_id]
canvas_edge = self.canvas.edges[token]
canvas_src_id = canvas_edge.src
canvas_dst_id = canvas_edge.dst
src_node = self.canvas.nodes[canvas_src_id]
dst_node = self.canvas.nodes[canvas_dst_id]
not_wlan = (
dst_node
if src_node.core_node.type == core_pb2.NodeType.WIRELESS_LAN
else src_node
)
x, y = self.canvas.coords(not_wlan.id)
if edge_id not in self.map:
tp_id = self.canvas.create_text(
x + 50,
y + 25,
font=("Arial", 8),
tags="throughput",
text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]),
)
self.map[edge_id] = tp_id
# redraw throughput
else:
self.canvas.itemconfig(
self.map[edge_id],
text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]),
)
def draw_throughputs(self):
for edge_id in self.tracker:
if self.edge_is_wired(self.edge_id_to_token[edge_id]):
self.draw_wired_throughput(edge_id)
else:
self.draw_wireless_throughput(edge_id)
def process_grpc_throughput_event(self, interface_throughputs):
self.load_throughput_info(interface_throughputs)
self.draw_throughputs()
def move(self, edge):
tpid = self.map.get(edge.id)
if tpid:
if self.edge_is_wired(edge.token):
x0, y0, x1, y1 = self.canvas.coords(edge.id)
self.canvas.coords(tpid, (x0 + x1) / 2, (y0 + y1) / 2)
else:
if (
self.canvas.nodes[edge.src].core_node.type
== core_pb2.NodeType.WIRELESS_LAN
):
x, y = self.canvas.coords(edge.dst)
self.canvas.coords(tpid, x + 50, y + 20)
else:
x, y = self.canvas.coords(edge.src)
self.canvas.coords(tpid, x + 50, y + 25)
def delete(self, edge):
tpid = self.map.get(edge.id)
if tpid:
eid = edge.id
self.canvas.delete(tpid)
self.tracker.pop(eid)
self.map.pop(eid)
self.edge_id_to_token.pop(eid)

View file

@ -0,0 +1,274 @@
import tkinter as tk
from tkinter import font
import grpc
from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import NodeType
from core.gui import themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog
from core.gui.dialogs.nodeservice import NodeService
from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.errors import show_grpc_error
from core.gui.graph import tags
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.nodeutils import NodeUtils
NODE_TEXT_OFFSET = 5
class CanvasNode:
def __init__(self, app, x, y, core_node, image):
self.app = app
self.canvas = app.canvas
self.image = image
self.core_node = core_node
self.id = self.canvas.create_image(
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()
self.text_id = self.canvas.create_text(
x,
label_y,
text=self.core_node.name,
tags=tags.NODE_NAME,
font=text_font,
fill="#0000CD",
)
self.tooltip = CanvasTooltip(self.canvas)
self.edges = set()
self.interfaces = []
self.wireless_edges = set()
self.antennae = []
self.setup_bindings()
def setup_bindings(self):
self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click)
self.canvas.tag_bind(self.id, "<Enter>", self.on_enter)
self.canvas.tag_bind(self.id, "<Leave>", self.on_leave)
def delete(self):
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)
self.delete_antennae()
def add_antenna(self):
x, y = self.canvas.coords(self.id)
offset = len(self.antennae) * 8
antenna_id = self.canvas.create_image(
x - 16 + offset,
y - 23,
anchor=tk.CENTER,
image=NodeUtils.ANTENNA_ICON,
tags=tags.ANTENNA,
)
self.antennae.append(antenna_id)
def delete_antenna(self):
"""
delete one antenna
:return: nothing
"""
if self.antennae:
antenna_id = self.antennae.pop()
self.canvas.delete(antenna_id)
def delete_antennae(self):
"""
delete all antennas
:return: nothing
"""
for antenna_id in self.antennae:
self.canvas.delete(antenna_id)
self.antennae.clear()
def redraw(self):
self.canvas.itemconfig(self.id, image=self.image)
self.canvas.itemconfig(self.text_id, text=self.core_node.name)
def _get_label_y(self):
image_box = self.canvas.bbox(self.id)
return image_box[3] + NODE_TEXT_OFFSET
def move(self, x, y):
x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id)
x_offset = x - current_x
y_offset = y - current_y
self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset, y_offset, update=True):
original_position = self.canvas.coords(self.id)
self.canvas.move(self.id, x_offset, y_offset)
x, y = self.canvas.coords(self.id)
# check new position
bbox = self.canvas.bbox(self.id)
if not self.canvas.valid_position(*bbox):
self.canvas.coords(self.id, original_position)
return
# move test and selection box
self.canvas.move(self.text_id, x_offset, y_offset)
self.canvas.move_selection(self.id, x_offset, y_offset)
# move antennae
for antenna_id in self.antennae:
self.canvas.move(antenna_id, x_offset, y_offset)
# move edges
for edge in self.edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id:
self.canvas.coords(edge.id, x, y, x2, y2)
else:
self.canvas.coords(edge.id, x1, y1, x, y)
self.canvas.throughput_draw.move(edge)
edge.update_labels()
for edge in self.wireless_edges:
x1, y1, x2, y2 = self.canvas.coords(edge.id)
if edge.src == self.id:
self.canvas.coords(edge.id, x, y, x2, y2)
else:
self.canvas.coords(edge.id, x1, y1, x, y)
# set actual coords for node and update core is running
real_x, real_y = self.canvas.get_actual_coords(x, y)
self.core_node.position.x = real_x
self.core_node.position.y = real_y
if self.app.core.is_runtime() and update:
self.app.core.edit_node(self.core_node)
def on_enter(self, event):
if self.app.core.is_runtime() and self.app.core.observer:
self.tooltip.text.set("waiting...")
self.tooltip.on_enter(event)
try:
output = self.app.core.run(self.core_node.id)
self.tooltip.text.set(output)
except grpc.RpcError as e:
show_grpc_error(e)
def on_leave(self, event):
self.tooltip.on_leave(event)
def double_click(self, event):
if self.app.core.is_runtime():
self.canvas.core.launch_terminal(self.core_node.id)
else:
self.show_config()
def create_context(self):
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_emane = self.core_node.type == NodeType.EMANE
context = tk.Menu(self.canvas)
themes.style_menu(context)
if self.app.core.is_runtime():
context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Services", state=tk.DISABLED)
if is_wlan:
context.add_command(label="WLAN Config", command=self.show_wlan_config)
if is_wlan and self.core_node.id in self.app.core.mobility_players:
context.add_command(
label="Mobility Player", command=self.show_mobility_player
)
context.add_command(label="Select Adjacent", state=tk.DISABLED)
context.add_command(label="Hide", state=tk.DISABLED)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Shell Window", state=tk.DISABLED)
context.add_command(label="Tcpdump", state=tk.DISABLED)
context.add_command(label="Tshark", state=tk.DISABLED)
context.add_command(label="Wireshark", state=tk.DISABLED)
context.add_command(label="View Log", state=tk.DISABLED)
else:
context.add_command(label="Configure", command=self.show_config)
if NodeUtils.is_container_node(self.core_node.type):
context.add_command(label="Services", command=self.show_services)
if is_emane:
context.add_command(
label="EMANE Config", command=self.show_emane_config
)
if is_wlan:
context.add_command(label="WLAN Config", command=self.show_wlan_config)
context.add_command(
label="Mobility Config", command=self.show_mobility_config
)
if NodeUtils.is_wireless_node(self.core_node.type):
context.add_command(
label="Link To Selected", command=self.wireless_link_selected
)
context.add_command(label="Select Members", state=tk.DISABLED)
context.add_command(label="Select Adjacent", state=tk.DISABLED)
context.add_command(label="Create Link To", state=tk.DISABLED)
context.add_command(label="Assign To", state=tk.DISABLED)
context.add_command(label="Move To", state=tk.DISABLED)
context.add_command(label="Cut", state=tk.DISABLED)
context.add_command(label="Copy", state=tk.DISABLED)
context.add_command(label="Paste", state=tk.DISABLED)
context.add_command(label="Delete", state=tk.DISABLED)
context.add_command(label="Hide", state=tk.DISABLED)
return context
def show_config(self):
self.canvas.context = None
dialog = NodeConfigDialog(self.app, self.app, self)
dialog.show()
def show_wlan_config(self):
self.canvas.context = None
dialog = WlanConfigDialog(self.app, self.app, self)
dialog.show()
def show_mobility_config(self):
self.canvas.context = None
dialog = MobilityConfigDialog(self.app, self.app, self)
dialog.show()
def show_mobility_player(self):
self.canvas.context = None
mobility_player = self.app.core.mobility_players[self.core_node.id]
mobility_player.show()
def show_emane_config(self):
self.canvas.context = None
dialog = EmaneConfigDialog(self.app, self.app, self)
dialog.show()
def show_services(self):
self.canvas.context = None
dialog = NodeService(self.app.master, self.app, self)
dialog.show()
def has_emane_link(self, interface_id):
result = None
for edge in self.edges:
if self.id == edge.src:
other_id = edge.dst
edge_interface_id = edge.src_interface.id
else:
other_id = edge.src
edge_interface_id = edge.dst_interface.id
if edge_interface_id != interface_id:
continue
other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE:
result = other_node.core_node
break
return result
def wireless_link_selected(self):
self.canvas.context = None
for canvas_nid in [
x for x in self.canvas.selection if "node" in self.canvas.gettags(x)
]:
core_node = self.canvas.nodes[canvas_nid].core_node
if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr":
self.canvas.create_edge(self, self.canvas.nodes[canvas_nid])
self.canvas.clear_selection()

View file

@ -0,0 +1,179 @@
import logging
from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags
from core.gui.graph.shapeutils import ShapeType
class AnnotationData:
def __init__(
self,
text="",
font="Arial",
font_size=12,
text_color="#000000",
fill_color="",
border_color="#000000",
border_width=1,
bold=False,
italic=False,
underline=False,
):
self.text = text
self.font = font
self.font_size = font_size
self.text_color = text_color
self.fill_color = fill_color
self.border_color = border_color
self.border_width = border_width
self.bold = bold
self.italic = italic
self.underline = underline
class Shape:
def __init__(self, app, canvas, shape_type, x1, y1, x2=None, y2=None, data=None):
self.app = app
self.canvas = canvas
self.shape_type = shape_type
self.id = None
self.text_id = None
self.x1 = x1
self.y1 = y1
if x2 is None:
x2 = x1
self.x2 = x2
if y2 is None:
y2 = y1
self.y2 = y2
if data is None:
self.created = False
self.shape_data = AnnotationData()
else:
self.created = True
self.shape_data = data
self.draw()
def draw(self):
if self.created:
dash = None
else:
dash = "-"
if self.shape_type == ShapeType.OVAL:
self.id = self.canvas.create_oval(
self.x1,
self.y1,
self.x2,
self.y2,
tags=tags.SHAPE,
dash=dash,
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
)
self.draw_shape_text()
elif self.shape_type == ShapeType.RECTANGLE:
self.id = self.canvas.create_rectangle(
self.x1,
self.y1,
self.x2,
self.y2,
tags=tags.SHAPE,
dash=dash,
fill=self.shape_data.fill_color,
outline=self.shape_data.border_color,
width=self.shape_data.border_width,
)
self.draw_shape_text()
elif self.shape_type == ShapeType.TEXT:
font = self.get_font()
self.id = self.canvas.create_text(
self.x1,
self.y1,
tags=tags.SHAPE_TEXT,
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
)
else:
logging.error("unknown shape type: %s", self.shape_type)
self.created = True
def get_font(self):
font = [self.shape_data.font, self.shape_data.font_size]
if self.shape_data.bold:
font.append("bold")
if self.shape_data.italic:
font.append("italic")
if self.shape_data.underline:
font.append("underline")
return font
def draw_shape_text(self):
if self.shape_data.text:
x = (self.x1 + self.x2) / 2
y = self.y1 + 1.5 * self.shape_data.font_size
font = self.get_font()
self.text_id = self.canvas.create_text(
x,
y,
tags=tags.SHAPE_TEXT,
text=self.shape_data.text,
fill=self.shape_data.text_color,
font=font,
)
def shape_motion(self, x1, y1):
self.canvas.coords(self.id, self.x1, self.y1, x1, y1)
def shape_complete(self, x, y):
for component in tags.ABOVE_SHAPE:
self.canvas.tag_raise(component)
s = ShapeDialog(self.app, self.app, self)
s.show()
def disappear(self):
self.canvas.delete(self.id)
def motion(self, x_offset, y_offset):
original_position = self.canvas.coords(self.id)
self.canvas.move(self.id, x_offset, y_offset)
coords = self.canvas.coords(self.id)
if not self.canvas.valid_position(*coords):
self.canvas.coords(self.id, original_position)
return
self.canvas.move_selection(self.id, x_offset, y_offset)
if self.text_id is not None:
self.canvas.move(self.text_id, x_offset, y_offset)
def delete(self):
self.canvas.delete(self.id)
self.canvas.delete(self.text_id)
def metadata(self):
coords = self.canvas.coords(self.id)
# update coords to actual positions
if len(coords) == 4:
x1, y1, x2, y2 = coords
x1, y1 = self.canvas.get_actual_coords(x1, y1)
x2, y2 = self.canvas.get_actual_coords(x2, y2)
coords = (x1, y1, x2, y2)
else:
x1, y1 = coords
x1, y1 = self.canvas.get_actual_coords(x1, y1)
coords = (x1, y1)
return {
"type": self.shape_type.value,
"iconcoords": coords,
"label": self.shape_data.text,
"fontfamily": self.shape_data.font,
"fontsize": self.shape_data.font_size,
"labelcolor": self.shape_data.text_color,
"color": self.shape_data.fill_color,
"border": self.shape_data.border_color,
"width": self.shape_data.border_width,
"bold": self.shape_data.bold,
"italic": self.shape_data.italic,
"underline": self.shape_data.underline,
}

Some files were not shown because too many files have changed in this diff Show more