fix merge conflict

This commit is contained in:
Huy Pham 2020-03-04 11:14:20 -08:00
commit 6ce29bea75
21 changed files with 371 additions and 178 deletions

View file

@ -475,9 +475,10 @@ class CoreGrpcClient:
self, self,
session_id: int, session_id: int,
node_id: int, node_id: int,
position: core_pb2.Position, position: core_pb2.Position = None,
icon: str = None, icon: str = None,
source: str = None, source: str = None,
geo: core_pb2.Geo = None,
) -> core_pb2.EditNodeResponse: ) -> core_pb2.EditNodeResponse:
""" """
Edit a node, currently only changes position. Edit a node, currently only changes position.
@ -487,6 +488,7 @@ class CoreGrpcClient:
:param position: position to set node to :param position: position to set node to
:param icon: path to icon for gui to use for node :param icon: path to icon for gui to use for node
:param source: application source editing node :param source: application source editing node
:param geo: lon,lat,alt location for node
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or node doesn't exist :raises grpc.RpcError: when session or node doesn't exist
""" """
@ -496,6 +498,7 @@ class CoreGrpcClient:
position=position, position=position,
icon=icon, icon=icon,
source=source, source=source,
geo=geo,
) )
return self.stub.EditNode(request) return self.stub.EditNode(request)

View file

@ -44,7 +44,9 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
position = node_proto.position position = node_proto.position
options.set_position(position.x, position.y) options.set_position(position.x, position.y)
options.set_location(position.lat, position.lon, position.alt) if node_proto.HasField("geo"):
geo = node_proto.geo
options.set_location(geo.lat, geo.lon, geo.alt)
return _type, _id, options return _type, _id, options
@ -379,7 +381,7 @@ def service_configuration(session: Session, config: core_pb2.ServiceConfig) -> N
if config.files: if config.files:
service.configs = tuple(config.files) service.configs = tuple(config.files)
if config.directories: if config.directories:
service.directories = tuple(config.directories) service.dirs = tuple(config.directories)
if config.startup: if config.startup:
service.startup = tuple(config.startup) service.startup = tuple(config.startup)
if config.validate: if config.validate:

View file

@ -688,21 +688,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
node = self.get_node(session, request.node_id, context) node = self.get_node(session, request.node_id, context)
options = NodeOptions() options = NodeOptions()
options.icon = request.icon options.icon = request.icon
x = request.position.x if request.HasField("position"):
y = request.position.y x = request.position.x
options.set_position(x, y) y = request.position.y
lat = request.position.lat options.set_position(x, y)
lon = request.position.lon lat, lon, alt = None, None, None
alt = request.position.alt has_geo = request.HasField("geo")
options.set_location(lat, lon, alt) if has_geo:
lat = request.geo.lat
lon = request.geo.lon
alt = request.geo.alt
options.set_location(lat, lon, alt)
result = True result = True
try: try:
session.edit_node(node.id, options) session.edit_node(node.id, options)
source = None source = None
if request.source: if request.source:
source = request.source source = request.source
node_data = node.data(0, source=source) if not has_geo:
session.broadcast_node(node_data) node_data = node.data(0, source=source)
session.broadcast_node(node_data)
except CoreError: except CoreError:
result = False result = False
return core_pb2.EditNodeResponse(result=result) return core_pb2.EditNodeResponse(result=result)

View file

@ -782,7 +782,8 @@ class Session:
node.canvas = options.canvas node.canvas = options.canvas
node.icon = options.icon node.icon = options.icon
self.sdt.edit_node(node) # provide edits to sdt
self.sdt.edit_node(node, options.lon, options.lat, options.alt)
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
""" """
@ -812,9 +813,11 @@ class Session:
# broadcast updated location when using lat/lon/alt # broadcast updated location when using lat/lon/alt
if using_lat_lon_alt: if using_lat_lon_alt:
self.broadcast_node_location(node) self.broadcast_node_location(node, lon, lat, alt)
def broadcast_node_location(self, node: NodeBase) -> None: def broadcast_node_location(
self, node: NodeBase, lon: float, lat: float, alt: float
) -> None:
""" """
Broadcast node location to all listeners. Broadcast node location to all listeners.
@ -826,6 +829,9 @@ class Session:
id=node.id, id=node.id,
x_position=node.position.x, x_position=node.position.x,
y_position=node.position.y, y_position=node.position.y,
latitude=lat,
longitude=lon,
altitude=alt,
) )
self.broadcast_node(node_data) self.broadcast_node(node_data)

View file

@ -1,3 +1,4 @@
import math
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
@ -47,11 +48,10 @@ class Application(tk.Frame):
def setup_scaling(self): def setup_scaling(self):
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()} self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
text_scale = self.app_scale if self.app_scale < 1 else math.sqrt(self.app_scale)
themes.scale_fonts(self.fonts_size, self.app_scale) themes.scale_fonts(self.fonts_size, self.app_scale)
self.icon_text_font = font.Font( self.icon_text_font = font.Font(family="TkIconFont", size=int(12 * text_scale))
family="TkIconFont", size=int(12 * self.app_scale) self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * text_scale))
)
self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * self.app_scale))
def setup_theme(self): def setup_theme(self):
themes.load(self.style) themes.load(self.style)

View file

@ -500,7 +500,6 @@ class CoreClient:
emane_config = {x: self.emane_config[x].value for x in self.emane_config} emane_config = {x: self.emane_config[x].value for x in self.emane_config}
else: else:
emane_config = None emane_config = None
response = core_pb2.StartSessionResponse(result=False) response = core_pb2.StartSessionResponse(result=False)
try: try:
response = self.client.start_session( response = self.client.start_session(
@ -521,7 +520,6 @@ class CoreClient:
logging.info( logging.info(
"start session(%s), result: %s", self.session_id, response.result "start session(%s), result: %s", self.session_id, response.result
) )
if response.result: if response.result:
self.set_metadata() self.set_metadata()
except grpc.RpcError as e: except grpc.RpcError as e:
@ -620,6 +618,8 @@ class CoreClient:
self, self,
node_id: int, node_id: int,
service_name: str, service_name: str,
dirs: List[str],
files: List[str],
startups: List[str], startups: List[str],
validations: List[str], validations: List[str],
shutdowns: List[str], shutdowns: List[str],
@ -628,14 +628,17 @@ class CoreClient:
self.session_id, self.session_id,
node_id, node_id,
service_name, service_name,
directories=dirs,
files=files,
startup=startups, startup=startups,
validate=validations, validate=validations,
shutdown=shutdowns, shutdown=shutdowns,
) )
logging.info( logging.info(
"Set %s service for node(%s), Startup: %s, Validation: %s, Shutdown: %s, Result: %s", "Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
service_name, service_name,
node_id, node_id,
files,
startups, startups,
validations, validations,
shutdowns, shutdowns,
@ -933,6 +936,8 @@ class CoreClient:
config_proto = core_pb2.ServiceConfig( config_proto = core_pb2.ServiceConfig(
node_id=node_id, node_id=node_id,
service=name, service=name,
directories=config.dirs,
files=config.configs,
startup=config.startup, startup=config.startup,
validate=config.validate, validate=config.validate,
shutdown=config.shutdown, shutdown=config.shutdown,

View file

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph import tags
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -19,7 +20,7 @@ class MarkerDialog(Dialog):
def __init__( def __init__(
self, master: "Application", app: "Application", initcolor: str = "#000000" self, master: "Application", app: "Application", initcolor: str = "#000000"
): ):
super().__init__(master, app, "marker tool", modal=False) super().__init__(master, app, "Marker Tool", modal=False)
self.app = app self.app = app
self.color = initcolor self.color = initcolor
self.radius = MARKER_THICKNESS[0] self.radius = MARKER_THICKNESS[0]
@ -56,8 +57,7 @@ class MarkerDialog(Dialog):
def clear_marker(self): def clear_marker(self):
canvas = self.app.canvas canvas = self.app.canvas
for i in canvas.find_withtag("marker"): canvas.delete(tags.MARKER)
canvas.delete(i)
def change_color(self, event: tk.Event): def change_color(self, event: tk.Event):
color_picker = ColorPickerDialog(self, self.app, self.color) color_picker = ColorPickerDialog(self, self.app, self.color)

View file

@ -1,4 +1,5 @@
import logging import logging
import math
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -6,10 +7,13 @@ from typing import TYPE_CHECKING
from core.gui import appconfig from core.gui import appconfig
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
SCALE_INTERVAL = 0.01
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, master: "Application", app: "Application"):
@ -73,18 +77,25 @@ class PreferencesDialog(Dialog):
scale_frame.columnconfigure(0, weight=1) scale_frame.columnconfigure(0, weight=1)
scale = ttk.Scale( scale = ttk.Scale(
scale_frame, scale_frame,
from_=0.5, from_=SMALLEST_SCALE,
to=5, to=LARGEST_SCALE,
value=1, value=1,
orient=tk.HORIZONTAL, orient=tk.HORIZONTAL,
variable=self.gui_scale, variable=self.gui_scale,
) )
scale.grid(row=0, column=0, sticky="ew") scale.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry( entry = ttk.Entry(
scale_frame, textvariable=self.gui_scale, width=4, state="disabled" scale_frame,
textvariable=self.gui_scale,
width=4,
validate="key",
validatecommand=(self.app.validation.app_scale, "%P"),
) )
entry.grid(row=0, column=1) entry.grid(row=0, column=1)
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
scrollbar.grid(row=0, column=2)
def draw_buttons(self): def draw_buttons(self):
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
@ -123,8 +134,9 @@ class PreferencesDialog(Dialog):
# scale fonts # scale fonts
scale_fonts(self.app.fonts_size, app_scale) scale_fonts(self.app.fonts_size, app_scale)
self.app.icon_text_font.config(size=int(12 * app_scale)) text_scale = app_scale if app_scale < 1 else math.sqrt(app_scale)
self.app.edge_font.config(size=int(8 * app_scale)) self.app.icon_text_font.config(size=int(12 * text_scale))
self.app.edge_font.config(size=int(8 * text_scale))
# scale application window # scale application window
self.app.center() self.app.center()
@ -132,3 +144,16 @@ class PreferencesDialog(Dialog):
# scale toolbar and canvas items # scale toolbar and canvas items
self.app.toolbar.scale() self.app.toolbar.scale()
self.app.canvas.scale_graph() self.app.canvas.scale_graph()
def adjust_scale(self, arg1: str, arg2: str, arg3: str):
scale_value = self.gui_scale.get()
if arg2 == "-1":
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:
self.gui_scale.set(round(scale_value + SCALE_INTERVAL, 2))
else:
self.gui_scale.set(round(LARGEST_SCALE, 2))
elif arg2 == "1":
if scale_value >= SMALLEST_SCALE + SCALE_INTERVAL:
self.gui_scale.set(round(scale_value - SCALE_INTERVAL, 2))
else:
self.gui_scale.set(round(SMALLEST_SCALE, 2))

View file

@ -1,8 +1,7 @@
""" import logging
Service configuration dialog import os
"""
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import filedialog, ttk
from typing import TYPE_CHECKING, Any, List from typing import TYPE_CHECKING, Any, List
import grpc import grpc
@ -48,12 +47,18 @@ class ServiceConfigDialog(Dialog):
self.validation_mode = None self.validation_mode = None
self.validation_time = None self.validation_time = None
self.validation_period = None self.validation_period = None
self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) self.directory_entry = None
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) self.default_directories = []
self.temp_directories = []
self.documentnew_img = Images.get(
ImageEnum.DOCUMENTNEW, int(16 * app.app_scale)
)
self.editdelete_img = Images.get(ImageEnum.EDITDELETE, int(16 * app.app_scale))
self.notebook = None self.notebook = None
self.metadata_entry = None self.metadata_entry = None
self.filename_combobox = None self.filename_combobox = None
self.dir_list = None
self.startup_commands_listbox = None self.startup_commands_listbox = None
self.shutdown_commands_listbox = None self.shutdown_commands_listbox = None
self.validate_commands_listbox = None self.validate_commands_listbox = None
@ -62,6 +67,7 @@ class ServiceConfigDialog(Dialog):
self.service_file_data = None self.service_file_data = None
self.validation_period_entry = None self.validation_period_entry = None
self.original_service_files = {} self.original_service_files = {}
self.default_config = None
self.temp_service_files = {} self.temp_service_files = {}
self.modified_files = set() self.modified_files = set()
@ -71,7 +77,7 @@ class ServiceConfigDialog(Dialog):
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def load(self) -> bool: def load(self):
try: try:
self.app.core.create_nodes_and_links() self.app.core.create_nodes_and_links()
default_config = self.app.core.get_node_service( default_config = self.app.core.get_node_service(
@ -80,15 +86,14 @@ class ServiceConfigDialog(Dialog):
self.default_startup = default_config.startup[:] self.default_startup = default_config.startup[:]
self.default_validate = default_config.validate[:] self.default_validate = default_config.validate[:]
self.default_shutdown = default_config.shutdown[:] self.default_shutdown = default_config.shutdown[:]
custom_configs = self.service_configs self.default_directories = default_config.dirs[:]
if ( custom_service_config = self.service_configs.get(self.node_id, {}).get(
self.node_id in custom_configs self.service_name, None
and self.service_name in custom_configs[self.node_id] )
): self.default_config = default_config
service_config = custom_configs[self.node_id][self.service_name] service_config = (
else: custom_service_config if custom_service_config else default_config
service_config = default_config )
self.dependencies = service_config.dependencies[:] self.dependencies = service_config.dependencies[:]
self.executables = service_config.executables[:] self.executables = service_config.executables[:]
self.metadata = service_config.meta self.metadata = service_config.meta
@ -98,20 +103,19 @@ class ServiceConfigDialog(Dialog):
self.shutdown_commands = service_config.shutdown[:] self.shutdown_commands = service_config.shutdown[:]
self.validation_mode = service_config.validation_mode self.validation_mode = service_config.validation_mode
self.validation_time = service_config.validation_timer self.validation_time = service_config.validation_timer
self.temp_directories = service_config.dirs[:]
self.original_service_files = { self.original_service_files = {
x: self.app.core.get_node_service_file( x: self.app.core.get_node_service_file(
self.node_id, self.service_name, x self.node_id, self.service_name, x
) )
for x in self.filenames for x in default_config.configs
} }
self.temp_service_files = dict(self.original_service_files) self.temp_service_files = dict(self.original_service_files)
file_configs = self.file_configs file_config = self.file_configs.get(self.node_id, {}).get(
if ( self.service_name, {}
self.node_id in file_configs )
and self.service_name in file_configs[self.node_id] for file, data in file_config.items():
): self.temp_service_files[file] = data
for file, data in file_configs[self.node_id][self.service_name].items():
self.temp_service_files[file] = data
except grpc.RpcError as e: except grpc.RpcError as e:
self.has_error = True self.has_error = True
show_grpc_error(e, self.master, self.app) show_grpc_error(e, self.master, self.app)
@ -155,18 +159,18 @@ class ServiceConfigDialog(Dialog):
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="File Name") label = ttk.Label(frame, text="File Name")
label.grid(row=0, column=0, padx=PADX, sticky="w") label.grid(row=0, column=0, padx=PADX, sticky="w")
self.filename_combobox = ttk.Combobox( self.filename_combobox = ttk.Combobox(frame, values=self.filenames)
frame, values=self.filenames, state="readonly"
)
self.filename_combobox.bind( self.filename_combobox.bind(
"<<ComboboxSelected>>", self.display_service_file_data "<<ComboboxSelected>>", self.display_service_file_data
) )
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
button = ttk.Button(frame, image=self.documentnew_img, state="disabled") button = ttk.Button(
button.bind("<Button-1>", self.add_filename) frame, image=self.documentnew_img, command=self.add_filename
)
button.grid(row=0, column=2, padx=PADX) button.grid(row=0, column=2, padx=PADX)
button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button = ttk.Button(
button.bind("<Button-1>", self.delete_filename) frame, image=self.editdelete_img, command=self.delete_filename
)
button.grid(row=0, column=3) button.grid(row=0, column=3)
frame = ttk.Frame(tab) frame = ttk.Frame(tab)
@ -229,7 +233,30 @@ class ServiceConfigDialog(Dialog):
tab, tab,
text="Directories required by this service that are unique for each node.", text="Directories required by this service that are unique for each node.",
) )
label.grid() label.grid(row=0, column=0, sticky="ew")
frame = ttk.Frame(tab, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=1, column=0, sticky="nsew")
var = tk.StringVar(value="")
self.directory_entry = ttk.Entry(frame, textvariable=var)
self.directory_entry.grid(row=0, column=0, sticky="ew")
button = ttk.Button(frame, text="...", command=self.find_directory_button)
button.grid(row=0, column=1, sticky="ew")
self.dir_list = ListboxScroll(tab)
self.dir_list.grid(row=2, column=0, sticky="nsew")
self.dir_list.listbox.bind("<<ListboxSelect>>", self.directory_select)
for d in self.temp_directories:
self.dir_list.listbox.insert("end", d)
frame = ttk.Frame(tab)
frame.grid(row=3, column=0, sticky="nsew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Add", command=self.add_directory)
button.grid(row=0, column=0, sticky="ew")
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
button.grid(row=0, column=1, sticky="ew")
def draw_tab_startstop(self): def draw_tab_startstop(self):
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
@ -358,26 +385,30 @@ class ServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
def add_filename(self, event: tk.Event): def add_filename(self):
# not worry about it for now filename = self.filename_combobox.get()
return if filename not in self.filename_combobox["values"]:
frame_contains_button = event.widget.master self.filename_combobox["values"] += (filename,)
combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] self.filename_combobox.set(filename)
filename = combobox.get() self.temp_service_files[filename] = self.service_file_data.text.get(
if filename not in combobox["values"]: 1.0, "end"
combobox["values"] += (filename,) )
else:
logging.debug("file already existed")
def delete_filename(self, event: tk.Event): def delete_filename(self):
# not worry about it for now cbb = self.filename_combobox
return filename = cbb.get()
frame_comntains_button = event.widget.master if filename in cbb["values"]:
combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] cbb["values"] = tuple([x for x in cbb["values"] if x != filename])
filename = combobox.get() cbb.set("")
if filename in combobox["values"]: self.service_file_data.text.delete(1.0, "end")
combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) self.temp_service_files.pop(filename, None)
combobox.set("") if filename in self.modified_files:
self.modified_files.remove(filename)
def add_command(self, event: tk.Event): @classmethod
def add_command(cls, event: tk.Event):
frame_contains_button = event.widget.master frame_contains_button = event.widget.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox 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() command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
@ -388,7 +419,8 @@ class ServiceConfigDialog(Dialog):
return return
listbox.insert(tk.END, command_to_add) listbox.insert(tk.END, command_to_add)
def update_entry(self, event: tk.Event): @classmethod
def update_entry(cls, event: tk.Event):
listbox = event.widget listbox = event.widget
current_selection = listbox.curselection() current_selection = listbox.curselection()
if len(current_selection) > 0: if len(current_selection) > 0:
@ -399,7 +431,8 @@ class ServiceConfigDialog(Dialog):
entry.delete(0, "end") entry.delete(0, "end")
entry.insert(0, cmd) entry.insert(0, cmd)
def delete_command(self, event: tk.Event): @classmethod
def delete_command(cls, event: tk.Event):
button = event.widget button = event.widget
frame_contains_button = button.master frame_contains_button = button.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
@ -410,30 +443,36 @@ class ServiceConfigDialog(Dialog):
entry.delete(0, tk.END) entry.delete(0, tk.END)
def click_apply(self): def click_apply(self):
current_listbox = self.master.current.listbox if (
if not self.is_custom_service_config() and not self.is_custom_service_file(): not self.is_custom_command()
if self.node_id in self.service_configs: and not self.is_custom_service_file()
self.service_configs[self.node_id].pop(self.service_name, None) and not self.has_new_files()
current_listbox.itemconfig(current_listbox.curselection()[0], bg="") and not self.is_custom_directory()
):
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
self.current_service_color("")
self.destroy() self.destroy()
return return
try: try:
if self.is_custom_service_config(): if (
startup_commands = self.startup_commands_listbox.get(0, "end") self.is_custom_command()
shutdown_commands = self.shutdown_commands_listbox.get(0, "end") or self.has_new_files()
validate_commands = self.validate_commands_listbox.get(0, "end") or self.is_custom_directory()
):
startup, validate, shutdown = self.get_commands()
config = self.core.set_node_service( config = self.core.set_node_service(
self.node_id, self.node_id,
self.service_name, self.service_name,
startups=startup_commands, dirs=self.temp_directories,
validations=validate_commands, files=list(self.filename_combobox["values"]),
shutdowns=shutdown_commands, startups=startup,
validations=validate,
shutdowns=shutdown,
) )
if self.node_id not in self.service_configs: if self.node_id not in self.service_configs:
self.service_configs[self.node_id] = {} self.service_configs[self.node_id] = {}
self.service_configs[self.node_id][self.service_name] = config self.service_configs[self.node_id][self.service_name] = config
for file in self.modified_files: for file in self.modified_files:
if self.node_id not in self.file_configs: if self.node_id not in self.file_configs:
self.file_configs[self.node_id] = {} self.file_configs[self.node_id] = {}
@ -442,53 +481,67 @@ class ServiceConfigDialog(Dialog):
self.file_configs[self.node_id][self.service_name][ self.file_configs[self.node_id][self.service_name][
file file
] = self.temp_service_files[file] ] = self.temp_service_files[file]
self.app.core.set_node_service_file( self.app.core.set_node_service_file(
self.node_id, self.service_name, file, self.temp_service_files[file] self.node_id, self.service_name, file, self.temp_service_files[file]
) )
all_current = current_listbox.get(0, tk.END) self.current_service_color("green")
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
except grpc.RpcError as e: except grpc.RpcError as e:
show_grpc_error(e, self.top, self.app) show_grpc_error(e, self.top, self.app)
self.destroy() self.destroy()
def display_service_file_data(self, event: tk.Event): def display_service_file_data(self, event: tk.Event):
combobox = event.widget filename = self.filename_combobox.get()
filename = combobox.get()
self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert("end", self.temp_service_files[filename]) self.service_file_data.text.insert("end", self.temp_service_files[filename])
def update_temp_service_file_data(self, event: tk.Event): def update_temp_service_file_data(self, event: tk.Event):
scrolledtext = event.widget
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
self.temp_service_files[filename] = scrolledtext.get(1.0, "end") self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
if self.temp_service_files[filename] != self.original_service_files[filename]: if self.temp_service_files[filename] != self.original_service_files.get(
filename, ""
):
self.modified_files.add(filename) self.modified_files.add(filename)
else: else:
self.modified_files.discard(filename) self.modified_files.discard(filename)
def is_custom_service_config(self): def is_custom_command(self):
startup_commands = self.startup_commands_listbox.get(0, "end") startup, validate, shutdown = self.get_commands()
shutdown_commands = self.shutdown_commands_listbox.get(0, "end")
validate_commands = self.validate_commands_listbox.get(0, "end")
return ( return (
set(self.default_startup) != set(startup_commands) set(self.default_startup) != set(startup)
or set(self.default_validate) != set(validate_commands) or set(self.default_validate) != set(validate)
or set(self.default_shutdown) != set(shutdown_commands) or set(self.default_shutdown) != set(shutdown)
) )
def has_new_files(self):
return set(self.filenames) != set(self.filename_combobox["values"])
def is_custom_service_file(self): def is_custom_service_file(self):
return len(self.modified_files) > 0 return len(self.modified_files) > 0
def is_custom_directory(self):
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
def click_defaults(self): def click_defaults(self):
if self.node_id in self.service_configs: """
self.service_configs[self.node_id].pop(self.service_name, None) clears out any custom configuration permanently
if self.node_id in self.file_configs: """
self.file_configs[self.node_id].pop(self.service_name, None) # clear coreclient data
self.service_configs.get(self.node_id, {}).pop(self.service_name, None)
self.file_configs.get(self.node_id, {}).pop(self.service_name, None)
self.temp_service_files = dict(self.original_service_files) self.temp_service_files = dict(self.original_service_files)
filename = self.filename_combobox.get() self.modified_files.clear()
# reset files tab
files = list(self.default_config.configs[:])
self.filenames = files
self.filename_combobox.config(values=files)
self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert("end", self.temp_service_files[filename]) if len(files) > 0:
filename = files[0]
self.filename_combobox.set(filename)
self.service_file_data.text.insert("end", self.temp_service_files[filename])
# reset commands
self.startup_commands_listbox.delete(0, tk.END) self.startup_commands_listbox.delete(0, tk.END)
self.validate_commands_listbox.delete(0, tk.END) self.validate_commands_listbox.delete(0, tk.END)
self.shutdown_commands_listbox.delete(0, tk.END) self.shutdown_commands_listbox.delete(0, tk.END)
@ -499,13 +552,68 @@ class ServiceConfigDialog(Dialog):
for cmd in self.default_shutdown: for cmd in self.default_shutdown:
self.shutdown_commands_listbox.insert(tk.END, cmd) self.shutdown_commands_listbox.insert(tk.END, cmd)
# reset directories
self.directory_entry.delete(0, "end")
self.dir_list.listbox.delete(0, "end")
self.temp_directories = list(self.default_directories)
for d in self.default_directories:
self.dir_list.listbox.insert("end", d)
self.current_service_color("")
def click_copy(self): def click_copy(self):
dialog = CopyServiceConfigDialog(self, self.app, self.node_id) dialog = CopyServiceConfigDialog(self, self.app, self.node_id)
dialog.show() dialog.show()
@classmethod
def append_commands( def append_commands(
self, commands: List[str], listbox: tk.Listbox, to_add: List[str] cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
): ):
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)
def get_commands(self):
startup = self.startup_commands_listbox.get(0, "end")
shutdown = self.shutdown_commands_listbox.get(0, "end")
validate = self.validate_commands_listbox.get(0, "end")
return startup, validate, shutdown
def find_directory_button(self):
d = filedialog.askdirectory(initialdir="/")
self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d)
def add_directory(self):
d = self.directory_entry.get()
if os.path.isdir(d):
if d not in self.temp_directories:
self.dir_list.listbox.insert("end", d)
self.temp_directories.append(d)
def remove_directory(self):
d = self.directory_entry.get()
dirs = self.dir_list.listbox.get(0, "end")
if d and d in self.temp_directories:
self.temp_directories.remove(d)
try:
i = dirs.index(d)
self.dir_list.listbox.delete(i)
except ValueError:
logging.debug("directory is not in the list")
self.directory_entry.delete(0, "end")
def directory_select(self, event):
i = self.dir_list.listbox.curselection()
if i:
d = self.dir_list.listbox.get(i)
self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d)
def current_service_color(self, color=""):
"""
change the current service label color
"""
listbox = self.master.current.listbox
services = listbox.get(0, tk.END)
listbox.itemconfig(services.index(self.service_name), bg=color)

View file

@ -534,7 +534,7 @@ class CanvasGraph(tk.Canvas):
y + r, y + r,
fill=self.app.toolbar.marker_tool.color, fill=self.app.toolbar.marker_tool.color,
outline="", outline="",
tags="marker", tags=tags.MARKER,
) )
return return
if selected is None: if selected is None:
@ -656,8 +656,11 @@ class CanvasGraph(tk.Canvas):
delete selected nodes and any data that relates to it delete selected nodes and any data that relates to it
""" """
logging.debug("press delete key") logging.debug("press delete key")
nodes = self.delete_selection_objects() if not self.app.core.is_runtime():
self.core.delete_graph_nodes(nodes) nodes = self.delete_selection_objects()
self.core.delete_graph_nodes(nodes)
else:
logging.info("node deletion is disabled during runtime state")
def double_click(self, event: tk.Event): def double_click(self, event: tk.Event):
selected = self.get_selected(event) selected = self.get_selected(event)
@ -850,11 +853,17 @@ class CanvasGraph(tk.Canvas):
self.core.create_link(edge, source, dest) self.core.create_link(edge, source, dest)
def copy(self): def copy(self):
if self.app.core.is_runtime():
logging.info("copy is disabled during runtime state")
return
if self.selection: if self.selection:
logging.debug("to copy %s nodes", len(self.selection)) logging.debug("to copy %s nodes", len(self.selection))
self.to_copy = self.selection.keys() self.to_copy = self.selection.keys()
def paste(self): def paste(self):
if self.app.core.is_runtime():
logging.info("paste is disabled during runtime state")
return
# maps original node canvas id to copy node canvas id # maps original node canvas id to copy node canvas id
copy_map = {} copy_map = {}
# the edges that will be copy over # the edges that will be copy over

View file

@ -17,7 +17,7 @@ from core.gui.errors import show_grpc_error
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, EdgeUtils, NodeUtils from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -66,48 +66,9 @@ class CanvasNode:
def delete(self): def delete(self):
logging.debug("Delete canvas node for %s", self.core_node) logging.debug("Delete canvas node for %s", self.core_node)
# if node is wlan, EMANE type, remove any existing wireless links between nodes connetect to this node
if NodeUtils.is_wireless_node(self.core_node.type):
nodes = []
for edge in self.edges:
token = edge.token
if self.id == token[0]:
nodes.append(token[1])
else:
nodes.append(token[0])
for i in range(len(nodes)):
for j in range(i + 1, len(nodes)):
token = EdgeUtils.get_token(nodes[i], nodes[j])
wireless_edge = self.canvas.wireless_edges.pop(token, None)
if wireless_edge:
self.canvas.nodes[nodes[i]].wireless_edges.remove(wireless_edge)
self.canvas.nodes[nodes[j]].wireless_edges.remove(wireless_edge)
self.canvas.delete(wireless_edge.id)
else:
logging.debug("%s is not a wireless edge", token)
# if node is MDR, remove wireless links to other MDRs
elif NodeUtils.is_mdr_node(self.core_node.type, self.core_node.model):
for wireless_edge in self.wireless_edges:
token = wireless_edge.token
other = token[0]
if other == self.id:
other = token[1]
self.canvas.nodes[other].wireless_edges.discard(wireless_edge)
try:
wlan_edge = self.canvas.wireless_edges.pop(token)
self.canvas.delete(wlan_edge.id)
except KeyError:
logging.error(
"wireless link not found, potentially multiple wireless link issue"
)
self.delete_antennas()
self.wireless_edges.clear()
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)
self.delete_antennas()
def add_antenna(self): def add_antenna(self):
x, y = self.canvas.coords(self.id) x, y = self.canvas.coords(self.id)

View file

@ -10,6 +10,7 @@ NODE = "node"
WALLPAPER = "wallpaper" WALLPAPER = "wallpaper"
SELECTION = "selectednodes" SELECTION = "selectednodes"
THROUGHPUT = "throughput" THROUGHPUT = "throughput"
MARKER = "marker"
ABOVE_WALLPAPER_TAGS = [ ABOVE_WALLPAPER_TAGS = [
GRIDLINE, GRIDLINE,
SHAPE, SHAPE,
@ -33,4 +34,5 @@ COMPONENT_TAGS = [
SELECTION, SELECTION,
SHAPE, SHAPE,
SHAPE_TEXT, SHAPE_TEXT,
MARKER,
] ]

View file

@ -106,6 +106,8 @@ class TypeToImage:
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE, (core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45, (core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL, (core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
(core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER,
(core_pb2.NodeType.LXC, ""): ImageEnum.LXC,
} }
@classmethod @classmethod

View file

@ -26,6 +26,7 @@ class Menubar(tk.Menu):
self.app = app self.app = app
self.menuaction = action.MenuAction(app, master) self.menuaction = action.MenuAction(app, master)
self.recent_menu = None self.recent_menu = None
self.edit_menu = None
self.draw() self.draw()
def draw(self): def draw(self):
@ -111,6 +112,7 @@ class Menubar(tk.Menu):
self.app.master.bind_all("<Control-c>", self.menuaction.copy) self.app.master.bind_all("<Control-c>", self.menuaction.copy)
self.app.master.bind_all("<Control-v>", self.menuaction.paste) self.app.master.bind_all("<Control-v>", self.menuaction.paste)
self.edit_menu = menu
def draw_canvas_menu(self): def draw_canvas_menu(self):
""" """
@ -444,3 +446,15 @@ class Menubar(tk.Menu):
def execute_python(self): def execute_python(self):
dialog = ExecutePythonDialog(self.app, self.app) dialog = ExecutePythonDialog(self.app, self.app)
dialog.show() dialog.show()
def change_menubar_item_state(self, is_runtime: bool):
for i in range(self.edit_menu.index("end")):
try:
label_name = self.edit_menu.entrycget(i, "label")
if label_name in ["Copy", "Paste"]:
if is_runtime:
self.edit_menu.entryconfig(i, state="disabled")
else:
self.edit_menu.entryconfig(i, state="normal")
except tk.TclError:
logging.debug("Ignore separators")

View file

@ -90,10 +90,6 @@ class NodeUtils:
def is_rj45_node(cls, node_type: NodeType) -> bool: def is_rj45_node(cls, node_type: NodeType) -> bool:
return node_type in cls.RJ45_NODES return node_type in cls.RJ45_NODES
@classmethod
def is_mdr_node(cls, node_type: NodeType, model: str) -> bool:
return cls.is_container_node(node_type) and model == "mdr"
@classmethod @classmethod
def node_icon( def node_icon(
cls, cls,

View file

@ -182,21 +182,21 @@ def theme_change(event: tk.Event):
background="green", background="green",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font="TkSmallCaptionFont", font="TkDefaultFont",
) )
style.configure( style.configure(
Styles.yellow_alert, Styles.yellow_alert,
background="yellow", background="yellow",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font="TkSmallCaptionFont", font="TkDefaultFont",
) )
style.configure( style.configure(
Styles.red_alert, Styles.red_alert,
background="red", background="red",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font="TkSmallCaptionFont", font="TkDefaultFont",
) )

View file

@ -280,6 +280,7 @@ class Toolbar(ttk.Frame):
server. server.
""" """
self.app.canvas.hide_context() self.app.canvas.hide_context()
self.app.menubar.change_menubar_item_state(is_runtime=True)
self.app.statusbar.progress_bar.start(5) self.app.statusbar.progress_bar.start(5)
self.app.canvas.mode = GraphMode.SELECT self.app.canvas.mode = GraphMode.SELECT
self.time = time.perf_counter() self.time = time.perf_counter()
@ -469,6 +470,7 @@ class Toolbar(ttk.Frame):
""" """
logging.info("Click stop button") logging.info("Click stop button")
self.app.canvas.hide_context() self.app.canvas.hide_context()
self.app.menubar.change_menubar_item_state(is_runtime=False)
self.app.statusbar.progress_bar.start(5) self.app.statusbar.progress_bar.start(5)
self.time = time.perf_counter() self.time = time.perf_counter()
task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback) task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)

View file

@ -11,12 +11,16 @@ from netaddr import IPNetwork
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
SMALLEST_SCALE = 0.5
LARGEST_SCALE = 5.0
class InputValidation: class InputValidation:
def __init__(self, app: "Application"): def __init__(self, app: "Application"):
self.master = app.master self.master = app.master
self.positive_int = None self.positive_int = None
self.positive_float = None self.positive_float = None
self.app_scale = None
self.name = None self.name = None
self.ip4 = None self.ip4 = None
self.rgb = None self.rgb = None
@ -26,6 +30,7 @@ class InputValidation:
def register(self): def register(self):
self.positive_int = self.master.register(self.check_positive_int) self.positive_int = self.master.register(self.check_positive_int)
self.positive_float = self.master.register(self.check_positive_float) self.positive_float = self.master.register(self.check_positive_float)
self.app_scale = self.master.register(self.check_scale_value)
self.name = self.master.register(self.check_node_name) self.name = self.master.register(self.check_node_name)
self.ip4 = self.master.register(self.check_ip4) self.ip4 = self.master.register(self.check_ip4)
self.rgb = self.master.register(self.check_rbg) self.rgb = self.master.register(self.check_rbg)
@ -105,6 +110,18 @@ class InputValidation:
except ValueError: except ValueError:
return False return False
@classmethod
def check_scale_value(cls, s: str) -> bool:
if not s:
return True
try:
float_value = float(s)
if SMALLEST_SCALE <= float_value <= LARGEST_SCALE or float_value == 0:
return True
return False
except ValueError:
return False
@classmethod @classmethod
def check_ip4(cls, s: str) -> bool: def check_ip4(cls, s: str) -> bool:
if not s: if not s:

View file

@ -255,20 +255,28 @@ class Sdt:
self.cmd(f"sprite {node_type} image {icon}") self.cmd(f"sprite {node_type} image {icon}")
self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}') self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}')
def edit_node(self, node: NodeBase) -> None: def edit_node(self, node: NodeBase, lon: float, lat: float, alt: float) -> None:
""" """
Handle updating a node in SDT. Handle updating a node in SDT.
:param node: node to update :param node: node to update
:param lon: node longitude
:param lat: node latitude
:param alt: node altitude
:return: nothing :return: nothing
""" """
logging.debug("sdt update node: %s - %s", node.id, node.name) logging.debug("sdt update node: %s - %s", node.id, node.name)
if not self.connect(): if not self.connect():
return return
pos = self.get_node_position(node)
if not pos: if all([lat is not None, lon is not None, alt is not None]):
return pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {node.id} {pos}") self.cmd(f"node {node.id} {pos}")
else:
pos = self.get_node_position(node)
if not pos:
return
self.cmd(f"node {node.id} {pos}")
def delete_node(self, node_id: int) -> None: def delete_node(self, node_id: int) -> None:
""" """

View file

@ -10,6 +10,8 @@ from core.emulator.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.enumerations import NodeTypes from core.emulator.enumerations import NodeTypes
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode
from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet from core.nodes.network import CtrlNet
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -213,8 +215,21 @@ class DeviceElement(NodeElement):
def __init__(self, session: "Session", node: NodeBase) -> None: def __init__(self, session: "Session", node: NodeBase) -> None:
super().__init__(session, node, "device") super().__init__(session, node, "device")
add_attribute(self.element, "type", node.type) add_attribute(self.element, "type", node.type)
self.add_class()
self.add_services() self.add_services()
def add_class(self) -> None:
clazz = ""
image = ""
if isinstance(self.node, DockerNode):
clazz = "docker"
image = self.node.image
elif isinstance(self.node, LxcNode):
clazz = "lxc"
image = self.node.image
add_attribute(self.element, "class", clazz)
add_attribute(self.element, "image", image)
def add_services(self) -> None: def add_services(self) -> None:
service_elements = etree.Element("services") service_elements = etree.Element("services")
for service in self.node.services: for service in self.node.services:
@ -796,9 +811,17 @@ class CoreXmlReader:
name = device_element.get("name") name = device_element.get("name")
model = device_element.get("type") model = device_element.get("type")
icon = device_element.get("icon") icon = device_element.get("icon")
options = NodeOptions(name, model) clazz = device_element.get("class")
image = device_element.get("image")
options = NodeOptions(name, model, image)
options.icon = icon options.icon = icon
node_type = NodeTypes.DEFAULT
if clazz == "docker":
node_type = NodeTypes.DOCKER
elif clazz == "lxc":
node_type = NodeTypes.LXC
service_elements = device_element.find("services") service_elements = device_element.find("services")
if service_elements is not None: if service_elements is not None:
options.services = [x.get("name") for x in service_elements.iterchildren()] options.services = [x.get("name") for x in service_elements.iterchildren()]
@ -823,7 +846,7 @@ class CoreXmlReader:
options.set_location(lat, lon, alt) options.set_location(lat, lon, alt)
logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name)
self.session.add_node(_id=node_id, options=options) self.session.add_node(_type=node_type, _id=node_id, options=options)
def read_network(self, network_element: etree.Element) -> None: def read_network(self, network_element: etree.Element) -> None:
node_id = get_int(network_element, "id") node_id = get_int(network_element, "id")

View file

@ -408,6 +408,7 @@ message EditNodeRequest {
Position position = 3; Position position = 3;
string icon = 4; string icon = 4;
string source = 5; string source = 5;
Geo geo = 6;
} }
message EditNodeResponse { message EditNodeResponse {
@ -977,6 +978,7 @@ message Node {
string image = 10; string image = 10;
string server = 11; string server = 11;
repeated string config_services = 12; repeated string config_services = 12;
Geo geo = 13;
} }
message Link { message Link {
@ -1029,7 +1031,10 @@ message Position {
float x = 1; float x = 1;
float y = 2; float y = 2;
float z = 3; float z = 3;
float lat = 4; }
float lon = 5;
float alt = 6; message Geo {
float lat = 1;
float lon = 2;
float alt = 3;
} }