Compare commits

...

3 commits

Author SHA1 Message Date
Huy Pham
c42d0161b0 touch up 2020-05-07 10:08:58 -07:00
Huy Pham
002caf09bf touch up 2020-05-07 10:01:03 -07:00
Huy Pham
9bb7902060 implement multiple node service configuration 2020-05-06 22:40:34 -07:00
4 changed files with 264 additions and 1 deletions

View file

@ -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) -> None:
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("<<ListboxSelect>>", 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("<<ListboxSelect>>", 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("<<ListboxSelect>>")
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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) -> None:
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()

View file

@ -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,11 @@ class CanvasNode:
self.context.add_command(
label="Config Services", command=self.show_config_services
)
if self.app.canvas.selection:
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 +300,10 @@ class CanvasNode:
dialog = NodeConfigServiceDialog(self.app, self)
dialog.show()
def multiple_node_service_config(self):
dialog = MultipleNodeServiceDialog(self.app)
dialog.show()
def has_emane_link(self, interface_id: int) -> core_pb2.Node:
result = None
for edge in self.edges:

View file

@ -31,6 +31,8 @@ class Colors:
white = "white"
black = "black"
listboxbg = "#f2f1f0"
common_services = "#008000"
noncommon_services = "#b38f00"
def load(style: ttk.Style):

View file

@ -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):