diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9c9c0d2f..faaf1c20 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -50,10 +50,8 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def draw_toolbar(self): - edit_frame = tk.Frame(self) - edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) - self.core_editbar.create_toolbar() + self.core_editbar = CoreToolbar(self, self) + self.core_editbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) def draw_canvas(self): self.canvas = CanvasGraph( diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 51a78f76..553b0949 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -42,7 +42,10 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) - config = {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]} + config = { + "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], + "nodes": [], + } save_config(config) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 318ccb99..96d613fd 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,12 +8,13 @@ from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig +from coretk.images import Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter"] class Node: @@ -64,6 +65,14 @@ class CoreServer: self.port = port +class CustomNode: + def __init__(self, name, image, image_file, services): + self.name = name + self.image = image + self.image_file = image_file + self.services = services + + class CoreClient: def __init__(self, app): """ @@ -77,13 +86,10 @@ class CoreClient: self.interface_helper = None self.services = {} - # distributed server data + # loaded configuration data self.servers = {} - for server_config in self.app.config["servers"]: - server = CoreServer( - server_config["name"], server_config["address"], server_config["port"] - ) - self.servers[server.name] = server + self.custom_nodes = {} + self.read_config() # data for managing the current session self.nodes = {} @@ -99,6 +105,23 @@ class CoreClient: self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None + def read_config(self): + # read distributed server + for server_config in self.app.config["servers"]: + server = CoreServer( + server_config["name"], server_config["address"], server_config["port"] + ) + self.servers[server.name] = server + + # read custom nodes + for node in self.app.config["nodes"]: + image_file = node["image"] + image = Images.get_custom(image_file) + custom_node = CustomNode( + node["name"], image, image_file, set(node["services"]) + ) + self.custom_nodes[custom_node.name] = custom_node + def handle_events(self, event): logging.info("event: %s", event) if event.link_event is not None: @@ -185,11 +208,9 @@ class CoreClient: # draw tool bar appropritate with session state if session_state == core_pb2.SessionState.RUNTIME: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_runtime_toolbar() + self.app.core_editbar.runtime_frame.tkraise() else: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_toolbar() + self.app.core_editbar.design_frame.tkraise() def create_new_session(self): """ @@ -217,7 +238,9 @@ class CoreClient: s = self.client.get_session(sid).session # delete links and nodes from running session if s.state == core_pb2.SessionState.RUNTIME: - self.set_session_state("datacollect", sid) + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DATACOLLECT + ) self.delete_links(sid) self.delete_nodes(sid) self.delete_session(sid) @@ -251,48 +274,6 @@ class CoreClient: # logging.info("get session: %s", response) return response.session.state - def set_session_state(self, state, custom_session_id=None): - """ - Set session state - - :param str state: session state to set - :return: nothing - """ - if custom_session_id is None: - sid = self.session_id - else: - sid = custom_session_id - - response = None - if state == "configuration": - response = self.client.set_session_state( - sid, core_pb2.SessionState.CONFIGURATION - ) - elif state == "instantiation": - response = self.client.set_session_state( - sid, core_pb2.SessionState.INSTANTIATION - ) - elif state == "datacollect": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DATACOLLECT - ) - elif state == "shutdown": - response = self.client.set_session_state( - sid, core_pb2.SessionState.SHUTDOWN - ) - elif state == "runtime": - response = self.client.set_session_state(sid, core_pb2.SessionState.RUNTIME) - elif state == "definition": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DEFINITION - ) - elif state == "none": - response = self.client.set_session_state(sid, core_pb2.SessionState.NONE) - else: - logging.error("coregrpc.py: set_session_state: INVALID STATE") - - logging.info("set session state: %s", response) - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.client.edit_node(self.session_id, node_id, position) @@ -451,6 +432,8 @@ class CoreClient: node_type = core_pb2.NodeType.WIRELESS_LAN elif name == "rj45": node_type = core_pb2.NodeType.RJ45 + elif name == "emane": + node_type = core_pb2.NodeType.EMANE elif name == "tunnel": node_type = core_pb2.NodeType.TUNNEL elif name == "emane": @@ -459,7 +442,7 @@ class CoreClient: node_type = core_pb2.NodeType.DEFAULT node_model = name else: - logging.error("grpcmanagemeny.py INVALID node name") + logging.error("invalid node name: %s", name) nid = self.get_id() create_node = Node(session_id, nid, node_type, node_model, x, y, name) @@ -473,9 +456,8 @@ class CoreClient: self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + "Adding node to core.. session id: %s, coords: (%s, %s), name: %s", session_id, x, y, diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index aeccb54f..aaed37a9 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from functools import partial from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.dialogs.customnodes import CustomNodesDialog @@ -8,21 +9,20 @@ from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -class CoreToolbar(object): +class CoreToolbar(tk.Frame): """ Core toolbar class """ - def __init__(self, app, edit_frame, menubar): + def __init__(self, master, app, cnf={}, **kwargs): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ + super().__init__(master, cnf, **kwargs) self.app = app self.master = app.master - self.edit_frame = edit_frame - self.menubar = menubar self.radio_value = tk.IntVar() self.exec_radio_value = tk.IntVar() @@ -30,44 +30,144 @@ class CoreToolbar(object): self.width = 32 self.height = 32 - self.selection_tool_button = None - # Reference to the option menus + self.selection_tool_button = None self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None - self.canvas = None + self.node_button = None + self.network_button = None + self.annotation_button = None - def destroy_previous_frame(self): - """ - Destroy any extra frame from previous before drawing a new one + # frames + self.design_frame = None + self.runtime_frame = None + self.node_picker = None + self.network_picker = None + self.annotation_picker = None - :return: nothing - """ - if ( - self.network_layer_option_menu - and self.network_layer_option_menu.winfo_exists() - ): - self.network_layer_option_menu.destroy() - if self.link_layer_option_menu and self.link_layer_option_menu.winfo_exists(): - self.link_layer_option_menu.destroy() - if self.marker_option_menu and self.marker_option_menu.winfo_exists(): - self.marker_option_menu.destroy() + # draw components + self.draw() - def destroy_children_widgets(self): - """ - Destroy all children of a parent widget + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_design_frame() + self.draw_runtime_frame() + self.design_frame.tkraise() - :param tkinter.Frame parent: parent frame - :return: nothing - """ + def draw_design_frame(self): + self.design_frame = tk.Frame(self) + self.design_frame.grid(row=0, column=0, sticky="nsew") + self.design_frame.columnconfigure(0, weight=1) - for i in self.edit_frame.winfo_children(): - if i.winfo_name() != "!frame": - i.destroy() + self.create_regular_button( + self.design_frame, + Images.get(ImageEnum.START), + self.click_start_session_tool, + "start the session", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.radio_value, + 1, + "selection tool", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.LINK), + self.click_link_tool, + self.radio_value, + 2, + "link tool", + ) + self.create_node_button() + self.create_link_layer_button() + self.create_marker_button() + self.radio_value.set(1) - def create_button(self, img, func, frame, main_button, btt_message): + def draw_runtime_frame(self): + self.runtime_frame = tk.Frame(self) + self.runtime_frame.grid(row=0, column=0, sticky="nsew") + self.runtime_frame.columnconfigure(0, weight=1) + + self.create_regular_button( + self.runtime_frame, + Images.get(ImageEnum.STOP), + self.click_stop_button, + "stop the session", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.exec_radio_value, + 1, + "selection tool", + ) + self.create_observe_button() + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.PLOT), + self.click_plot_button, + self.exec_radio_value, + 2, + "plot", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.MARKER), + self.click_marker_button, + self.exec_radio_value, + 3, + "marker", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.TWONODE), + self.click_two_node_button, + self.exec_radio_value, + 4, + "run command from one node to another", + ) + self.create_regular_button( + self.runtime_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" + ) + self.exec_radio_value.set(1) + + def draw_node_picker(self): + self.hide_pickers() + self.node_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.ROUTER, "router"), + (ImageEnum.HOST, "host"), + (ImageEnum.PC, "PC"), + (ImageEnum.MDR, "mdr"), + (ImageEnum.PROUTER, "prouter"), + (ImageEnum.EDITNODE, "custom node types"), + ] + for image_enum, tooltip in nodes: + self.create_button( + Images.get(image_enum), + partial(self.update_button, self.node_button, image_enum, tooltip), + self.node_picker, + tooltip, + ) + self.show_picker(self.node_button, self.node_picker) + + def show_picker(self, button, picker): + first_button = self.winfo_children()[0] + x = button.winfo_rootx() - first_button.winfo_rootx() + 40 + y = button.winfo_rooty() - first_button.winfo_rooty() - 1 + picker.place(x=x, y=y) + self.app.bind_all("", lambda e: self.hide_pickers()) + self.wait_window(picker) + self.app.unbind_all("") + + def create_button(self, img, func, frame, tooltip): """ Create button and put it on the frame @@ -78,9 +178,9 @@ class CoreToolbar(object): :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=img) + button.bind("", lambda e: func()) button.pack(side=tk.LEFT, pady=1) - CreateToolTip(button, btt_message) - button.bind("", lambda mb: func(main_button)) + CreateToolTip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( @@ -93,326 +193,108 @@ class CoreToolbar(object): variable=variable, command=func, ) - button.pack(side=tk.TOP, pady=1) + button.grid() CreateToolTip(button, tooltip_msg) - def create_regular_button(self, frame, image, func, btt_message): + def create_regular_button(self, frame, image, func, tooltip): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) - button.pack(side=tk.TOP, pady=1) - CreateToolTip(button, btt_message) - - def draw_button_menu_frame(self, edit_frame, option_frame, main_button): - """ - Draw option menu frame right next to the main button - - :param tkinter.Frame edit_frame: parent frame of the main button - :param tkinter.Frame option_frame: option frame to draw - :param tkinter.Radiobutton main_button: the main button - :return: nothing - """ - - first_button = edit_frame.winfo_children()[0] - _x = main_button.winfo_rootx() - first_button.winfo_rootx() + 40 - _y = main_button.winfo_rooty() - first_button.winfo_rooty() - 1 - option_frame.place(x=_x, y=_y) - - def bind_widgets_before_frame_hide(self, frame): - """ - Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before - any further action is performed - - :param tkinter.Frame frame: the frame to be destroyed - :return: nothing - """ - self.menubar.bind("", lambda e: frame.destroy()) - self.master.bind("", lambda e: frame.destroy()) - - def unbind_widgets_after_frame_hide(self): - """ - Unbind the widgets to make sure everything works normally again after the menu option frame is destroyed - - :return: nothing - """ - self.master.unbind("") - self.menubar.unbind("Button-1>") + button.grid() + CreateToolTip(button, tooltip) def click_selection_tool(self): - logging.debug("Click SELECTION TOOL") + logging.debug("clicked selection tool") self.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): """ - Start session handler: redraw buttons, send node and link messages to grpc server + Start session handler redraw buttons, send node and link messages to grpc + server. :return: nothing """ - logging.debug("Click START STOP SESSION button") + logging.debug("clicked start button") helper = CoreToolbarHelp(self.app) - self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT - - # set configuration state - # state = self.canvas.core_grpc.get_session_state() - # if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: - # self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) - # self.application.is_open_xml = False - # - # self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - # helper.add_nodes() - # helper.add_edges() - # self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) helper.gui_start_session() - self.create_runtime_toolbar() - - # for node in self.canvas.grpc_manager.nodes.values(): - # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) - # self.canvas.core_grpc.add_node( - # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - # ) - - # print(len(self.canvas.grpc_manager.edges)) - # for edge in self.canvas.grpc_manager.edges.values(): - # print(edge.id1, edge.id2, edge.type1, edge.type2) - # self.canvas.core_grpc.add_link( - # edge.id1, edge.id2, edge.type1, edge.type2, edge - # ) - # self.canvas.core_grpc.get_session() - # self.application.is_open_xml = False + self.runtime_frame.tkraise() def click_link_tool(self): logging.debug("Click LINK button") self.canvas.mode = GraphMode.EDGE - def pick_router(self, main_button): - logging.debug("Pick router option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.ROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER) - self.canvas.draw_node_name = "router" + def update_button(self, button, image_enum, name): + logging.info("update button(%s): %s, %s", button, image_enum, name) + self.hide_pickers() + if image_enum == ImageEnum.EDITNODE: + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + else: + image = Images.get(image_enum) + logging.info("updating button(%s): %s", button, name) + button.configure(image=image) + self.canvas.mode = GraphMode.NODE + self.canvas.draw_node_image = image + self.canvas.draw_node_name = name - def pick_host(self, main_button): - logging.debug("Pick host option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HOST)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HOST) - self.canvas.draw_node_name = "host" + def hide_pickers(self): + logging.info("hiding pickers") + if self.node_picker: + self.node_picker.destroy() + self.node_picker = None + if self.network_picker: + self.network_picker.destroy() + self.network_picker = None + if self.annotation_picker: + self.annotation_picker.destroy() + self.annotation_picker = None - def pick_pc(self, main_button): - logging.debug("Pick PC option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PC)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PC) - self.canvas.draw_node_name = "PC" - - def pick_mdr(self, main_button): - logging.debug("Pick MDR option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MDR)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MDR) - self.canvas.draw_node_name = "mdr" - - def pick_prouter(self, main_button): - logging.debug("Pick prouter option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER) - self.canvas.draw_node_name = "prouter" - - def pick_ovs(self, main_button): - logging.debug("Pick OVS option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVS)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.OVS) - self.canvas.draw_node_name = "OVS" - - def pick_editnode(self, main_button): - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EDITNODE)) - logging.debug("Pick editnode option") - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - - def draw_network_layer_options(self, network_layer_button): - """ - Draw the options for network-layer button - - :param tkinter.Radiobutton network_layer_button: network-layer button - :return: nothing - """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.ROUTER), - Images.get(ImageEnum.HOST), - Images.get(ImageEnum.PC), - Images.get(ImageEnum.MDR), - Images.get(ImageEnum.PROUTER), - Images.get(ImageEnum.OVS), - Images.get(ImageEnum.EDITNODE), - ] - func_list = [ - self.pick_router, - self.pick_host, - self.pick_pc, - self.pick_mdr, - self.pick_prouter, - self.pick_ovs, - self.pick_editnode, - ] - tooltip_list = [ - "router", - "host", - "PC", - "mdr", - "prouter", - "OVS", - "edit node types", - ] - for i in range(len(img_list)): - self.create_button( - img_list[i], - func_list[i], - option_frame, - network_layer_button, - tooltip_list[i], - ) - - # place frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, network_layer_button) - self.network_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() - - def create_network_layer_button(self): + def create_node_button(self): """ Create network layer button :return: nothing """ router_image = Images.get(ImageEnum.ROUTER) - network_layer_button = tk.Radiobutton( - self.edit_frame, + self.node_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=3, width=self.width, height=self.height, image=router_image, - command=lambda: self.draw_network_layer_options(network_layer_button), + command=self.draw_node_picker, ) - network_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(network_layer_button, "Network-layer virtual nodes") + self.node_button.grid() + CreateToolTip(self.node_button, "Network-layer virtual nodes") - def pick_hub(self, main_button): - logging.debug("Pick link-layer node HUB") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HUB)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HUB) - self.canvas.draw_node_name = "hub" - - def pick_switch(self, main_button): - logging.debug("Pick link-layer node SWITCH") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.SWITCH)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH) - self.canvas.draw_node_name = "switch" - - def pick_wlan(self, main_button): - logging.debug("Pick link-layer node WLAN") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.WLAN)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.WLAN) - self.canvas.draw_node_name = "wlan" - - def pick_rj45(self, main_button): - logging.debug("Pick link-layer node RJ45") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RJ45)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.RJ45) - self.canvas.draw_node_name = "rj45" - - def pick_tunnel(self, main_button): - logging.debug("Pick link-layer node TUNNEL") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TUNNEL)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL) - self.canvas.draw_node_name = "tunnel" - - def pick_emane(self, main_button): - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EMANE)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.EMANE) - self.canvas.draw_node_name = "emane" - - def draw_link_layer_options(self, link_layer_button): + def draw_network_picker(self): """ Draw the options for link-layer button :param tkinter.RadioButton link_layer_button: link-layer button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.HUB), - Images.get(ImageEnum.SWITCH), - Images.get(ImageEnum.WLAN), - Images.get(ImageEnum.EMANE), - Images.get(ImageEnum.RJ45), - Images.get(ImageEnum.TUNNEL), + self.hide_pickers() + self.network_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.HUB, "hub", "ethernet hub"), + (ImageEnum.SWITCH, "switch", "ethernet switch"), + (ImageEnum.WLAN, "wlan", "wireless LAN"), + (ImageEnum.EMANE, "emane", "EMANE"), + (ImageEnum.RJ45, "rj45", "rj45 physical interface tool"), + (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] - func_list = [ - self.pick_hub, - self.pick_switch, - self.pick_wlan, - self.pick_emane, - self.pick_rj45, - self.pick_tunnel, - ] - tooltip_list = [ - "ethernet hub", - "ethernet switch", - "wireless LAN", - "emane", - "rj45 physical interface tool", - "tunnel tool", - ] - for i in range(len(img_list)): + for image_enum, name, tooltip in nodes: self.create_button( - img_list[i], - func_list[i], - option_frame, - link_layer_button, - tooltip_list[i], + Images.get(image_enum), + partial(self.update_button, self.network_button, image_enum, name), + self.network_picker, + tooltip, ) - - # place frame at a calculated position as well as keep a reference of the frame - self.draw_button_menu_frame(self.edit_frame, option_frame, link_layer_button) - self.link_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.network_button, self.network_picker) def create_link_layer_button(self): """ @@ -421,75 +303,42 @@ class CoreToolbar(object): :return: nothing """ hub_image = Images.get(ImageEnum.HUB) - link_layer_button = tk.Radiobutton( - self.edit_frame, + self.network_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=4, width=self.width, height=self.height, image=hub_image, - command=lambda: self.draw_link_layer_options(link_layer_button), + command=self.draw_network_picker, ) - link_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(link_layer_button, "link-layer nodes") + self.network_button.grid() + CreateToolTip(self.network_button, "link-layer nodes") - def pick_marker(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MARKER)) - logging.debug("Pick MARKER") - - def pick_oval(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVAL)) - logging.debug("Pick OVAL") - - def pick_rectangle(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RECTANGLE)) - logging.debug("Pick RECTANGLE") - - def pick_text(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TEXT)) - logging.debug("Pick TEXT") - - def draw_marker_options(self, main_button): + def draw_annotation_picker(self): """ Draw the options for marker button :param tkinter.Radiobutton main_button: the main button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.MARKER), - Images.get(ImageEnum.OVAL), - Images.get(ImageEnum.RECTANGLE), - Images.get(ImageEnum.TEXT), + self.hide_pickers() + self.annotation_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.MARKER, "marker"), + (ImageEnum.OVAL, "oval"), + (ImageEnum.RECTANGLE, "rectangle"), + (ImageEnum.TEXT, "text"), ] - func_list = [ - self.pick_marker, - self.pick_oval, - self.pick_rectangle, - self.pick_text, - ] - tooltip_list = ["marker", "oval", "rectangle", "text"] - for i in range(len(img_list)): + for image_enum, tooltip in nodes: self.create_button( - img_list[i], func_list[i], option_frame, main_button, tooltip_list[i] + Images.get(image_enum), + partial(self.update_annotation, image_enum), + self.annotation_picker, + tooltip, ) - - # place the frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) - self.marker_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.annotation_button, self.annotation_picker) def create_marker_button(self): """ @@ -498,55 +347,22 @@ class CoreToolbar(object): :return: nothing """ marker_image = Images.get(ImageEnum.MARKER) - marker_main_button = tk.Radiobutton( - self.edit_frame, + self.annotation_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=5, width=self.width, height=self.height, image=marker_image, - command=lambda: self.draw_marker_options(marker_main_button), + command=self.draw_annotation_picker, ) - marker_main_button.pack(side=tk.TOP, pady=1) - CreateToolTip(marker_main_button, "background annotation tools") - - def create_toolbar(self): - """ - Create buttons for toolbar in edit mode - - :return: nothing - """ - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.START), - self.click_start_session_tool, - "start the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.radio_value, - 1, - "selection tool", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.LINK), - self.click_link_tool, - self.radio_value, - 2, - "link tool", - ) - self.create_network_layer_button() - self.create_link_layer_button() - self.create_marker_button() - self.radio_value.set(1) + self.annotation_button.grid() + CreateToolTip(self.annotation_button, "background annotation tools") def create_observe_button(self): menu_button = tk.Menubutton( - self.edit_frame, + self.runtime_frame, image=Images.get(ImageEnum.OBSERVE), width=self.width, height=self.height, @@ -555,7 +371,7 @@ class CoreToolbar(object): ) menu_button.menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu_button.menu - menu_button.pack(side=tk.TOP, pady=1) + menu_button.grid() menu_button.menu.add_command(label="None") menu_button.menu.add_command(label="processes") @@ -581,9 +397,13 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click on STOP button ") - self.destroy_children_widgets() self.app.core.stop_session() - self.create_toolbar() + self.design_frame.tkraise() + + def update_annotation(self, image_enum): + logging.info("clicked annotation: ") + self.hide_pickers() + self.annotation_button.configure(image=Images.get(image_enum)) def click_run_button(self): logging.debug("Click on RUN button") @@ -596,48 +416,3 @@ class CoreToolbar(object): def click_two_node_button(self): logging.debug("Click TWONODE button") - - def create_runtime_toolbar(self): - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.STOP), - self.click_stop_button, - "stop the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.exec_radio_value, - 1, - "selection tool", - ) - self.create_observe_button() - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.PLOT), - self.click_plot_button, - self.exec_radio_value, - 2, - "plot", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.MARKER), - self.click_marker_button, - self.exec_radio_value, - 3, - "marker", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.TWONODE), - self.click_two_node_button, - self.exec_radio_value, - 4, - "run command from one node to another", - ) - self.create_regular_button( - self.edit_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" - ) - self.exec_radio_value.set(1) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 38d59c83..f2423bf0 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,17 +1,21 @@ +import logging import tkinter as tk +from pathlib import Path +from coretk import appdirs +from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.widgets import CheckboxList, ListboxScroll class ServicesSelectDialog(Dialog): - def __init__(self, master, app): + 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() + self.current_services = current_services self.draw() def draw(self): @@ -37,14 +41,16 @@ class ServicesSelectDialog(Dialog): self.current = ListboxScroll(frame, text="Selected") self.current.grid(row=0, column=2, sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) frame = tk.Frame(self) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save") + button = tk.Button(frame, text="Save", command=self.click_cancel) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") # trigger group change @@ -69,15 +75,24 @@ class ServicesSelectDialog(Dialog): 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.save_button = None + 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): @@ -93,13 +108,11 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - listbox = tk.Listbox(frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set) - listbox.grid(row=0, column=0, sticky="nsew") - - scrollbar.config(command=listbox.yview) + self.nodes_list = ListboxScroll(frame) + self.nodes_list.grid(row=0, column=0, sticky="nsew") + self.nodes_list.listbox.bind("<>", self.handle_node_select) + for name in sorted(self.app.core.custom_nodes): + self.nodes_list.listbox.insert(tk.END, name) frame = tk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") @@ -120,10 +133,10 @@ class CustomNodesDialog(Dialog): button = tk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( - frame, text="Save", state=tk.DISABLED, command=self.click_save + self.edit_button = tk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew") self.delete_button = tk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -142,22 +155,89 @@ class CustomNodesDialog(Dialog): button = tk.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): dialog = IconDialog(self, self.app, self.name.get(), self.image) dialog.show() if dialog.image: self.image = dialog.image + self.image_file = dialog.file_path.get() self.image_button.config(image=self.image) def click_services(self): - dialog = ServicesSelectDialog(self, self.app) + dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() - - def click_create(self): - pass + if dialog.current_services is not None: + self.services = dialog.current_services def click_save(self): - pass + self.app.config["nodes"].clear() + for name in sorted(self.app.core.custom_nodes): + custom_node = self.app.core.custom_nodes[name] + self.app.config["nodes"].append( + { + "name": custom_node.name, + "image": custom_node.image_file, + "services": list(custom_node.services), + } + ) + logging.info("saving custom nodes: %s", self.app.config["nodes"]) + appdirs.save_config(self.app.config) + + def click_create(self): + name = self.name.get() + if name not in self.app.core.custom_nodes: + custom_node = CustomNode( + name, self.image, Path(self.image_file).name, set(self.services) + ) + self.app.core.custom_nodes[name] = custom_node + 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 + custom_node = self.app.core.custom_nodes.pop(previous_name) + custom_node.name = name + custom_node.image = self.image + custom_node.image_file = Path(self.image_file).name + custom_node.services = self.services + self.app.core.custom_nodes[name] = custom_node + 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): - pass + 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("<>") + + 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) + custom_node = self.app.core.custom_nodes[self.selected] + self.name.set(custom_node.name) + self.services = custom_node.services + self.image = custom_node.image + self.image_file = custom_node.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) diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/icondialog.py similarity index 100% rename from coretk/coretk/dialogs/nodeicon.py rename to coretk/coretk/dialogs/icondialog.py diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index bc03dc51..3f13488a 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index ecd2610b..78dc2c22 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog class WlanConfigDialog(Dialog): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index af769766..19491180 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -278,7 +278,7 @@ class CanvasGraph(tk.Canvas): else: self.focus_set() self.selected = self.get_selected(event) - logging.debug(f"click release selected: {self.selected}") + 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: @@ -416,6 +416,7 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] + logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: node = CanvasNode( x=x, @@ -539,13 +540,13 @@ class CanvasNode: self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): - logging.debug(f"click press {self.name}: {event}") + logging.debug(f"node click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) self.canvas.canvas_management.node_select(self) def click_release(self, event): - logging.debug(f"click release {self.name}: {event}") + logging.debug(f"node click release {self.name}: {event}") self.update_coords() self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 768b33ba..f25b0eb1 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -29,6 +29,10 @@ class Images: def get(cls, image): return cls.images[image.value] + @classmethod + def get_custom(cls, name): + return cls.images[name] + @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): """