From 9bb7902060ff76299a0b23771d26e6807de0e078 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 6 May 2020 22:40:34 -0700 Subject: [PATCH] implement multiple node service configuration --- .../gui/dialogs/multinodeserviceconfig.py | 235 ++++++++++++++++++ daemon/core/gui/graph/node.py | 10 + daemon/core/gui/themes.py | 2 + daemon/core/gui/widgets.py | 18 +- 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 daemon/core/gui/dialogs/multinodeserviceconfig.py diff --git a/daemon/core/gui/dialogs/multinodeserviceconfig.py b/daemon/core/gui/dialogs/multinodeserviceconfig.py new file mode 100644 index 00000000..9e71e5a5 --- /dev/null +++ b/daemon/core/gui/dialogs/multinodeserviceconfig.py @@ -0,0 +1,235 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING, Set, Tuple + +from core.gui.dialogs.dialog import Dialog +from core.gui.nodeutils import NodeUtils +from core.gui.themes import FRAME_PAD, PADX, PADY, Colors +from core.gui.widgets import CheckboxList, ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + + +class MultipleNodeServiceDialog(Dialog): + def __init__(self, app: "Application"): + super().__init__(app, "Multiple node service config") + self.canvas = app.canvas + self.nodes = None + self.groups = None + self.services = None + self.current = None + self.current_services = set() + self.selected_nodes = {} + self.all_nodes = {} + + # maps node name to node id + self.node_names = {} + # track all the nodes that will need custom service configuration when we click save + self.custom_nodes = set() + + self.store_node_services() + self.draw() + + def store_node_services(self) -> None: + """ + create a mapping of core node canvas id to core node services + one mapping for currently selected nodes, one for all nodes + """ + + for node_id in self.canvas.selection: + core_node = self.canvas.nodes[node_id].core_node + if NodeUtils.is_container_node(core_node.type): + self.selected_nodes[core_node.id] = set(core_node.services[:]) + + for canvas_node in self.canvas.nodes.values(): + core_node = canvas_node.core_node + if NodeUtils.is_container_node(core_node.type): + self.all_nodes[core_node.id] = set(core_node.services[:]) + self.node_names[core_node.name] = core_node.id + + def common_services(self) -> Tuple[Set, Set]: + """ + find the common services of all the selected nodes and the common services + of some but not all nodes + """ + common_services = set() + non_common_services = set() + + for index, service_set in enumerate(self.selected_nodes.values()): + if index == 0: + common_services = service_set + else: + common_services = common_services.intersection(service_set) + non_common_services = non_common_services.union(service_set) + + if len(self.selected_nodes) == 1: + non_common_services = set() + else: + non_common_services = non_common_services - common_services + + return common_services, non_common_services + + def populate_node_list(self): + self.nodes.listbox.delete(0, tk.END) + for canvas_id, canvas_node in self.canvas.nodes.items(): + core_node = canvas_node.core_node + if NodeUtils.is_container_node(core_node.type): + self.nodes.listbox.insert(tk.END, canvas_node.core_node.name) + for index, name in enumerate(self.nodes.listbox.get(0, tk.END)): + if self.node_names[name] in self.selected_nodes: + self.nodes.listbox.selection_set(index) + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + frame = ttk.Frame(self.top) + frame.grid(stick="nsew", pady=PADY) + frame.rowconfigure(0, weight=1) + for i in range(4): + frame.columnconfigure(i, weight=1) + + # nodes frame + label_frame = ttk.LabelFrame(frame, text="Nodes", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.nodes = ListboxScroll(label_frame) + self.nodes.listbox.configure(selectmode=tk.MULTIPLE) + self.nodes.grid(sticky="nsew") + + self.populate_node_list() + self.nodes.listbox.bind("<>", self.handle_node_selection) + + # group frame + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") + for group in sorted(self.app.core.services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) + + # service frame + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + self.services = CheckboxList( + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD + ) + self.services.grid(sticky="nsew") + + # service frame + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=3, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") + + # buttons frame + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(5): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Turn off", command=self.turn_off) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Turn on", command=self.turn_on) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Save", command=self.save_config) + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Default", command=self.default_config) + button.grid(row=0, column=3, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=4, sticky="ew") + + # trigger group change + self.groups.listbox.event_generate("<>") + + def handle_node_selection(self, _event: tk.Event = None) -> None: + """ + update selected_nodes as user add/remove nodes to configure + """ + for index, node_name in enumerate(self.nodes.listbox.get(0, tk.END)): + node_id = self.node_names[node_name] + if self.nodes.listbox.selection_includes(index): + if node_id not in self.selected_nodes: + self.selected_nodes[node_id] = self.all_nodes[node_id] + else: + if node_id in self.selected_nodes: + self.selected_nodes.pop(node_id, None) + # update services color according to current node selection + self.handle_group_change() + + def handle_group_change(self, _event: tk.Event = None) -> None: + """ + Display a list of services that belongs to currently selected group + Service that is common to all selected nodes is colored green + Service that is common to some but not all nodes is colored yellow + """ + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + common, non_common = self.common_services() + for name in sorted(self.app.core.services[group]): + checked = name in self.current_services + color = Colors.frame + if name in common: + color = Colors.common_services + elif name in non_common: + color = Colors.noncommon_services + self.services.add_with_color(name, checked, color) + + def service_clicked(self, name: str, var: tk.IntVar): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) + + def turn_off(self): + if not self.current_services: + return + for service in self.current_services: + for node_id, service_set in self.selected_nodes.items(): + service_set.discard(service) + self.all_nodes[node_id].discard(service) + self.handle_group_change() + self.custom_nodes = self.custom_nodes.union(set(self.selected_nodes.keys())) + + def turn_on(self): + if not self.current_services: + return + for service in self.current_services: + for node_id, service_set in self.selected_nodes.items(): + service_set.add(service) + self.all_nodes[node_id].add(service) + self.handle_group_change() + self.custom_nodes = self.custom_nodes.union(set(self.selected_nodes.keys())) + + def save_config(self): + for node_id in self.custom_nodes: + self.app.core.canvas_nodes[node_id].core_node.services[:] = self.all_nodes[ + node_id + ] + self.destroy() + + def default_config(self): + self.current_services.clear() + self.all_nodes.clear() + self.custom_nodes.clear() + self.selected_nodes.clear() + + self.store_node_services() + self.populate_node_list() + self.current.listbox.delete(0, tk.END) + self.handle_group_change() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 451298e0..2e1e364c 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -10,6 +10,7 @@ from core.api.grpc.core_pb2 import NodeType from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog +from core.gui.dialogs.multinodeserviceconfig import MultipleNodeServiceDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog @@ -212,6 +213,10 @@ class CanvasNode: self.context.add_command( label="Config Services", command=self.show_config_services ) + self.context.add_command( + label="Multiple Node Service Configuration", + command=self.multiple_node_service_config, + ) if is_emane: self.context.add_command( label="EMANE Config", command=self.show_emane_config @@ -294,6 +299,11 @@ class CanvasNode: dialog = NodeConfigServiceDialog(self.app, self) dialog.show() + def multiple_node_service_config(self): + print("multiple nodes services config ") + dialog = MultipleNodeServiceDialog(self.app) + dialog.show() + def has_emane_link(self, interface_id: int) -> core_pb2.Node: result = None for edge in self.edges: diff --git a/daemon/core/gui/themes.py b/daemon/core/gui/themes.py index 141a7a5c..c45bfa59 100644 --- a/daemon/core/gui/themes.py +++ b/daemon/core/gui/themes.py @@ -31,6 +31,8 @@ class Colors: white = "white" black = "black" listboxbg = "#f2f1f0" + common_services = "#008000" + noncommon_services = "#b38f00" def load(style: ttk.Style): diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 5750e286..dc372f14 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict from core.api.grpc import common_pb2, core_pb2 from core.gui import themes -from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.themes import FRAME_PAD, PADX, PADY, Colors if TYPE_CHECKING: from core.gui.app import Application @@ -225,6 +225,22 @@ class CheckboxList(FrameScroll): checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") + def add_with_color(self, name: str, checked: bool, color: str): + var = tk.BooleanVar(value=checked) + func = partial(self.clicked, name, var) + checkbox = tk.Checkbutton( + self.frame, + text=name, + variable=var, + command=func, + background=color, + foreground="white", + selectcolor="black", + activebackground=Colors.selectbg, + highlightbackground=Colors.frame, + ) + checkbox.grid(sticky="w") + class CodeFont(font.Font): def __init__(self):