moved coretk under daemon/core/gui
0
daemon/core/gui/__init__.py
Normal file
107
daemon/core/gui/app.py
Normal 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()
|
111
daemon/core/gui/appconfig.py
Normal 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)
|
909
daemon/core/gui/coreclient.py
Normal 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
|
BIN
daemon/core/gui/data/backgrounds/sample1-bg.gif
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
daemon/core/gui/data/backgrounds/sample4-bg.jpg
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
daemon/core/gui/data/icons/OVS.gif
Executable file
After Width: | Height: | Size: 744 B |
BIN
daemon/core/gui/data/icons/alert.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/antenna.gif
Normal file
After Width: | Height: | Size: 230 B |
BIN
daemon/core/gui/data/icons/core-icon.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
daemon/core/gui/data/icons/docker.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/document-new.gif
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/document-properties.gif
Normal file
After Width: | Height: | Size: 635 B |
BIN
daemon/core/gui/data/icons/document-save.gif
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/edit-delete.gif
Normal file
After Width: | Height: | Size: 1,006 B |
BIN
daemon/core/gui/data/icons/edit-node.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
daemon/core/gui/data/icons/emane.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
daemon/core/gui/data/icons/fileopen.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/host.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
daemon/core/gui/data/icons/hub.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
daemon/core/gui/data/icons/lanswitch.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/link.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
daemon/core/gui/data/icons/lxc.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
daemon/core/gui/data/icons/marker.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/icons/markerclear.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
daemon/core/gui/data/icons/mdr.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
daemon/core/gui/data/icons/observe.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/oval.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
daemon/core/gui/data/icons/pause.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
daemon/core/gui/data/icons/pc.png
Normal file
After Width: | Height: | Size: 828 B |
BIN
daemon/core/gui/data/icons/plot.gif
Normal file
After Width: | Height: | Size: 265 B |
BIN
daemon/core/gui/data/icons/prouter.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
daemon/core/gui/data/icons/rectangle.png
Normal file
After Width: | Height: | Size: 259 B |
BIN
daemon/core/gui/data/icons/rj45.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/icons/router.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
daemon/core/gui/data/icons/run.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
daemon/core/gui/data/icons/select.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
daemon/core/gui/data/icons/start.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
daemon/core/gui/data/icons/stop.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
daemon/core/gui/data/icons/text.png
Normal file
After Width: | Height: | Size: 314 B |
BIN
daemon/core/gui/data/icons/tunnel.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
daemon/core/gui/data/icons/twonode.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
daemon/core/gui/data/icons/wlan.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
28
daemon/core/gui/data/mobility/sample1.scen
Normal 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"
|
BIN
daemon/core/gui/data/oldicons/docker.gif
Normal file
After Width: | Height: | Size: 719 B |
BIN
daemon/core/gui/data/oldicons/emane.gif
Normal file
After Width: | Height: | Size: 337 B |
BIN
daemon/core/gui/data/oldicons/host.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/hub.gif
Normal file
After Width: | Height: | Size: 719 B |
BIN
daemon/core/gui/data/oldicons/lanswitch.gif
Normal file
After Width: | Height: | Size: 744 B |
BIN
daemon/core/gui/data/oldicons/link.gif
Normal file
After Width: | Height: | Size: 86 B |
BIN
daemon/core/gui/data/oldicons/lxc.gif
Normal file
After Width: | Height: | Size: 724 B |
BIN
daemon/core/gui/data/oldicons/marker.gif
Normal file
After Width: | Height: | Size: 375 B |
BIN
daemon/core/gui/data/oldicons/mdr.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/oval.gif
Normal file
After Width: | Height: | Size: 174 B |
BIN
daemon/core/gui/data/oldicons/pc.gif
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
daemon/core/gui/data/oldicons/rectangle.gif
Normal file
After Width: | Height: | Size: 160 B |
BIN
daemon/core/gui/data/oldicons/rj45.gif
Normal file
After Width: | Height: | Size: 755 B |
BIN
daemon/core/gui/data/oldicons/router.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/oldicons/router_green.gif
Normal file
After Width: | Height: | Size: 753 B |
BIN
daemon/core/gui/data/oldicons/run.gif
Normal file
After Width: | Height: | Size: 324 B |
BIN
daemon/core/gui/data/oldicons/select.gif
Normal file
After Width: | Height: | Size: 925 B |
BIN
daemon/core/gui/data/oldicons/start.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
daemon/core/gui/data/oldicons/stop.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
daemon/core/gui/data/oldicons/text.gif
Normal file
After Width: | Height: | Size: 127 B |
BIN
daemon/core/gui/data/oldicons/tunnel.gif
Normal file
After Width: | Height: | Size: 799 B |
BIN
daemon/core/gui/data/oldicons/twonode.gif
Normal file
After Width: | Height: | Size: 220 B |
BIN
daemon/core/gui/data/oldicons/wlan.gif
Normal file
After Width: | Height: | Size: 173 B |
1869
daemon/core/gui/data/xmls/sample1.xml
Normal file
0
daemon/core/gui/dialogs/__init__.py
Normal file
44
daemon/core/gui/dialogs/about.py
Normal 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")
|
171
daemon/core/gui/dialogs/alerts.py
Normal 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")
|
254
daemon/core/gui/dialogs/canvassizeandscale.py
Normal 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()
|
179
daemon/core/gui/dialogs/canvaswallpaper.py
Normal 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()
|
251
daemon/core/gui/dialogs/colorpicker.py
Normal 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)
|
261
daemon/core/gui/dialogs/customnodes.py
Normal 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)
|
37
daemon/core/gui/dialogs/dialog.py
Normal 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)
|
235
daemon/core/gui/dialogs/emaneconfig.py
Normal 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()
|
152
daemon/core/gui/dialogs/hooks.py
Normal 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)
|
362
daemon/core/gui/dialogs/linkconfig.py
Normal 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))
|
73
daemon/core/gui/dialogs/marker.py
Normal 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))
|
55
daemon/core/gui/dialogs/mobilityconfig.py
Normal 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()
|
163
daemon/core/gui/dialogs/mobilityplayer.py
Normal 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)
|
237
daemon/core/gui/dialogs/nodeconfig.py
Normal 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()
|
134
daemon/core/gui/dialogs/nodeservice.py
Normal 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()
|
143
daemon/core/gui/dialogs/observers.py
Normal 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)
|
87
daemon/core/gui/dialogs/preferences.py
Normal 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()
|
173
daemon/core/gui/dialogs/servers.py
Normal 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)
|
453
daemon/core/gui/dialogs/serviceconfiguration.py
Normal 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")
|
53
daemon/core/gui/dialogs/sessionoptions.py
Normal 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()
|
181
daemon/core/gui/dialogs/sessions.py
Normal 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()
|
252
daemon/core/gui/dialogs/shapemod.py
Normal 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()
|
66
daemon/core/gui/dialogs/wlanconfig.py
Normal 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()
|
8
daemon/core/gui/errors.py
Normal 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())
|
0
daemon/core/gui/graph/__init__.py
Normal file
181
daemon/core/gui/graph/edges.py
Normal 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()
|
18
daemon/core/gui/graph/enums.py
Normal 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
|
839
daemon/core/gui/graph/graph.py
Normal 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)
|
152
daemon/core/gui/graph/linkinfo.py
Normal 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)
|
274
daemon/core/gui/graph/node.py
Normal 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()
|
179
daemon/core/gui/graph/shape.py
Normal 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,
|
||||
}
|