import logging import time import tkinter as tk from enum import Enum from functools import partial from tkinter import ttk from typing import TYPE_CHECKING, Callable from core.api.grpc import core_pb2 from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.marker import MarkerDialog from core.gui.graph.enums import GraphMode from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.images import ImageEnum, Images from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.task import BackgroundTask from core.gui.themes import Styles from core.gui.tooltip import Tooltip if TYPE_CHECKING: from core.gui.app import Application from PIL import ImageTk TOOLBAR_SIZE = 32 PICKER_SIZE = 24 class NodeTypeEnum(Enum): NODE = 0 NETWORK = 1 OTHER = 2 def icon(image_enum, width=TOOLBAR_SIZE): return Images.get(image_enum, width) class Toolbar(ttk.Frame): """ Core toolbar class """ def __init__(self, master: "Application", app: "Application", **kwargs): """ Create a CoreToolbar instance """ super().__init__(master, **kwargs) self.app = app self.master = app.master self.time = None # design buttons self.play_button = None self.select_button = None self.link_button = None self.node_button = None self.network_button = None self.annotation_button = None # runtime buttons self.runtime_select_button = None self.stop_button = None self.plot_button = None self.runtime_marker_button = None self.node_command_button = None self.run_command_button = None # frames self.design_frame = None self.runtime_frame = None self.node_picker = None self.network_picker = None self.annotation_picker = None # dialog self.marker_tool = None # these variables help keep track of what images being drawn so that scaling is possible # since ImageTk.PhotoImage does not have resize method self.node_enum = None self.network_enum = None self.annotation_enum = None # draw components self.draw() def get_icon(self, image_enum, width=TOOLBAR_SIZE): return Images.get(image_enum, int(width * self.app.app_scale)) def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.draw_design_frame() self.draw_runtime_frame() self.design_frame.tkraise() def draw_design_frame(self): self.design_frame = ttk.Frame(self) self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.columnconfigure(0, weight=1) self.play_button = self.create_button( self.design_frame, self.get_icon(ImageEnum.START), self.click_start, "start the session", ) self.select_button = self.create_button( self.design_frame, self.get_icon(ImageEnum.SELECT), self.click_selection, "selection tool", ) self.link_button = self.create_button( self.design_frame, self.get_icon(ImageEnum.LINK), self.click_link, "link tool", ) self.create_node_button() self.create_network_button() self.create_annotation_button() def design_select(self, button: ttk.Button): logging.debug("selecting design button: %s", button) self.select_button.state(["!pressed"]) self.link_button.state(["!pressed"]) self.node_button.state(["!pressed"]) self.network_button.state(["!pressed"]) self.annotation_button.state(["!pressed"]) button.state(["pressed"]) def runtime_select(self, button: ttk.Button): logging.debug("selecting runtime button: %s", button) self.runtime_select_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) self.plot_button.state(["!pressed"]) self.runtime_marker_button.state(["!pressed"]) self.node_command_button.state(["!pressed"]) self.run_command_button.state(["!pressed"]) button.state(["pressed"]) def draw_runtime_frame(self): self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) self.stop_button = self.create_button( self.runtime_frame, self.get_icon(ImageEnum.STOP), self.click_stop, "stop the session", ) self.runtime_select_button = self.create_button( self.runtime_frame, self.get_icon(ImageEnum.SELECT), self.click_runtime_selection, "selection tool", ) self.plot_button = self.create_button( self.runtime_frame, self.get_icon(ImageEnum.PLOT), self.click_plot_button, "plot", ) self.runtime_marker_button = self.create_button( self.runtime_frame, icon(ImageEnum.MARKER), self.click_marker_button, "marker", ) self.node_command_button = self.create_button( self.runtime_frame, icon(ImageEnum.TWONODE), self.click_two_node_button, "run command from one node to another", ) self.run_command_button = self.create_button( self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" ) def draw_node_picker(self): self.hide_pickers() self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE) func = partial( self.update_button, self.node_button, toolbar_image, node_draw, NodeTypeEnum.NODE, node_draw.image_enum, ) self.create_picker_button(image, func, self.node_picker, node_draw.label) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] toolbar_image = Images.get_custom( node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale) ) image = Images.get_custom( node_draw.image_file, int(PICKER_SIZE * self.app.app_scale) ) func = partial( self.update_button, self.node_button, toolbar_image, node_draw, NodeTypeEnum, node_draw.image_file, ) self.create_picker_button(image, func, self.node_picker, name) # draw edit node # image = icon(ImageEnum.EDITNODE, PICKER_SIZE) image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE) self.create_picker_button( image, self.click_edit_node, self.node_picker, "Custom" ) self.design_select(self.node_button) self.node_button.after( 0, lambda: self.show_picker(self.node_button, self.node_picker) ) def show_picker(self, button: ttk.Button, picker: ttk.Frame): x = self.winfo_width() + 1 y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 picker.place(x=x, y=y) self.app.bind_all("", lambda e: self.hide_pickers()) picker.wait_visibility() picker.grab_set() self.wait_window(picker) self.app.unbind_all("") def create_picker_button( self, image: "ImageTk.PhotoImage", func: Callable, frame: ttk.Frame, label: str ): """ Create button and put it on the frame :param image: button image :param func: the command that is executed when button is clicked :param frame: frame that contains the button :param label: button label """ button = ttk.Button( frame, image=image, text=label, compound=tk.TOP, style=Styles.picker_button ) button.image = image button.bind("", lambda e: func()) button.grid(pady=1) def create_button( self, frame: ttk.Frame, image: "ImageTk.PhotoImage", func: Callable, tooltip: str, ): button = ttk.Button(frame, image=image, command=func) button.image = image button.grid(sticky="ew") Tooltip(button, tooltip) return button def click_selection(self): logging.debug("clicked selection tool") self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT def click_runtime_selection(self): logging.debug("clicked selection tool") self.runtime_select(self.runtime_select_button) self.app.canvas.mode = GraphMode.SELECT def click_start(self): """ Start session handler redraw buttons, send node and link messages to grpc server. """ self.app.canvas.hide_context() self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT self.time = time.perf_counter() task = BackgroundTask(self, self.app.core.start_session, self.start_callback) task.start() def start_callback(self, response: core_pb2.StartSessionResponse): self.app.statusbar.progress_bar.stop() total = time.perf_counter() - self.time message = f"Start ran for {total:.3f} seconds" self.app.statusbar.set_status(message) self.time = None if response.result: self.set_runtime() self.app.core.set_metadata() self.app.core.show_mobility_players() def set_runtime(self): self.runtime_frame.tkraise() self.click_runtime_selection() def set_design(self): self.design_frame.tkraise() self.click_selection() def click_link(self): logging.debug("Click LINK button") self.design_select(self.link_button) self.app.canvas.mode = GraphMode.EDGE def click_edit_node(self): self.hide_pickers() dialog = CustomNodesDialog(self.app, self.app) dialog.show() def update_button( self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw, type_enum, image_enum, ): logging.debug("update button(%s): %s", button, node_draw) self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.node_draw = node_draw if type_enum == NodeTypeEnum.NODE: self.node_enum = image_enum elif type_enum == NodeTypeEnum.NETWORK: self.network_enum = image_enum def hide_pickers(self): logging.debug("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 create_node_button(self): """ Create network layer button """ image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE) self.node_button = ttk.Button( self.design_frame, image=image, command=self.draw_node_picker ) self.node_button.image = image self.node_button.grid(sticky="ew") Tooltip(self.node_button, "Network-layer virtual nodes") self.node_enum = ImageEnum.ROUTER def draw_network_picker(self): """ Draw the options for link-layer button. """ self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, partial( self.update_button, self.network_button, toolbar_image, node_draw, NodeTypeEnum.NETWORK, node_draw.image_enum, ), self.network_picker, node_draw.label, ) self.design_select(self.network_button) self.network_button.after( 0, lambda: self.show_picker(self.network_button, self.network_picker) ) def create_network_button(self): """ Create link-layer node button and the options that represent different link-layer node types. """ image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE) self.network_button = ttk.Button( self.design_frame, image=image, command=self.draw_network_picker ) self.network_button.image = image self.network_button.grid(sticky="ew") Tooltip(self.network_button, "link-layer nodes") self.network_enum = ImageEnum.HUB def draw_annotation_picker(self): """ Draw the options for marker button. """ self.hide_pickers() self.annotation_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.MARKER, ShapeType.MARKER), (ImageEnum.OVAL, ShapeType.OVAL), (ImageEnum.RECTANGLE, ShapeType.RECTANGLE), (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE) image = self.get_icon(image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_annotation, toolbar_image, shape_type, image_enum), self.annotation_picker, shape_type.value, ) self.design_select(self.annotation_button) self.annotation_button.after( 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) ) def create_annotation_button(self): """ Create marker button and options that represent different marker types """ image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE) self.annotation_button = ttk.Button( self.design_frame, image=image, command=self.draw_annotation_picker ) self.annotation_button.image = image self.annotation_button.grid(sticky="ew") Tooltip(self.annotation_button, "background annotation tools") self.annotation_enum = ImageEnum.MARKER def create_observe_button(self): menu_button = ttk.Menubutton( self.runtime_frame, image=icon(ImageEnum.OBSERVE), direction=tk.RIGHT ) menu_button.grid(sticky="ew") menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu menu.add_command(label="None") menu.add_command(label="processes") menu.add_command(label="ifconfig") menu.add_command(label="IPv4 routes") menu.add_command(label="IPv6 routes") menu.add_command(label="OSPFv2 neighbors") menu.add_command(label="OSPFv3 neighbors") menu.add_command(label="Listening sockets") menu.add_command(label="IPv4 MFC entries") menu.add_command(label="IPv6 MFC entries") menu.add_command(label="firewall rules") menu.add_command(label="IPSec policies") menu.add_command(label="docker logs") menu.add_command(label="OSPFv3 MDR level") menu.add_command(label="PIM neighbors") menu.add_command(label="Edit...") def click_stop(self): """ redraw buttons on the toolbar, send node and link messages to grpc server """ logging.info("Click stop button") self.app.canvas.hide_context() self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.statusbar.progress_bar.start(5) self.time = time.perf_counter() task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback) task.start() def stop_callback(self, response: core_pb2.StopSessionResponse): self.app.statusbar.progress_bar.stop() self.set_design() total = time.perf_counter() - self.time message = f"Stopped in {total:.3f} seconds" self.app.statusbar.set_status(message) self.app.canvas.stopped_session() def update_annotation( self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum ): logging.debug("clicked annotation: ") self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type self.annotation_enum = image_enum if is_marker(shape_type): if self.marker_tool: self.marker_tool.destroy() self.marker_tool = MarkerDialog(self.app, self.app) self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") def click_plot_button(self): logging.debug("Click on plot button") def click_marker_button(self): logging.debug("Click on marker button") self.runtime_select(self.runtime_marker_button) self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = ShapeType.MARKER if self.marker_tool: self.marker_tool.destroy() self.marker_tool = MarkerDialog(self.app, self.app) self.marker_tool.show() def click_two_node_button(self): logging.debug("Click TWONODE button") # def scale_button(cls, button, image_enum, scale): def scale_button(self, button, image_enum): image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale)) button.config(image=image) button.image = image def scale(self): self.scale_button(self.play_button, ImageEnum.START) self.scale_button(self.select_button, ImageEnum.SELECT) self.scale_button(self.link_button, ImageEnum.LINK) self.scale_button(self.node_button, self.node_enum) self.scale_button(self.network_button, self.network_enum) self.scale_button(self.annotation_button, self.annotation_enum) self.scale_button(self.runtime_select_button, ImageEnum.SELECT) self.scale_button(self.stop_button, ImageEnum.STOP) self.scale_button(self.plot_button, ImageEnum.PLOT) self.scale_button(self.runtime_marker_button, ImageEnum.MARKER) self.scale_button(self.node_command_button, ImageEnum.TWONODE) self.scale_button(self.run_command_button, ImageEnum.RUN)