Merge pull request #371 from coreemu/coretk-enhance/scaling

Coretk enhance/scaling
This commit is contained in:
bharnden 2020-02-20 10:29:21 -08:00 committed by GitHub
commit ceb3d072da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 325 additions and 83 deletions

View file

@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import font, ttk
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -29,8 +29,15 @@ class Application(tk.Frame):
self.statusbar = None self.statusbar = None
self.validation = None self.validation = None
# fonts
self.fonts_size = None
self.icon_text_font = None
self.edge_font = None
# setup # setup
self.guiconfig = appconfig.read() self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig["scale"]
self.setup_scaling()
self.style = ttk.Style() self.style = ttk.Style()
self.setup_theme() self.setup_theme()
self.core = CoreClient(self, proxy) self.core = CoreClient(self, proxy)
@ -38,6 +45,14 @@ class Application(tk.Frame):
self.draw() self.draw()
self.core.set_up() self.core.set_up()
def setup_scaling(self):
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
themes.scale_fonts(self.fonts_size, self.app_scale)
self.icon_text_font = font.Font(
family="TkIconFont", size=int(12 * self.app_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)
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu) self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
@ -56,9 +71,11 @@ class Application(tk.Frame):
def center(self): def center(self):
screen_width = self.master.winfo_screenwidth() screen_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight() screen_height = self.master.winfo_screenheight()
x = int((screen_width / 2) - (WIDTH / 2)) x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
y = int((screen_height / 2) - (HEIGHT / 2)) y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") self.master.geometry(
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
)
def draw(self): def draw(self):
self.master.option_add("*tearOff", tk.FALSE) self.master.option_add("*tearOff", tk.FALSE)

View file

@ -96,6 +96,7 @@ def check_directory():
"nodes": [], "nodes": [],
"recentfiles": [], "recentfiles": [],
"observers": [{"name": "hello", "cmd": "echo hello"}], "observers": [{"name": "hello", "cmd": "echo hello"}],
"scale": 1.0,
} }
save(config) save(config)

View file

@ -794,7 +794,7 @@ class CoreClient:
image=image, image=image,
emane=emane, emane=emane,
) )
if NodeUtils.is_custom(model): if NodeUtils.is_custom(node_type, model):
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model) services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
node.services[:] = services node.services[:] = services
logging.info( logging.info(

View file

@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog):
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.START, width=ICON_SIZE) image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale))
self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button = ttk.Button(frame, image=image, command=self.click_play)
self.play_button.image = image self.play_button.image = image
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale))
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
self.pause_button.image = image self.pause_button.image = image
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
image = Images.get(ImageEnum.STOP, width=ICON_SIZE) image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale))
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)

View file

@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog):
if len(services) == 0: if len(services) == 0:
# not custom node type and node's services haven't been modified before # not custom node type and node's services haven't been modified before
if not NodeUtils.is_custom( if not NodeUtils.is_custom(
canvas_node.core_node.model canvas_node.core_node.type, canvas_node.core_node.model
) and not self.app.core.service_been_modified(self.node_id): ) and not self.app.core.service_been_modified(self.node_id):
services = set(self.app.core.default_services[model]) services = set(self.app.core.default_services[model])
# services of default type nodes were modified to be empty # services of default type nodes were modified to be empty

View file

@ -5,7 +5,7 @@ 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 from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -14,6 +14,7 @@ if TYPE_CHECKING:
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, master: "Application", app: "Application"): def __init__(self, master: "Application", app: "Application"):
super().__init__(master, app, "Preferences", modal=True) super().__init__(master, app, "Preferences", modal=True)
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
preferences = self.app.guiconfig["preferences"] preferences = self.app.guiconfig["preferences"]
self.editor = tk.StringVar(value=preferences["editor"]) self.editor = tk.StringVar(value=preferences["editor"])
self.theme = tk.StringVar(value=preferences["theme"]) self.theme = tk.StringVar(value=preferences["theme"])
@ -64,6 +65,26 @@ class PreferencesDialog(Dialog):
entry = ttk.Entry(frame, textvariable=self.gui3d) entry = ttk.Entry(frame, textvariable=self.gui3d)
entry.grid(row=3, column=1, sticky="ew") entry.grid(row=3, column=1, sticky="ew")
label = ttk.Label(frame, text="Scaling")
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
scale_frame = ttk.Frame(frame)
scale_frame.grid(row=4, column=1, sticky="ew")
scale_frame.columnconfigure(0, weight=1)
scale = ttk.Scale(
scale_frame,
from_=0.5,
to=5,
value=1,
orient=tk.HORIZONTAL,
variable=self.gui_scale,
)
scale.grid(row=0, column=0, sticky="ew")
entry = ttk.Entry(
scale_frame, textvariable=self.gui_scale, width=4, state="disabled"
)
entry.grid(row=0, column=1)
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")
@ -87,5 +108,27 @@ class PreferencesDialog(Dialog):
preferences["editor"] = self.editor.get() preferences["editor"] = self.editor.get()
preferences["gui3d"] = self.gui3d.get() preferences["gui3d"] = self.gui3d.get()
preferences["theme"] = self.theme.get() preferences["theme"] = self.theme.get()
self.gui_scale.set(round(self.gui_scale.get(), 2))
app_scale = self.gui_scale.get()
self.app.guiconfig["scale"] = app_scale
self.app.save_config() self.app.save_config()
self.scale_adjust()
self.destroy() self.destroy()
def scale_adjust(self):
app_scale = self.gui_scale.get()
self.app.app_scale = app_scale
self.app.master.tk.call("tk", "scaling", app_scale)
# scale fonts
scale_fonts(self.app.fonts_size, app_scale)
self.app.icon_text_font.config(size=int(12 * app_scale))
self.app.edge_font.config(size=int(8 * app_scale))
# scale application window
self.app.center()
# scale toolbar and canvas items
self.app.toolbar.scale()
self.app.canvas.scale_graph()

View file

@ -1,6 +1,5 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter.font import Font
from typing import TYPE_CHECKING, Any, Tuple from typing import TYPE_CHECKING, Any, Tuple
from core.gui import themes from core.gui import themes
@ -14,6 +13,8 @@ if TYPE_CHECKING:
TEXT_DISTANCE = 0.30 TEXT_DISTANCE = 0.30
EDGE_WIDTH = 3 EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000" EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933"
class CanvasWirelessEdge: class CanvasWirelessEdge:
@ -31,7 +32,10 @@ class CanvasWirelessEdge:
self.dst = dst self.dst = dst
self.canvas = canvas self.canvas = canvas
self.id = self.canvas.create_line( self.id = self.canvas.create_line(
*position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" *position,
tags=tags.WIRELESS_EDGE,
width=WIRELESS_WIDTH * self.canvas.app.app_scale,
fill=WIRELESS_COLOR,
) )
def delete(self): def delete(self):
@ -61,13 +65,18 @@ class CanvasEdge:
self.dst_interface = None self.dst_interface = None
self.canvas = canvas self.canvas = canvas
self.id = self.canvas.create_line( self.id = self.canvas.create_line(
x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, fill=EDGE_COLOR x1,
y1,
x2,
y2,
tags=tags.EDGE,
width=EDGE_WIDTH * self.canvas.app.app_scale,
fill=EDGE_COLOR,
) )
self.text_src = None self.text_src = None
self.text_dst = None self.text_dst = None
self.text_middle = None self.text_middle = None
self.token = None self.token = None
self.font = Font(size=8)
self.link = None self.link = None
self.asymmetric_link = None self.asymmetric_link = None
self.throughput = None self.throughput = None
@ -117,7 +126,7 @@ class CanvasEdge:
y1, y1,
text=label_one, text=label_one,
justify=tk.CENTER, justify=tk.CENTER,
font=self.font, font=self.canvas.app.edge_font,
tags=tags.LINK_INFO, tags=tags.LINK_INFO,
) )
self.text_dst = self.canvas.create_text( self.text_dst = self.canvas.create_text(
@ -125,7 +134,7 @@ class CanvasEdge:
y2, y2,
text=label_two, text=label_two,
justify=tk.CENTER, justify=tk.CENTER,
font=self.font, font=self.canvas.app.edge_font,
tags=tags.LINK_INFO, tags=tags.LINK_INFO,
) )
@ -146,7 +155,7 @@ class CanvasEdge:
if self.text_middle is None: if self.text_middle is None:
x, y = self.get_midpoint() x, y = self.get_midpoint()
self.text_middle = self.canvas.create_text( self.text_middle = self.canvas.create_text(
x, y, tags=tags.THROUGHPUT, font=self.font, text=value x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value
) )
else: else:
self.canvas.itemconfig(self.text_middle, text=value) self.canvas.itemconfig(self.text_middle, text=value)

View file

@ -7,12 +7,12 @@ from PIL import Image, ImageTk
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge
from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.enums import GraphMode, ScaleOption
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.nodeutils import EdgeUtils, NodeUtils from core.gui.nodeutils import EdgeUtils, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
@ -220,10 +220,14 @@ class CanvasGraph(tk.Canvas):
# peer to peer node is not drawn on the GUI # peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type): if NodeUtils.is_ignore_node(core_node.type):
continue continue
image = NodeUtils.node_image(core_node, self.app.guiconfig) image = NodeUtils.node_image(
core_node, self.app.guiconfig, self.app.app_scale
)
# if the gui can't find node's image, default to the "edit-node" image # if the gui can't find node's image, default to the "edit-node" image
if not image: if not image:
image = Images.get(ImageEnum.EDITNODE, ICON_SIZE) image = Images.get(
ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)
)
x = core_node.position.x x = core_node.position.x
y = core_node.position.y y = core_node.position.y
node = CanvasNode(self.master, x, y, core_node, image) node = CanvasNode(self.master, x, y, core_node, image)
@ -663,6 +667,14 @@ class CanvasGraph(tk.Canvas):
core_node = self.core.create_node( core_node = self.core.create_node(
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
) )
try:
self.node_draw.image = Images.get(
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
)
except AttributeError:
self.node_draw.image = Images.get_custom(
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
)
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node self.nodes[node.id] = node
@ -911,3 +923,28 @@ class CanvasGraph(tk.Canvas):
width=self.itemcget(edge.id, "width"), width=self.itemcget(edge.id, "width"),
fill=self.itemcget(edge.id, "fill"), fill=self.itemcget(edge.id, "fill"),
) )
def scale_graph(self):
for nid, canvas_node in self.nodes.items():
img = None
if NodeUtils.is_custom(
canvas_node.core_node.type, canvas_node.core_node.model
):
for custom_node in self.app.guiconfig["nodes"]:
if custom_node["name"] == canvas_node.core_node.model:
img = Images.get_custom(
custom_node["image"], int(ICON_SIZE * self.app.app_scale)
)
else:
image_enum = TypeToImage.get(
canvas_node.core_node.type, canvas_node.core_node.model
)
img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale))
self.itemconfig(nid, image=img)
canvas_node.image = img
canvas_node.scale_text()
canvas_node.scale_antennas()
for edge_id in self.find_withtag(tags.EDGE):
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))

View file

@ -1,6 +1,5 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import font
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import grpc import grpc
@ -17,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.errors import show_grpc_error 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.nodeutils import NodeUtils from core.gui.images import ImageEnum, Images
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
@ -42,21 +42,21 @@ class CanvasNode:
self.id = self.canvas.create_image( self.id = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
) )
text_font = font.Font(family="TkIconFont", size=12)
label_y = self._get_label_y() label_y = self._get_label_y()
self.text_id = self.canvas.create_text( self.text_id = self.canvas.create_text(
x, x,
label_y, label_y,
text=self.core_node.name, text=self.core_node.name,
tags=tags.NODE_NAME, tags=tags.NODE_NAME,
font=text_font, font=self.app.icon_text_font,
fill="#0000CD", fill="#0000CD",
) )
self.tooltip = CanvasTooltip(self.canvas) self.tooltip = CanvasTooltip(self.canvas)
self.edges = set() self.edges = set()
self.interfaces = [] self.interfaces = []
self.wireless_edges = set() self.wireless_edges = set()
self.antennae = [] self.antennas = []
self.antenna_images = {}
self.setup_bindings() self.setup_bindings()
def setup_bindings(self): def setup_bindings(self):
@ -72,33 +72,37 @@ class CanvasNode:
def add_antenna(self): def add_antenna(self):
x, y = self.canvas.coords(self.id) x, y = self.canvas.coords(self.id)
offset = len(self.antennae) * 8 offset = len(self.antennas) * 8 * self.app.app_scale
img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale))
antenna_id = self.canvas.create_image( antenna_id = self.canvas.create_image(
x - 16 + offset, x - 16 + offset,
y - 23, y - int(23 * self.app.app_scale),
anchor=tk.CENTER, anchor=tk.CENTER,
image=NodeUtils.ANTENNA_ICON, image=img,
tags=tags.ANTENNA, tags=tags.ANTENNA,
) )
self.antennae.append(antenna_id) self.antennas.append(antenna_id)
self.antenna_images[antenna_id] = img
def delete_antenna(self): def delete_antenna(self):
""" """
delete one antenna delete one antenna
""" """
logging.debug("Delete an antenna on %s", self.core_node.name) logging.debug("Delete an antenna on %s", self.core_node.name)
if self.antennae: if self.antennas:
antenna_id = self.antennae.pop() antenna_id = self.antennas.pop()
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antenna_images.pop(antenna_id, None)
def delete_antennas(self): def delete_antennas(self):
""" """
delete all antennas delete all antennas
""" """
logging.debug("Remove all antennas for %s", self.core_node.name) logging.debug("Remove all antennas for %s", self.core_node.name)
for antenna_id in self.antennae: for antenna_id in self.antennas:
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antennae.clear() self.antennas.clear()
self.antenna_images.clear()
def redraw(self): def redraw(self):
self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.id, image=self.image)
@ -108,6 +112,12 @@ class CanvasNode:
image_box = self.canvas.bbox(self.id) image_box = self.canvas.bbox(self.id)
return image_box[3] + NODE_TEXT_OFFSET return image_box[3] + NODE_TEXT_OFFSET
def scale_text(self):
text_bound = self.canvas.bbox(self.text_id)
prev_y = (text_bound[3] + text_bound[1]) / 2
new_y = self._get_label_y()
self.canvas.move(self.text_id, 0, new_y - prev_y)
def move(self, x: int, y: int): def move(self, x: int, y: int):
x, y = self.canvas.get_scaled_coords(x, y) x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id) current_x, current_y = self.canvas.coords(self.id)
@ -131,7 +141,7 @@ class CanvasNode:
self.canvas.move_selection(self.id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset)
# move antennae # move antennae
for antenna_id in self.antennae: for antenna_id in self.antennas:
self.canvas.move(antenna_id, x_offset, y_offset) self.canvas.move(antenna_id, x_offset, y_offset)
# move edges # move edges
@ -295,3 +305,17 @@ class CanvasNode:
if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr":
self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) self.canvas.create_edge(self, self.canvas.nodes[canvas_nid])
self.canvas.clear_selection() self.canvas.clear_selection()
def scale_antennas(self):
for i in range(len(self.antennas)):
antenna_id = self.antennas[i]
image = Images.get(
ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)
)
self.canvas.itemconfig(antenna_id, image=image)
self.antenna_images[antenna_id] = image
node_x, node_y = self.canvas.coords(self.id)
x, y = self.canvas.coords(antenna_id)
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
dy = node_y - int(23 * self.app.app_scale) - y
self.canvas.move(antenna_id, dx, dy)

View file

@ -3,6 +3,7 @@ from tkinter import messagebox
from PIL import Image, ImageTk from PIL import Image, ImageTk
from core.api.grpc import core_pb2
from core.gui.appconfig import LOCAL_ICONS_PATH from core.gui.appconfig import LOCAL_ICONS_PATH
@ -90,3 +91,23 @@ class ImageEnum(Enum):
SHUTDOWN = "shutdown" SHUTDOWN = "shutdown"
CANCEL = "cancel" CANCEL = "cancel"
ERROR = "error" ERROR = "error"
class TypeToImage:
type_to_image = {
(core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
(core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC,
(core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST,
(core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
(core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
(core_pb2.NodeType.HUB, ""): ImageEnum.HUB,
(core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH,
(core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
}
@classmethod
def get(cls, node_type, model):
return cls.type_to_image.get((node_type, model), None)

View file

@ -2,7 +2,7 @@ import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from core.api.grpc.core_pb2 import NodeType from core.api.grpc.core_pb2 import NodeType
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images, TypeToImage
if TYPE_CHECKING: if TYPE_CHECKING:
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
@ -96,32 +96,35 @@ class NodeUtils:
node_type: NodeType, node_type: NodeType,
model: str, model: str,
gui_config: Dict[str, List[Dict[str, str]]], gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
if model == "":
model = None image_enum = TypeToImage.get(node_type, model)
try: if image_enum:
image = cls.NODE_ICONS[(node_type, model)] return Images.get(image_enum, int(ICON_SIZE * scale))
return image else:
except KeyError:
image_stem = cls.get_image_file(gui_config, model) image_stem = cls.get_image_file(gui_config, model)
if image_stem: if image_stem:
return Images.get_with_image_file(image_stem, ICON_SIZE) return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
@classmethod @classmethod
def node_image( def node_image(
cls, core_node: "core_pb2.Node", gui_config: Dict[str, List[Dict[str, str]]] cls,
core_node: "core_pb2.Node",
gui_config: Dict[str, List[Dict[str, str]]],
scale=1.0,
) -> "ImageTk.PhotoImage": ) -> "ImageTk.PhotoImage":
image = cls.node_icon(core_node.type, core_node.model, gui_config) image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
if core_node.icon: if core_node.icon:
try: try:
image = Images.create(core_node.icon, ICON_SIZE) image = Images.create(core_node.icon, int(ICON_SIZE * scale))
except OSError: except OSError:
logging.error("invalid icon: %s", core_node.icon) logging.error("invalid icon: %s", core_node.icon)
return image return image
@classmethod @classmethod
def is_custom(cls, model: str) -> bool: def is_custom(cls, node_type: NodeType, model: str) -> bool:
return model not in cls.NODE_MODELS return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
@classmethod @classmethod
def get_custom_node_services( def get_custom_node_services(

View file

@ -29,7 +29,7 @@ class StatusBar(ttk.Frame):
def draw(self): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=7) self.columnconfigure(1, weight=5)
self.columnconfigure(2, weight=1) self.columnconfigure(2, weight=1)
self.columnconfigure(3, weight=1) self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1) self.columnconfigure(4, weight=1)

View file

@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import font, ttk
THEME_DARK = "black" THEME_DARK = "black"
PADX = (0, 5) PADX = (0, 5)
@ -176,25 +176,35 @@ def style_listbox(widget: tk.Widget):
def theme_change(event: tk.Event): def theme_change(event: tk.Event):
style = ttk.Style() style = ttk.Style()
style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) style.configure(Styles.picker_button, font="TkSmallCaptionFont")
style.configure( style.configure(
Styles.green_alert, Styles.green_alert,
background="green", background="green",
padding=0, padding=0,
relief=tk.NONE, relief=tk.NONE,
font=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
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=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
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=("TkDefaultFont", 8, "normal"), font="TkSmallCaptionFont",
) )
def scale_fonts(fonts_size, scale):
for name in font.names():
f = font.nametofont(name)
if name in fonts_size:
if name == "TkSmallCaptionFont":
f.config(size=int(fonts_size[name] * scale * 8 / 9))
else:
f.config(size=int(fonts_size[name] * scale))

View file

@ -1,9 +1,9 @@
import logging import logging
import time import time
import tkinter as tk import tkinter as tk
from enum import Enum
from functools import partial from functools import partial
from tkinter import ttk from tkinter import ttk
from tkinter.font import Font
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
@ -25,6 +25,12 @@ TOOLBAR_SIZE = 32
PICKER_SIZE = 24 PICKER_SIZE = 24
class NodeTypeEnum(Enum):
NODE = 0
NETWORK = 1
OTHER = 2
def icon(image_enum, width=TOOLBAR_SIZE): def icon(image_enum, width=TOOLBAR_SIZE):
return Images.get(image_enum, width) return Images.get(image_enum, width)
@ -43,10 +49,8 @@ class Toolbar(ttk.Frame):
self.master = app.master self.master = app.master
self.time = None self.time = None
# picker data
self.picker_font = Font(size=8)
# design buttons # design buttons
self.play_button = None
self.select_button = None self.select_button = None
self.link_button = None self.link_button = None
self.node_button = None self.node_button = None
@ -71,9 +75,18 @@ class Toolbar(ttk.Frame):
# dialog # dialog
self.marker_tool = None 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 # draw components
self.draw() 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): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
@ -85,20 +98,23 @@ class Toolbar(ttk.Frame):
self.design_frame = ttk.Frame(self) self.design_frame = ttk.Frame(self)
self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.grid(row=0, column=0, sticky="nsew")
self.design_frame.columnconfigure(0, weight=1) self.design_frame.columnconfigure(0, weight=1)
self.create_button( self.play_button = self.create_button(
self.design_frame, self.design_frame,
icon(ImageEnum.START), self.get_icon(ImageEnum.START),
self.click_start, self.click_start,
"start the session", "start the session",
) )
self.select_button = self.create_button( self.select_button = self.create_button(
self.design_frame, self.design_frame,
icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT),
self.click_selection, self.click_selection,
"selection tool", "selection tool",
) )
self.link_button = self.create_button( self.link_button = self.create_button(
self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" self.design_frame,
self.get_icon(ImageEnum.LINK),
self.click_link,
"link tool",
) )
self.create_node_button() self.create_node_button()
self.create_network_button() self.create_network_button()
@ -130,18 +146,21 @@ class Toolbar(ttk.Frame):
self.stop_button = self.create_button( self.stop_button = self.create_button(
self.runtime_frame, self.runtime_frame,
icon(ImageEnum.STOP), self.get_icon(ImageEnum.STOP),
self.click_stop, self.click_stop,
"stop the session", "stop the session",
) )
self.runtime_select_button = self.create_button( self.runtime_select_button = self.create_button(
self.runtime_frame, self.runtime_frame,
icon(ImageEnum.SELECT), self.get_icon(ImageEnum.SELECT),
self.click_runtime_selection, self.click_runtime_selection,
"selection tool", "selection tool",
) )
self.plot_button = self.create_button( self.plot_button = self.create_button(
self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" self.runtime_frame,
self.get_icon(ImageEnum.PLOT),
self.click_plot_button,
"plot",
) )
self.runtime_marker_button = self.create_button( self.runtime_marker_button = self.create_button(
self.runtime_frame, self.runtime_frame,
@ -164,23 +183,38 @@ class Toolbar(ttk.Frame):
self.node_picker = ttk.Frame(self.master) self.node_picker = ttk.Frame(self.master)
# draw default nodes # draw default nodes
for node_draw in NodeUtils.NODES: for node_draw in NodeUtils.NODES:
toolbar_image = icon(node_draw.image_enum) toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
image = icon(node_draw.image_enum, PICKER_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
func = partial( func = partial(
self.update_button, self.node_button, toolbar_image, node_draw 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) self.create_picker_button(image, func, self.node_picker, node_draw.label)
# draw custom nodes # draw custom nodes
for name in sorted(self.app.core.custom_nodes): for name in sorted(self.app.core.custom_nodes):
node_draw = self.app.core.custom_nodes[name] node_draw = self.app.core.custom_nodes[name]
toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) toolbar_image = Images.get_custom(
image = Images.get_custom(node_draw.image_file, PICKER_SIZE) 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( func = partial(
self.update_button, self.node_button, toolbar_image, node_draw 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) self.create_picker_button(image, func, self.node_picker, name)
# draw edit node # draw edit node
image = icon(ImageEnum.EDITNODE, PICKER_SIZE) # image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, self.click_edit_node, self.node_picker, "Custom" image, self.click_edit_node, self.node_picker, "Custom"
) )
@ -281,13 +315,24 @@ class Toolbar(ttk.Frame):
dialog = CustomNodesDialog(self.app, self.app) dialog = CustomNodesDialog(self.app, self.app)
dialog.show() dialog.show()
def update_button(self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw): 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) logging.debug("update button(%s): %s", button, node_draw)
self.hide_pickers() self.hide_pickers()
button.configure(image=image) button.configure(image=image)
button.image = image button.image = image
self.app.canvas.mode = GraphMode.NODE self.app.canvas.mode = GraphMode.NODE
self.app.canvas.node_draw = node_draw 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): def hide_pickers(self):
logging.debug("hiding pickers") logging.debug("hiding pickers")
@ -305,13 +350,14 @@ class Toolbar(ttk.Frame):
""" """
Create network layer button Create network layer button
""" """
image = icon(ImageEnum.ROUTER) image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE)
self.node_button = ttk.Button( self.node_button = ttk.Button(
self.design_frame, image=image, command=self.draw_node_picker self.design_frame, image=image, command=self.draw_node_picker
) )
self.node_button.image = image self.node_button.image = image
self.node_button.grid(sticky="ew") self.node_button.grid(sticky="ew")
Tooltip(self.node_button, "Network-layer virtual nodes") Tooltip(self.node_button, "Network-layer virtual nodes")
self.node_enum = ImageEnum.ROUTER
def draw_network_picker(self): def draw_network_picker(self):
""" """
@ -320,12 +366,17 @@ class Toolbar(ttk.Frame):
self.hide_pickers() self.hide_pickers()
self.network_picker = ttk.Frame(self.master) self.network_picker = ttk.Frame(self.master)
for node_draw in NodeUtils.NETWORK_NODES: for node_draw in NodeUtils.NETWORK_NODES:
toolbar_image = icon(node_draw.image_enum) toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
image = icon(node_draw.image_enum, PICKER_SIZE) image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, image,
partial( partial(
self.update_button, self.network_button, toolbar_image, node_draw self.update_button,
self.network_button,
toolbar_image,
node_draw,
NodeTypeEnum.NETWORK,
node_draw.image_enum,
), ),
self.network_picker, self.network_picker,
node_draw.label, node_draw.label,
@ -340,13 +391,14 @@ class Toolbar(ttk.Frame):
Create link-layer node button and the options that represent different Create link-layer node button and the options that represent different
link-layer node types. link-layer node types.
""" """
image = icon(ImageEnum.HUB) image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE)
self.network_button = ttk.Button( self.network_button = ttk.Button(
self.design_frame, image=image, command=self.draw_network_picker self.design_frame, image=image, command=self.draw_network_picker
) )
self.network_button.image = image self.network_button.image = image
self.network_button.grid(sticky="ew") self.network_button.grid(sticky="ew")
Tooltip(self.network_button, "link-layer nodes") Tooltip(self.network_button, "link-layer nodes")
self.network_enum = ImageEnum.HUB
def draw_annotation_picker(self): def draw_annotation_picker(self):
""" """
@ -361,11 +413,11 @@ class Toolbar(ttk.Frame):
(ImageEnum.TEXT, ShapeType.TEXT), (ImageEnum.TEXT, ShapeType.TEXT),
] ]
for image_enum, shape_type in nodes: for image_enum, shape_type in nodes:
toolbar_image = icon(image_enum) toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE)
image = icon(image_enum, PICKER_SIZE) image = self.get_icon(image_enum, PICKER_SIZE)
self.create_picker_button( self.create_picker_button(
image, image,
partial(self.update_annotation, toolbar_image, shape_type), partial(self.update_annotation, toolbar_image, shape_type, image_enum),
self.annotation_picker, self.annotation_picker,
shape_type.value, shape_type.value,
) )
@ -378,13 +430,14 @@ class Toolbar(ttk.Frame):
""" """
Create marker button and options that represent different marker types Create marker button and options that represent different marker types
""" """
image = icon(ImageEnum.MARKER) image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE)
self.annotation_button = ttk.Button( self.annotation_button = ttk.Button(
self.design_frame, image=image, command=self.draw_annotation_picker self.design_frame, image=image, command=self.draw_annotation_picker
) )
self.annotation_button.image = image self.annotation_button.image = image
self.annotation_button.grid(sticky="ew") self.annotation_button.grid(sticky="ew")
Tooltip(self.annotation_button, "background annotation tools") Tooltip(self.annotation_button, "background annotation tools")
self.annotation_enum = ImageEnum.MARKER
def create_observe_button(self): def create_observe_button(self):
menu_button = ttk.Menubutton( menu_button = ttk.Menubutton(
@ -429,13 +482,16 @@ class Toolbar(ttk.Frame):
self.app.statusbar.set_status(message) self.app.statusbar.set_status(message)
self.app.canvas.stopped_session() self.app.canvas.stopped_session()
def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType): def update_annotation(
self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum
):
logging.debug("clicked annotation: ") logging.debug("clicked annotation: ")
self.hide_pickers() self.hide_pickers()
self.annotation_button.configure(image=image) self.annotation_button.configure(image=image)
self.annotation_button.image = image self.annotation_button.image = image
self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.mode = GraphMode.ANNOTATION
self.app.canvas.annotation_type = shape_type self.app.canvas.annotation_type = shape_type
self.annotation_enum = image_enum
if is_marker(shape_type): if is_marker(shape_type):
if self.marker_tool: if self.marker_tool:
self.marker_tool.destroy() self.marker_tool.destroy()
@ -460,3 +516,24 @@ class Toolbar(ttk.Frame):
def click_two_node_button(self): def click_two_node_button(self):
logging.debug("Click TWONODE button") 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)