pygui: added type hinting to everything under base core.gui
This commit is contained in:
parent
adfce52632
commit
0356f3b19c
17 changed files with 473 additions and 430 deletions
|
@ -436,7 +436,7 @@ class CoreGrpcClient:
|
||||||
session_id: int,
|
session_id: int,
|
||||||
handler: Callable[[core_pb2.Event], None],
|
handler: Callable[[core_pb2.Event], None],
|
||||||
events: List[core_pb2.Event] = None,
|
events: List[core_pb2.Event] = None,
|
||||||
) -> Any:
|
) -> grpc.Channel:
|
||||||
"""
|
"""
|
||||||
Listen for session events.
|
Listen for session events.
|
||||||
|
|
||||||
|
@ -453,7 +453,7 @@ class CoreGrpcClient:
|
||||||
|
|
||||||
def throughputs(
|
def throughputs(
|
||||||
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
|
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
|
||||||
) -> Any:
|
) -> grpc.Channel:
|
||||||
"""
|
"""
|
||||||
Listen for throughput events with information for interfaces and bridges.
|
Listen for throughput events with information for interfaces and bridges.
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@ import math
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import PhotoImage, font, ttk
|
from tkinter import PhotoImage, font, ttk
|
||||||
from tkinter.ttk import Progressbar
|
from tkinter.ttk import Progressbar
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
from core.gui import appconfig, themes
|
from core.gui import appconfig, themes
|
||||||
|
from core.gui.appconfig import GuiConfig
|
||||||
from core.gui.coreclient import CoreClient
|
from core.gui.coreclient import CoreClient
|
||||||
from core.gui.dialogs.error import ErrorDialog
|
from core.gui.dialogs.error import ErrorDialog
|
||||||
from core.gui.graph.graph import CanvasGraph
|
from core.gui.graph.graph import CanvasGraph
|
||||||
|
@ -16,8 +18,8 @@ from core.gui.nodeutils import NodeUtils
|
||||||
from core.gui.statusbar import StatusBar
|
from core.gui.statusbar import StatusBar
|
||||||
from core.gui.toolbar import Toolbar
|
from core.gui.toolbar import Toolbar
|
||||||
|
|
||||||
WIDTH = 1000
|
WIDTH: int = 1000
|
||||||
HEIGHT = 800
|
HEIGHT: int = 800
|
||||||
|
|
||||||
|
|
||||||
class Application(ttk.Frame):
|
class Application(ttk.Frame):
|
||||||
|
@ -27,25 +29,25 @@ class Application(ttk.Frame):
|
||||||
NodeUtils.setup()
|
NodeUtils.setup()
|
||||||
|
|
||||||
# widgets
|
# widgets
|
||||||
self.menubar = None
|
self.menubar: Optional[Menubar] = None
|
||||||
self.toolbar = None
|
self.toolbar: Optional[Toolbar] = None
|
||||||
self.right_frame = None
|
self.right_frame: Optional[ttk.Frame] = None
|
||||||
self.canvas = None
|
self.canvas: Optional[CanvasGraph] = None
|
||||||
self.statusbar = None
|
self.statusbar: Optional[StatusBar] = None
|
||||||
self.progress = None
|
self.progress: Optional[Progressbar] = None
|
||||||
|
|
||||||
# fonts
|
# fonts
|
||||||
self.fonts_size = None
|
self.fonts_size: Dict[str, int] = {}
|
||||||
self.icon_text_font = None
|
self.icon_text_font: Optional[font.Font] = None
|
||||||
self.edge_font = None
|
self.edge_font: Optional[font.Font] = None
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
self.guiconfig = appconfig.read()
|
self.guiconfig: GuiConfig = appconfig.read()
|
||||||
self.app_scale = self.guiconfig.scale
|
self.app_scale: float = self.guiconfig.scale
|
||||||
self.setup_scaling()
|
self.setup_scaling()
|
||||||
self.style = ttk.Style()
|
self.style: ttk.Style = ttk.Style()
|
||||||
self.setup_theme()
|
self.setup_theme()
|
||||||
self.core = CoreClient(self, proxy)
|
self.core: CoreClient = CoreClient(self, proxy)
|
||||||
self.setup_app()
|
self.setup_app()
|
||||||
self.draw()
|
self.draw()
|
||||||
self.core.setup()
|
self.core.setup()
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from core.gui import themes
|
from core.gui import themes
|
||||||
|
|
||||||
HOME_PATH = Path.home().joinpath(".coregui")
|
HOME_PATH: Path = Path.home().joinpath(".coregui")
|
||||||
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds")
|
BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds")
|
||||||
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane")
|
CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane")
|
||||||
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services")
|
CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services")
|
||||||
ICONS_PATH = HOME_PATH.joinpath("icons")
|
ICONS_PATH: Path = HOME_PATH.joinpath("icons")
|
||||||
MOBILITY_PATH = HOME_PATH.joinpath("mobility")
|
MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility")
|
||||||
XMLS_PATH = HOME_PATH.joinpath("xmls")
|
XMLS_PATH: Path = HOME_PATH.joinpath("xmls")
|
||||||
CONFIG_PATH = HOME_PATH.joinpath("config.yaml")
|
CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml")
|
||||||
LOG_PATH = HOME_PATH.joinpath("gui.log")
|
LOG_PATH: Path = HOME_PATH.joinpath("gui.log")
|
||||||
SCRIPT_PATH = HOME_PATH.joinpath("scripts")
|
SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts")
|
||||||
|
|
||||||
# local paths
|
# local paths
|
||||||
DATA_PATH = Path(__file__).parent.joinpath("data")
|
DATA_PATH: Path = Path(__file__).parent.joinpath("data")
|
||||||
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute()
|
LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute()
|
||||||
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute()
|
LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute()
|
||||||
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute()
|
LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
|
||||||
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute()
|
LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
|
||||||
|
|
||||||
# configuration data
|
# configuration data
|
||||||
TERMINALS = {
|
TERMINALS: Dict[str, str] = {
|
||||||
"xterm": "xterm -e",
|
"xterm": "xterm -e",
|
||||||
"aterm": "aterm -e",
|
"aterm": "aterm -e",
|
||||||
"eterm": "eterm -e",
|
"eterm": "eterm -e",
|
||||||
|
@ -36,45 +36,45 @@ TERMINALS = {
|
||||||
"xfce4-terminal": "xfce4-terminal -x",
|
"xfce4-terminal": "xfce4-terminal -x",
|
||||||
"gnome-terminal": "gnome-terminal --window --",
|
"gnome-terminal": "gnome-terminal --window --",
|
||||||
}
|
}
|
||||||
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
|
||||||
|
|
||||||
|
|
||||||
class IndentDumper(yaml.Dumper):
|
class IndentDumper(yaml.Dumper):
|
||||||
def increase_indent(self, flow=False, indentless=False):
|
def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
|
||||||
return super().increase_indent(flow, False)
|
super().increase_indent(flow, False)
|
||||||
|
|
||||||
|
|
||||||
class CustomNode(yaml.YAMLObject):
|
class CustomNode(yaml.YAMLObject):
|
||||||
yaml_tag = "!CustomNode"
|
yaml_tag: str = "!CustomNode"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
def __init__(self, name: str, image: str, services: List[str]) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.image = image
|
self.image: str = image
|
||||||
self.services = services
|
self.services: List[str] = services
|
||||||
|
|
||||||
|
|
||||||
class CoreServer(yaml.YAMLObject):
|
class CoreServer(yaml.YAMLObject):
|
||||||
yaml_tag = "!CoreServer"
|
yaml_tag: str = "!CoreServer"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, address: str) -> None:
|
def __init__(self, name: str, address: str) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.address = address
|
self.address: str = address
|
||||||
|
|
||||||
|
|
||||||
class Observer(yaml.YAMLObject):
|
class Observer(yaml.YAMLObject):
|
||||||
yaml_tag = "!Observer"
|
yaml_tag: str = "!Observer"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(self, name: str, cmd: str) -> None:
|
def __init__(self, name: str, cmd: str) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.cmd = cmd
|
self.cmd: str = cmd
|
||||||
|
|
||||||
|
|
||||||
class PreferencesConfig(yaml.YAMLObject):
|
class PreferencesConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!PreferencesConfig"
|
yaml_tag: str = "!PreferencesConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -85,17 +85,17 @@ class PreferencesConfig(yaml.YAMLObject):
|
||||||
width: int = 1000,
|
width: int = 1000,
|
||||||
height: int = 750,
|
height: int = 750,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.theme = theme
|
self.theme: str = theme
|
||||||
self.editor = editor
|
self.editor: str = editor
|
||||||
self.terminal = terminal
|
self.terminal: str = terminal
|
||||||
self.gui3d = gui3d
|
self.gui3d: str = gui3d
|
||||||
self.width = width
|
self.width: int = width
|
||||||
self.height = height
|
self.height: int = height
|
||||||
|
|
||||||
|
|
||||||
class LocationConfig(yaml.YAMLObject):
|
class LocationConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!LocationConfig"
|
yaml_tag: str = "!LocationConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -107,18 +107,18 @@ class LocationConfig(yaml.YAMLObject):
|
||||||
alt: float = 2.0,
|
alt: float = 2.0,
|
||||||
scale: float = 150.0,
|
scale: float = 150.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.x = x
|
self.x: float = x
|
||||||
self.y = y
|
self.y: float = y
|
||||||
self.z = z
|
self.z: float = z
|
||||||
self.lat = lat
|
self.lat: float = lat
|
||||||
self.lon = lon
|
self.lon: float = lon
|
||||||
self.alt = alt
|
self.alt: float = alt
|
||||||
self.scale = scale
|
self.scale: float = scale
|
||||||
|
|
||||||
|
|
||||||
class IpConfigs(yaml.YAMLObject):
|
class IpConfigs(yaml.YAMLObject):
|
||||||
yaml_tag = "!IpConfigs"
|
yaml_tag: str = "!IpConfigs"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -129,21 +129,21 @@ class IpConfigs(yaml.YAMLObject):
|
||||||
) -> None:
|
) -> None:
|
||||||
if ip4s is None:
|
if ip4s is None:
|
||||||
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
|
||||||
self.ip4s = ip4s
|
self.ip4s: List[str] = ip4s
|
||||||
if ip6s is None:
|
if ip6s is None:
|
||||||
ip6s = ["2001::", "2002::", "a::"]
|
ip6s = ["2001::", "2002::", "a::"]
|
||||||
self.ip6s = ip6s
|
self.ip6s: List[str] = ip6s
|
||||||
if ip4 is None:
|
if ip4 is None:
|
||||||
ip4 = self.ip4s[0]
|
ip4 = self.ip4s[0]
|
||||||
self.ip4 = ip4
|
self.ip4: str = ip4
|
||||||
if ip6 is None:
|
if ip6 is None:
|
||||||
ip6 = self.ip6s[0]
|
ip6 = self.ip6s[0]
|
||||||
self.ip6 = ip6
|
self.ip6: str = ip6
|
||||||
|
|
||||||
|
|
||||||
class GuiConfig(yaml.YAMLObject):
|
class GuiConfig(yaml.YAMLObject):
|
||||||
yaml_tag = "!GuiConfig"
|
yaml_tag: str = "!GuiConfig"
|
||||||
yaml_loader = yaml.SafeLoader
|
yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -159,30 +159,30 @@ class GuiConfig(yaml.YAMLObject):
|
||||||
) -> None:
|
) -> None:
|
||||||
if preferences is None:
|
if preferences is None:
|
||||||
preferences = PreferencesConfig()
|
preferences = PreferencesConfig()
|
||||||
self.preferences = preferences
|
self.preferences: PreferencesConfig = preferences
|
||||||
if location is None:
|
if location is None:
|
||||||
location = LocationConfig()
|
location = LocationConfig()
|
||||||
self.location = location
|
self.location: LocationConfig = location
|
||||||
if servers is None:
|
if servers is None:
|
||||||
servers = []
|
servers = []
|
||||||
self.servers = servers
|
self.servers: List[CoreServer] = servers
|
||||||
if nodes is None:
|
if nodes is None:
|
||||||
nodes = []
|
nodes = []
|
||||||
self.nodes = nodes
|
self.nodes: List[CustomNode] = nodes
|
||||||
if recentfiles is None:
|
if recentfiles is None:
|
||||||
recentfiles = []
|
recentfiles = []
|
||||||
self.recentfiles = recentfiles
|
self.recentfiles: List[str] = recentfiles
|
||||||
if observers is None:
|
if observers is None:
|
||||||
observers = []
|
observers = []
|
||||||
self.observers = observers
|
self.observers: List[Observer] = observers
|
||||||
self.scale = scale
|
self.scale: float = scale
|
||||||
if ips is None:
|
if ips is None:
|
||||||
ips = IpConfigs()
|
ips = IpConfigs()
|
||||||
self.ips = ips
|
self.ips: IpConfigs = ips
|
||||||
self.mac = mac
|
self.mac: str = mac
|
||||||
|
|
||||||
|
|
||||||
def copy_files(current_path, new_path) -> None:
|
def copy_files(current_path: Path, new_path: Path) -> None:
|
||||||
for current_file in current_path.glob("*"):
|
for current_file in current_path.glob("*"):
|
||||||
new_file = new_path.joinpath(current_file.name)
|
new_file = new_path.joinpath(current_file.name)
|
||||||
shutil.copy(current_file, new_file)
|
shutil.copy(current_file, new_file)
|
||||||
|
|
|
@ -4,18 +4,41 @@ Incorporate grpc into python tkinter GUI
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tkinter as tk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
|
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2
|
from core.api.grpc import client
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig
|
||||||
|
from core.api.grpc.core_pb2 import (
|
||||||
|
Event,
|
||||||
|
ExceptionEvent,
|
||||||
|
Hook,
|
||||||
|
Interface,
|
||||||
|
Link,
|
||||||
|
LinkEvent,
|
||||||
|
LinkType,
|
||||||
|
MessageType,
|
||||||
|
Node,
|
||||||
|
NodeEvent,
|
||||||
|
NodeType,
|
||||||
|
Position,
|
||||||
|
SessionLocation,
|
||||||
|
SessionState,
|
||||||
|
StartSessionResponse,
|
||||||
|
StopSessionResponse,
|
||||||
|
ThroughputsEvent,
|
||||||
|
)
|
||||||
from core.api.grpc.emane_pb2 import EmaneModelConfig
|
from core.api.grpc.emane_pb2 import EmaneModelConfig
|
||||||
from core.api.grpc.mobility_pb2 import MobilityConfig
|
from core.api.grpc.mobility_pb2 import MobilityConfig
|
||||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
|
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig, ServiceFileConfig
|
||||||
from core.api.grpc.wlan_pb2 import WlanConfig
|
from core.api.grpc.wlan_pb2 import WlanConfig
|
||||||
from core.gui import appconfig
|
from core.gui import appconfig
|
||||||
|
from core.gui.appconfig import CoreServer
|
||||||
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
|
from core.gui.dialogs.emaneinstall import EmaneInstallDialog
|
||||||
from core.gui.dialogs.error import ErrorDialog
|
from core.gui.dialogs.error import ErrorDialog
|
||||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||||
|
@ -34,47 +57,46 @@ GUI_SOURCE = "gui"
|
||||||
|
|
||||||
|
|
||||||
class CoreClient:
|
class CoreClient:
|
||||||
def __init__(self, app: "Application", proxy: bool):
|
def __init__(self, app: "Application", proxy: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Create a CoreGrpc instance
|
Create a CoreGrpc instance
|
||||||
"""
|
"""
|
||||||
self._client = client.CoreGrpcClient(proxy=proxy)
|
self.app: "Application" = app
|
||||||
self.session_id = None
|
self.master: tk.Tk = app.master
|
||||||
self.node_ids = []
|
self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy)
|
||||||
self.app = app
|
self.session_id: Optional[int] = None
|
||||||
self.master = app.master
|
self.services: Dict[str, Set[str]] = {}
|
||||||
self.services = {}
|
self.config_services_groups: Dict[str, Set[str]] = {}
|
||||||
self.config_services_groups = {}
|
self.config_services: Dict[str, ConfigService] = {}
|
||||||
self.config_services = {}
|
self.default_services: Dict[NodeType, Set[str]] = {}
|
||||||
self.default_services = {}
|
self.emane_models: List[str] = []
|
||||||
self.emane_models = []
|
self.observer: Optional[str] = None
|
||||||
self.observer = None
|
|
||||||
|
|
||||||
# loaded configuration data
|
# loaded configuration data
|
||||||
self.servers = {}
|
self.servers: Dict[str, CoreServer] = {}
|
||||||
self.custom_nodes = {}
|
self.custom_nodes: Dict[str, NodeDraw] = {}
|
||||||
self.custom_observers = {}
|
self.custom_observers: Dict[str, str] = {}
|
||||||
self.read_config()
|
self.read_config()
|
||||||
|
|
||||||
# helpers
|
# helpers
|
||||||
self.iface_to_edge = {}
|
self.iface_to_edge: Dict[Tuple[int, int], Tuple[int, int]] = {}
|
||||||
self.ifaces_manager = InterfaceManager(self.app)
|
self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
|
||||||
|
|
||||||
# session data
|
# session data
|
||||||
self.state = None
|
self.state: Optional[SessionState] = None
|
||||||
self.canvas_nodes = {}
|
self.canvas_nodes: Dict[int, CanvasNode] = {}
|
||||||
self.location = None
|
self.location: Optional[SessionLocation] = None
|
||||||
self.links = {}
|
self.links: Dict[Tuple[int, int], CanvasEdge] = {}
|
||||||
self.hooks = {}
|
self.hooks: Dict[str, Hook] = {}
|
||||||
self.emane_config = None
|
self.emane_config: Dict[str, ConfigOption] = {}
|
||||||
self.mobility_players = {}
|
self.mobility_players: Dict[int, MobilityPlayer] = {}
|
||||||
self.handling_throughputs = None
|
self.handling_throughputs: Optional[grpc.Channel] = None
|
||||||
self.handling_events = None
|
self.handling_events: Optional[grpc.Channel] = None
|
||||||
self.xml_dir = None
|
self.xml_dir: Optional[str] = None
|
||||||
self.xml_file = None
|
self.xml_file: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self) -> client.CoreGrpcClient:
|
||||||
if self.session_id:
|
if self.session_id:
|
||||||
response = self._client.check_session(self.session_id)
|
response = self._client.check_session(self.session_id)
|
||||||
if not response.result:
|
if not response.result:
|
||||||
|
@ -89,7 +111,7 @@ class CoreClient:
|
||||||
self.enable_throughputs()
|
self.enable_throughputs()
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
# helpers
|
# helpers
|
||||||
self.ifaces_manager.reset()
|
self.ifaces_manager.reset()
|
||||||
self.iface_to_edge.clear()
|
self.iface_to_edge.clear()
|
||||||
|
@ -104,14 +126,14 @@ class CoreClient:
|
||||||
self.cancel_throughputs()
|
self.cancel_throughputs()
|
||||||
self.cancel_events()
|
self.cancel_events()
|
||||||
|
|
||||||
def close_mobility_players(self):
|
def close_mobility_players(self) -> None:
|
||||||
for mobility_player in self.mobility_players.values():
|
for mobility_player in self.mobility_players.values():
|
||||||
mobility_player.close()
|
mobility_player.close()
|
||||||
|
|
||||||
def set_observer(self, value: str):
|
def set_observer(self, value: Optional[str]) -> None:
|
||||||
self.observer = value
|
self.observer = value
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self) -> None:
|
||||||
# read distributed servers
|
# read distributed servers
|
||||||
for server in self.app.guiconfig.servers:
|
for server in self.app.guiconfig.servers:
|
||||||
self.servers[server.name] = server
|
self.servers[server.name] = server
|
||||||
|
@ -125,7 +147,7 @@ class CoreClient:
|
||||||
for observer in self.app.guiconfig.observers:
|
for observer in self.app.guiconfig.observers:
|
||||||
self.custom_observers[observer.name] = observer
|
self.custom_observers[observer.name] = observer
|
||||||
|
|
||||||
def handle_events(self, event: core_pb2.Event):
|
def handle_events(self, event: Event) -> None:
|
||||||
if event.session_id != self.session_id:
|
if event.session_id != self.session_id:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"ignoring event session(%s) current(%s)",
|
"ignoring event session(%s) current(%s)",
|
||||||
|
@ -139,7 +161,7 @@ class CoreClient:
|
||||||
elif event.HasField("session_event"):
|
elif event.HasField("session_event"):
|
||||||
logging.info("session event: %s", event)
|
logging.info("session event: %s", event)
|
||||||
session_event = event.session_event
|
session_event = event.session_event
|
||||||
if session_event.event <= core_pb2.SessionState.SHUTDOWN:
|
if session_event.event <= SessionState.SHUTDOWN:
|
||||||
self.state = event.session_event.event
|
self.state = event.session_event.event
|
||||||
elif session_event.event in {7, 8, 9}:
|
elif session_event.event in {7, 8, 9}:
|
||||||
node_id = session_event.node_id
|
node_id = session_event.node_id
|
||||||
|
@ -162,7 +184,7 @@ class CoreClient:
|
||||||
else:
|
else:
|
||||||
logging.info("unhandled event: %s", event)
|
logging.info("unhandled event: %s", event)
|
||||||
|
|
||||||
def handle_link_event(self, event: core_pb2.LinkEvent):
|
def handle_link_event(self, event: LinkEvent) -> None:
|
||||||
logging.debug("Link event: %s", event)
|
logging.debug("Link event: %s", event)
|
||||||
node1_id = event.link.node1_id
|
node1_id = event.link.node1_id
|
||||||
node2_id = event.link.node2_id
|
node2_id = event.link.node2_id
|
||||||
|
@ -171,16 +193,16 @@ class CoreClient:
|
||||||
return
|
return
|
||||||
canvas_node1 = self.canvas_nodes[node1_id]
|
canvas_node1 = self.canvas_nodes[node1_id]
|
||||||
canvas_node2 = self.canvas_nodes[node2_id]
|
canvas_node2 = self.canvas_nodes[node2_id]
|
||||||
if event.message_type == core_pb2.MessageType.ADD:
|
if event.message_type == MessageType.ADD:
|
||||||
self.app.canvas.add_wireless_edge(canvas_node1, canvas_node2, event.link)
|
self.app.canvas.add_wireless_edge(canvas_node1, canvas_node2, event.link)
|
||||||
elif event.message_type == core_pb2.MessageType.DELETE:
|
elif event.message_type == MessageType.DELETE:
|
||||||
self.app.canvas.delete_wireless_edge(canvas_node1, canvas_node2, event.link)
|
self.app.canvas.delete_wireless_edge(canvas_node1, canvas_node2, event.link)
|
||||||
elif event.message_type == core_pb2.MessageType.NONE:
|
elif event.message_type == MessageType.NONE:
|
||||||
self.app.canvas.update_wireless_edge(canvas_node1, canvas_node2, event.link)
|
self.app.canvas.update_wireless_edge(canvas_node1, canvas_node2, event.link)
|
||||||
else:
|
else:
|
||||||
logging.warning("unknown link event: %s", event)
|
logging.warning("unknown link event: %s", event)
|
||||||
|
|
||||||
def handle_node_event(self, event: core_pb2.NodeEvent):
|
def handle_node_event(self, event: NodeEvent) -> None:
|
||||||
logging.debug("node event: %s", event)
|
logging.debug("node event: %s", event)
|
||||||
if event.source == GUI_SOURCE:
|
if event.source == GUI_SOURCE:
|
||||||
return
|
return
|
||||||
|
@ -190,22 +212,22 @@ class CoreClient:
|
||||||
canvas_node = self.canvas_nodes[node_id]
|
canvas_node = self.canvas_nodes[node_id]
|
||||||
canvas_node.move(x, y)
|
canvas_node.move(x, y)
|
||||||
|
|
||||||
def enable_throughputs(self):
|
def enable_throughputs(self) -> None:
|
||||||
self.handling_throughputs = self.client.throughputs(
|
self.handling_throughputs = self.client.throughputs(
|
||||||
self.session_id, self.handle_throughputs
|
self.session_id, self.handle_throughputs
|
||||||
)
|
)
|
||||||
|
|
||||||
def cancel_throughputs(self):
|
def cancel_throughputs(self) -> None:
|
||||||
if self.handling_throughputs:
|
if self.handling_throughputs:
|
||||||
self.handling_throughputs.cancel()
|
self.handling_throughputs.cancel()
|
||||||
self.handling_throughputs = None
|
self.handling_throughputs = None
|
||||||
|
|
||||||
def cancel_events(self):
|
def cancel_events(self) -> None:
|
||||||
if self.handling_events:
|
if self.handling_events:
|
||||||
self.handling_events.cancel()
|
self.handling_events.cancel()
|
||||||
self.handling_events = None
|
self.handling_events = None
|
||||||
|
|
||||||
def handle_throughputs(self, event: core_pb2.ThroughputsEvent):
|
def handle_throughputs(self, event: ThroughputsEvent) -> None:
|
||||||
if event.session_id != self.session_id:
|
if event.session_id != self.session_id:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"ignoring throughput event session(%s) current(%s)",
|
"ignoring throughput event session(%s) current(%s)",
|
||||||
|
@ -216,11 +238,11 @@ class CoreClient:
|
||||||
logging.debug("handling throughputs event: %s", event)
|
logging.debug("handling throughputs event: %s", event)
|
||||||
self.app.after(0, self.app.canvas.set_throughputs, event)
|
self.app.after(0, self.app.canvas.set_throughputs, event)
|
||||||
|
|
||||||
def handle_exception_event(self, event: core_pb2.ExceptionEvent):
|
def handle_exception_event(self, event: ExceptionEvent) -> None:
|
||||||
logging.info("exception event: %s", event)
|
logging.info("exception event: %s", event)
|
||||||
self.app.statusbar.core_alarms.append(event)
|
self.app.statusbar.core_alarms.append(event)
|
||||||
|
|
||||||
def join_session(self, session_id: int, query_location: bool = True):
|
def join_session(self, session_id: int, query_location: bool = True) -> None:
|
||||||
logging.info("join session(%s)", session_id)
|
logging.info("join session(%s)", session_id)
|
||||||
# update session and title
|
# update session and title
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
|
@ -331,9 +353,9 @@ class CoreClient:
|
||||||
self.app.after(0, self.app.joined_session_update)
|
self.app.after(0, self.app.joined_session_update)
|
||||||
|
|
||||||
def is_runtime(self) -> bool:
|
def is_runtime(self) -> bool:
|
||||||
return self.state == core_pb2.SessionState.RUNTIME
|
return self.state == SessionState.RUNTIME
|
||||||
|
|
||||||
def parse_metadata(self, config: Dict[str, str]):
|
def parse_metadata(self, config: Dict[str, str]) -> None:
|
||||||
# canvas setting
|
# canvas setting
|
||||||
canvas_config = config.get("canvas")
|
canvas_config = config.get("canvas")
|
||||||
logging.debug("canvas metadata: %s", canvas_config)
|
logging.debug("canvas metadata: %s", canvas_config)
|
||||||
|
@ -386,7 +408,7 @@ class CoreClient:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.exception("unknown shape: %s", shape_type)
|
logging.exception("unknown shape: %s", shape_type)
|
||||||
|
|
||||||
def create_new_session(self):
|
def create_new_session(self) -> None:
|
||||||
"""
|
"""
|
||||||
Create a new session
|
Create a new session
|
||||||
"""
|
"""
|
||||||
|
@ -394,7 +416,7 @@ class CoreClient:
|
||||||
response = self.client.create_session()
|
response = self.client.create_session()
|
||||||
logging.info("created session: %s", response)
|
logging.info("created session: %s", response)
|
||||||
location_config = self.app.guiconfig.location
|
location_config = self.app.guiconfig.location
|
||||||
self.location = core_pb2.SessionLocation(
|
self.location = SessionLocation(
|
||||||
x=location_config.x,
|
x=location_config.x,
|
||||||
y=location_config.y,
|
y=location_config.y,
|
||||||
z=location_config.z,
|
z=location_config.z,
|
||||||
|
@ -407,7 +429,7 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("New Session Error", e)
|
self.app.show_grpc_exception("New Session Error", e)
|
||||||
|
|
||||||
def delete_session(self, session_id: int = None):
|
def delete_session(self, session_id: int = None) -> None:
|
||||||
if session_id is None:
|
if session_id is None:
|
||||||
session_id = self.session_id
|
session_id = self.session_id
|
||||||
try:
|
try:
|
||||||
|
@ -416,7 +438,7 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Delete Session Error", e)
|
self.app.show_grpc_exception("Delete Session Error", e)
|
||||||
|
|
||||||
def setup(self):
|
def setup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Query sessions, if there exist any, prompt whether to join one
|
Query sessions, if there exist any, prompt whether to join one
|
||||||
"""
|
"""
|
||||||
|
@ -451,7 +473,7 @@ class CoreClient:
|
||||||
dialog.show()
|
dialog.show()
|
||||||
self.app.close()
|
self.app.close()
|
||||||
|
|
||||||
def edit_node(self, core_node: core_pb2.Node):
|
def edit_node(self, core_node: Node) -> None:
|
||||||
try:
|
try:
|
||||||
self.client.edit_node(
|
self.client.edit_node(
|
||||||
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
|
||||||
|
@ -459,12 +481,12 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Edit Node Error", e)
|
self.app.show_grpc_exception("Edit Node Error", e)
|
||||||
|
|
||||||
def start_session(self) -> core_pb2.StartSessionResponse:
|
def start_session(self) -> StartSessionResponse:
|
||||||
self.ifaces_manager.reset_mac()
|
self.ifaces_manager.reset_mac()
|
||||||
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
nodes = [x.core_node for x in self.canvas_nodes.values()]
|
||||||
links = []
|
links = []
|
||||||
for edge in self.links.values():
|
for edge in self.links.values():
|
||||||
link = core_pb2.Link()
|
link = Link()
|
||||||
link.CopyFrom(edge.link)
|
link.CopyFrom(edge.link)
|
||||||
if link.HasField("iface1") and not link.iface1.mac:
|
if link.HasField("iface1") and not link.iface1.mac:
|
||||||
link.iface1.mac = self.ifaces_manager.next_mac()
|
link.iface1.mac = self.ifaces_manager.next_mac()
|
||||||
|
@ -485,7 +507,7 @@ class CoreClient:
|
||||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||||
else:
|
else:
|
||||||
emane_config = None
|
emane_config = None
|
||||||
response = core_pb2.StartSessionResponse(result=False)
|
response = StartSessionResponse(result=False)
|
||||||
try:
|
try:
|
||||||
response = self.client.start_session(
|
response = self.client.start_session(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
|
@ -511,10 +533,10 @@ class CoreClient:
|
||||||
self.app.show_grpc_exception("Start Session Error", e)
|
self.app.show_grpc_exception("Start Session Error", e)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def stop_session(self, session_id: int = None) -> core_pb2.StartSessionResponse:
|
def stop_session(self, session_id: int = None) -> StopSessionResponse:
|
||||||
if not session_id:
|
if not session_id:
|
||||||
session_id = self.session_id
|
session_id = self.session_id
|
||||||
response = core_pb2.StopSessionResponse(result=False)
|
response = StopSessionResponse(result=False)
|
||||||
try:
|
try:
|
||||||
response = self.client.stop_session(session_id)
|
response = self.client.stop_session(session_id)
|
||||||
logging.info("stopped session(%s), result: %s", session_id, response)
|
logging.info("stopped session(%s), result: %s", session_id, response)
|
||||||
|
@ -522,9 +544,9 @@ class CoreClient:
|
||||||
self.app.show_grpc_exception("Stop Session Error", e)
|
self.app.show_grpc_exception("Stop Session Error", e)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def show_mobility_players(self):
|
def show_mobility_players(self) -> None:
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if canvas_node.mobility_config:
|
if canvas_node.mobility_config:
|
||||||
mobility_player = MobilityPlayer(
|
mobility_player = MobilityPlayer(
|
||||||
|
@ -534,7 +556,7 @@ class CoreClient:
|
||||||
self.mobility_players[node_id] = mobility_player
|
self.mobility_players[node_id] = mobility_player
|
||||||
mobility_player.show()
|
mobility_player.show()
|
||||||
|
|
||||||
def set_metadata(self):
|
def set_metadata(self) -> None:
|
||||||
# create canvas data
|
# create canvas data
|
||||||
wallpaper = None
|
wallpaper = None
|
||||||
if self.app.canvas.wallpaper_file:
|
if self.app.canvas.wallpaper_file:
|
||||||
|
@ -558,7 +580,7 @@ class CoreClient:
|
||||||
response = self.client.set_session_metadata(self.session_id, metadata)
|
response = self.client.set_session_metadata(self.session_id, metadata)
|
||||||
logging.info("set session metadata %s, result: %s", metadata, response)
|
logging.info("set session metadata %s, result: %s", metadata, response)
|
||||||
|
|
||||||
def launch_terminal(self, node_id: int):
|
def launch_terminal(self, node_id: int) -> None:
|
||||||
try:
|
try:
|
||||||
terminal = self.app.guiconfig.preferences.terminal
|
terminal = self.app.guiconfig.preferences.terminal
|
||||||
if not terminal:
|
if not terminal:
|
||||||
|
@ -575,12 +597,12 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Node Terminal Error", e)
|
self.app.show_grpc_exception("Node Terminal Error", e)
|
||||||
|
|
||||||
def save_xml(self, file_path: str):
|
def save_xml(self, file_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Save core session as to an xml file
|
Save core session as to an xml file
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.state != core_pb2.SessionState.RUNTIME:
|
if self.state != SessionState.RUNTIME:
|
||||||
logging.debug("Send session data to the daemon")
|
logging.debug("Send session data to the daemon")
|
||||||
self.send_data()
|
self.send_data()
|
||||||
response = self.client.save_xml(self.session_id, file_path)
|
response = self.client.save_xml(self.session_id, file_path)
|
||||||
|
@ -588,7 +610,7 @@ class CoreClient:
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
self.app.show_grpc_exception("Save XML Error", e)
|
self.app.show_grpc_exception("Save XML Error", e)
|
||||||
|
|
||||||
def open_xml(self, file_path: str):
|
def open_xml(self, file_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Open core xml
|
Open core xml
|
||||||
"""
|
"""
|
||||||
|
@ -627,7 +649,8 @@ class CoreClient:
|
||||||
shutdown=shutdowns,
|
shutdown=shutdowns,
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
"Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s",
|
"Set %s service for node(%s), files: %s, Startup: %s, "
|
||||||
|
"Validation: %s, Shutdown: %s, Result: %s",
|
||||||
service_name,
|
service_name,
|
||||||
node_id,
|
node_id,
|
||||||
files,
|
files,
|
||||||
|
@ -656,7 +679,7 @@ class CoreClient:
|
||||||
|
|
||||||
def set_node_service_file(
|
def set_node_service_file(
|
||||||
self, node_id: int, service_name: str, file_name: str, data: str
|
self, node_id: int, service_name: str, file_name: str, data: str
|
||||||
):
|
) -> None:
|
||||||
response = self.client.set_node_service_file(
|
response = self.client.set_node_service_file(
|
||||||
self.session_id, node_id, service_name, file_name, data
|
self.session_id, node_id, service_name, file_name, data
|
||||||
)
|
)
|
||||||
|
@ -669,18 +692,16 @@ class CoreClient:
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_nodes_and_links(self):
|
def create_nodes_and_links(self) -> None:
|
||||||
"""
|
"""
|
||||||
create nodes and links that have not been created yet
|
create nodes and links that have not been created yet
|
||||||
"""
|
"""
|
||||||
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
node_protos = [x.core_node for x in self.canvas_nodes.values()]
|
||||||
link_protos = [x.link for x in self.links.values()]
|
link_protos = [x.link for x in self.links.values()]
|
||||||
if self.state != core_pb2.SessionState.DEFINITION:
|
if self.state != SessionState.DEFINITION:
|
||||||
self.client.set_session_state(
|
self.client.set_session_state(self.session_id, SessionState.DEFINITION)
|
||||||
self.session_id, core_pb2.SessionState.DEFINITION
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION)
|
self.client.set_session_state(self.session_id, SessionState.DEFINITION)
|
||||||
for node_proto in node_protos:
|
for node_proto in node_protos:
|
||||||
response = self.client.add_node(self.session_id, node_proto)
|
response = self.client.add_node(self.session_id, node_proto)
|
||||||
logging.debug("create node: %s", response)
|
logging.debug("create node: %s", response)
|
||||||
|
@ -695,7 +716,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
logging.debug("create link: %s", response)
|
logging.debug("create link: %s", response)
|
||||||
|
|
||||||
def send_data(self):
|
def send_data(self) -> None:
|
||||||
"""
|
"""
|
||||||
send to daemon all session info, but don't start the session
|
send to daemon all session info, but don't start the session
|
||||||
"""
|
"""
|
||||||
|
@ -738,10 +759,9 @@ class CoreClient:
|
||||||
if self.emane_config:
|
if self.emane_config:
|
||||||
config = {x: self.emane_config[x].value for x in self.emane_config}
|
config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||||
self.client.set_emane_config(self.session_id, config)
|
self.client.set_emane_config(self.session_id, config)
|
||||||
|
|
||||||
self.set_metadata()
|
self.set_metadata()
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""
|
"""
|
||||||
Clean ups when done using grpc
|
Clean ups when done using grpc
|
||||||
"""
|
"""
|
||||||
|
@ -760,31 +780,31 @@ class CoreClient:
|
||||||
return i
|
return i
|
||||||
|
|
||||||
def create_node(
|
def create_node(
|
||||||
self, x: float, y: float, node_type: core_pb2.NodeType, model: str
|
self, x: float, y: float, node_type: NodeType, model: str
|
||||||
) -> Optional[core_pb2.Node]:
|
) -> Optional[Node]:
|
||||||
"""
|
"""
|
||||||
Add node, with information filled in, to grpc manager
|
Add node, with information filled in, to grpc manager
|
||||||
"""
|
"""
|
||||||
node_id = self.next_node_id()
|
node_id = self.next_node_id()
|
||||||
position = core_pb2.Position(x=x, y=y)
|
position = Position(x=x, y=y)
|
||||||
image = None
|
image = None
|
||||||
if NodeUtils.is_image_node(node_type):
|
if NodeUtils.is_image_node(node_type):
|
||||||
image = "ubuntu:latest"
|
image = "ubuntu:latest"
|
||||||
emane = None
|
emane = None
|
||||||
if node_type == core_pb2.NodeType.EMANE:
|
if node_type == NodeType.EMANE:
|
||||||
if not self.emane_models:
|
if not self.emane_models:
|
||||||
dialog = EmaneInstallDialog(self.app)
|
dialog = EmaneInstallDialog(self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
return
|
return
|
||||||
emane = self.emane_models[0]
|
emane = self.emane_models[0]
|
||||||
name = f"EMANE{node_id}"
|
name = f"EMANE{node_id}"
|
||||||
elif node_type == core_pb2.NodeType.WIRELESS_LAN:
|
elif node_type == NodeType.WIRELESS_LAN:
|
||||||
name = f"WLAN{node_id}"
|
name = f"WLAN{node_id}"
|
||||||
elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]:
|
elif node_type in [NodeType.RJ45, NodeType.TUNNEL]:
|
||||||
name = "UNASSIGNED"
|
name = "UNASSIGNED"
|
||||||
else:
|
else:
|
||||||
name = f"n{node_id}"
|
name = f"n{node_id}"
|
||||||
node = core_pb2.Node(
|
node = Node(
|
||||||
id=node_id,
|
id=node_id,
|
||||||
type=node_type,
|
type=node_type,
|
||||||
name=name,
|
name=name,
|
||||||
|
@ -810,7 +830,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]):
|
def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None:
|
||||||
"""
|
"""
|
||||||
remove the nodes selected by the user and anything related to that node
|
remove the nodes selected by the user and anything related to that node
|
||||||
such as link, configurations, interfaces
|
such as link, configurations, interfaces
|
||||||
|
@ -826,14 +846,14 @@ class CoreClient:
|
||||||
links.append(edge.link)
|
links.append(edge.link)
|
||||||
self.ifaces_manager.removed(links)
|
self.ifaces_manager.removed(links)
|
||||||
|
|
||||||
def create_iface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
|
def create_iface(self, canvas_node: CanvasNode) -> Interface:
|
||||||
node = canvas_node.core_node
|
node = canvas_node.core_node
|
||||||
ip4, ip6 = self.ifaces_manager.get_ips(node)
|
ip4, ip6 = self.ifaces_manager.get_ips(node)
|
||||||
ip4_mask = self.ifaces_manager.ip4_mask
|
ip4_mask = self.ifaces_manager.ip4_mask
|
||||||
ip6_mask = self.ifaces_manager.ip6_mask
|
ip6_mask = self.ifaces_manager.ip6_mask
|
||||||
iface_id = canvas_node.next_iface_id()
|
iface_id = canvas_node.next_iface_id()
|
||||||
name = f"eth{iface_id}"
|
name = f"eth{iface_id}"
|
||||||
iface = core_pb2.Interface(
|
iface = Interface(
|
||||||
id=iface_id,
|
id=iface_id,
|
||||||
name=name,
|
name=name,
|
||||||
ip4=ip4,
|
ip4=ip4,
|
||||||
|
@ -852,7 +872,7 @@ class CoreClient:
|
||||||
|
|
||||||
def create_link(
|
def create_link(
|
||||||
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create core link for a pair of canvas nodes, with token referencing
|
Create core link for a pair of canvas nodes, with token referencing
|
||||||
the canvas edge.
|
the canvas edge.
|
||||||
|
@ -873,8 +893,8 @@ class CoreClient:
|
||||||
dst_iface = self.create_iface(canvas_dst_node)
|
dst_iface = self.create_iface(canvas_dst_node)
|
||||||
self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
|
self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
|
||||||
|
|
||||||
link = core_pb2.Link(
|
link = Link(
|
||||||
type=core_pb2.LinkType.WIRED,
|
type=LinkType.WIRED,
|
||||||
node1_id=src_node.id,
|
node1_id=src_node.id,
|
||||||
node2_id=dst_node.id,
|
node2_id=dst_node.id,
|
||||||
iface1=src_iface,
|
iface1=src_iface,
|
||||||
|
@ -896,7 +916,7 @@ class CoreClient:
|
||||||
def get_wlan_configs_proto(self) -> List[WlanConfig]:
|
def get_wlan_configs_proto(self) -> List[WlanConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if not canvas_node.wlan_config:
|
if not canvas_node.wlan_config:
|
||||||
continue
|
continue
|
||||||
|
@ -910,7 +930,7 @@ class CoreClient:
|
||||||
def get_mobility_configs_proto(self) -> List[MobilityConfig]:
|
def get_mobility_configs_proto(self) -> List[MobilityConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN:
|
if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
|
||||||
continue
|
continue
|
||||||
if not canvas_node.mobility_config:
|
if not canvas_node.mobility_config:
|
||||||
continue
|
continue
|
||||||
|
@ -924,7 +944,7 @@ class CoreClient:
|
||||||
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
|
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
|
||||||
configs = []
|
configs = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if canvas_node.core_node.type != core_pb2.NodeType.EMANE:
|
if canvas_node.core_node.type != NodeType.EMANE:
|
||||||
continue
|
continue
|
||||||
node_id = canvas_node.core_node.id
|
node_id = canvas_node.core_node.id
|
||||||
for key, config in canvas_node.emane_model_configs.items():
|
for key, config in canvas_node.emane_model_configs.items():
|
||||||
|
@ -975,9 +995,7 @@ class CoreClient:
|
||||||
configs.append(config_proto)
|
configs.append(config_proto)
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
def get_config_service_configs_proto(
|
def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]:
|
||||||
self
|
|
||||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
|
||||||
config_service_protos = []
|
config_service_protos = []
|
||||||
for canvas_node in self.canvas_nodes.values():
|
for canvas_node in self.canvas_nodes.values():
|
||||||
if not NodeUtils.is_container_node(canvas_node.core_node.type):
|
if not NodeUtils.is_container_node(canvas_node.core_node.type):
|
||||||
|
@ -987,7 +1005,7 @@ class CoreClient:
|
||||||
node_id = canvas_node.core_node.id
|
node_id = canvas_node.core_node.id
|
||||||
for name, service_config in canvas_node.config_service_configs.items():
|
for name, service_config in canvas_node.config_service_configs.items():
|
||||||
config = service_config.get("config", {})
|
config = service_config.get("config", {})
|
||||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
config_proto = ConfigServiceConfig(
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
name=name,
|
name=name,
|
||||||
templates=service_config["templates"],
|
templates=service_config["templates"],
|
||||||
|
@ -1000,7 +1018,7 @@ class CoreClient:
|
||||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||||
|
|
||||||
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]:
|
||||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
|
@ -1010,7 +1028,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
return dict(config)
|
return dict(config)
|
||||||
|
|
||||||
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
|
||||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
|
@ -1022,7 +1040,7 @@ class CoreClient:
|
||||||
|
|
||||||
def get_emane_model_config(
|
def get_emane_model_config(
|
||||||
self, node_id: int, model: str, iface_id: int = None
|
self, node_id: int, model: str, iface_id: int = None
|
||||||
) -> Dict[str, common_pb2.ConfigOption]:
|
) -> Dict[str, ConfigOption]:
|
||||||
if iface_id is None:
|
if iface_id is None:
|
||||||
iface_id = -1
|
iface_id = -1
|
||||||
response = self.client.get_emane_model_config(
|
response = self.client.get_emane_model_config(
|
||||||
|
@ -1030,7 +1048,8 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
config = response.config
|
config = response.config
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s",
|
"get emane model config: node id: %s, EMANE model: %s, "
|
||||||
|
"interface: %s, config: %s",
|
||||||
node_id,
|
node_id,
|
||||||
model,
|
model,
|
||||||
iface_id,
|
iface_id,
|
||||||
|
@ -1038,7 +1057,7 @@ class CoreClient:
|
||||||
)
|
)
|
||||||
return dict(config)
|
return dict(config)
|
||||||
|
|
||||||
def execute_script(self, script):
|
def execute_script(self, script) -> None:
|
||||||
response = self.client.execute_script(script)
|
response = self.client.execute_script(script)
|
||||||
logging.info("execute python script %s", response)
|
logging.info("execute python script %s", response)
|
||||||
if response.session_id != -1:
|
if response.session_id != -1:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from tkinter import BooleanVar
|
from tkinter import BooleanVar
|
||||||
from typing import TYPE_CHECKING, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
@ -864,7 +864,7 @@ class CanvasGraph(tk.Canvas):
|
||||||
for tag in tags.ORGANIZE_TAGS:
|
for tag in tags.ORGANIZE_TAGS:
|
||||||
self.tag_raise(tag)
|
self.tag_raise(tag)
|
||||||
|
|
||||||
def set_wallpaper(self, filename: str):
|
def set_wallpaper(self, filename: Optional[str]):
|
||||||
logging.debug("setting wallpaper: %s", filename)
|
logging.debug("setting wallpaper: %s", filename)
|
||||||
if filename:
|
if filename:
|
||||||
img = Image.open(filename)
|
img = Image.open(filename)
|
||||||
|
|
|
@ -1,46 +1,44 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image
|
||||||
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc.core_pb2 import NodeType
|
||||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||||
|
|
||||||
|
|
||||||
class Images:
|
class Images:
|
||||||
images = {}
|
images: Dict[str, str] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, file_path: str, width: int, height: int = None):
|
def create(cls, file_path: str, width: int, height: int = None) -> PhotoImage:
|
||||||
if height is None:
|
if height is None:
|
||||||
height = width
|
height = width
|
||||||
image = Image.open(file_path)
|
image = Image.open(file_path)
|
||||||
image = image.resize((width, height), Image.ANTIALIAS)
|
image = image.resize((width, height), Image.ANTIALIAS)
|
||||||
return ImageTk.PhotoImage(image)
|
return PhotoImage(image)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_all(cls):
|
def load_all(cls) -> None:
|
||||||
for image in LOCAL_ICONS_PATH.glob("*"):
|
for image in LOCAL_ICONS_PATH.glob("*"):
|
||||||
cls.images[image.stem] = str(image)
|
cls.images[image.stem] = str(image)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(
|
def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage:
|
||||||
cls, image_enum: Enum, width: int, height: int = None
|
|
||||||
) -> ImageTk.PhotoImage:
|
|
||||||
file_path = cls.images[image_enum.value]
|
file_path = cls.images[image_enum.value]
|
||||||
return cls.create(file_path, width, height)
|
return cls.create(file_path, width, height)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_with_image_file(
|
def get_with_image_file(
|
||||||
cls, stem: str, width: int, height: int = None
|
cls, stem: str, width: int, height: int = None
|
||||||
) -> ImageTk.PhotoImage:
|
) -> PhotoImage:
|
||||||
file_path = cls.images[stem]
|
file_path = cls.images[stem]
|
||||||
return cls.create(file_path, width, height)
|
return cls.create(file_path, width, height)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_custom(
|
def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage:
|
||||||
cls, name: str, width: int, height: int = None
|
|
||||||
) -> ImageTk.PhotoImage:
|
|
||||||
try:
|
try:
|
||||||
file_path = cls.images[name]
|
file_path = cls.images[name]
|
||||||
return cls.create(file_path, width, height)
|
return cls.create(file_path, width, height)
|
||||||
|
@ -95,22 +93,22 @@ class ImageEnum(Enum):
|
||||||
|
|
||||||
|
|
||||||
class TypeToImage:
|
class TypeToImage:
|
||||||
type_to_image = {
|
type_to_image: Dict[Tuple[NodeType, str], ImageEnum] = {
|
||||||
(core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
(NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
||||||
(core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
(NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
||||||
(core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
(NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||||
(core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
(NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||||
(core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
(NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||||
(core_pb2.NodeType.HUB, ""): ImageEnum.HUB,
|
(NodeType.HUB, ""): ImageEnum.HUB,
|
||||||
(core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
(NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||||
(core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
(NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||||
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
|
(NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||||
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
|
(NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||||
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
(NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||||
(core_pb2.NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
(NodeType.DOCKER, ""): ImageEnum.DOCKER,
|
||||||
(core_pb2.NodeType.LXC, ""): ImageEnum.LXC,
|
(NodeType.LXC, ""): ImageEnum.LXC,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, node_type, model):
|
def get(cls, node_type, model) -> Optional[ImageEnum]:
|
||||||
return cls.type_to_image.get((node_type, model), None)
|
return cls.type_to_image.get((node_type, model))
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
from netaddr import EUI, IPNetwork
|
from netaddr import EUI, IPNetwork
|
||||||
|
|
||||||
|
from core.api.grpc.core_pb2 import Interface, Link, Node
|
||||||
|
from core.gui.graph.node import CanvasNode
|
||||||
from core.gui.nodeutils import NodeUtils
|
from core.gui.nodeutils import NodeUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.api.grpc import core_pb2
|
|
||||||
from core.gui.graph.node import CanvasNode
|
|
||||||
|
|
||||||
|
|
||||||
def get_index(iface: "core_pb2.Interface") -> Optional[int]:
|
def get_index(iface: Interface) -> Optional[int]:
|
||||||
if not iface.ip4:
|
if not iface.ip4:
|
||||||
return None
|
return None
|
||||||
net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}")
|
net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}")
|
||||||
|
@ -44,18 +44,18 @@ class Subnets:
|
||||||
|
|
||||||
class InterfaceManager:
|
class InterfaceManager:
|
||||||
def __init__(self, app: "Application") -> None:
|
def __init__(self, app: "Application") -> None:
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
ip4 = self.app.guiconfig.ips.ip4
|
ip4 = self.app.guiconfig.ips.ip4
|
||||||
ip6 = self.app.guiconfig.ips.ip6
|
ip6 = self.app.guiconfig.ips.ip6
|
||||||
self.ip4_mask = 24
|
self.ip4_mask: int = 24
|
||||||
self.ip6_mask = 64
|
self.ip6_mask: int = 64
|
||||||
self.ip4_subnets = IPNetwork(f"{ip4}/{self.ip4_mask}")
|
self.ip4_subnets: IPNetwork = IPNetwork(f"{ip4}/{self.ip4_mask}")
|
||||||
self.ip6_subnets = IPNetwork(f"{ip6}/{self.ip6_mask}")
|
self.ip6_subnets: IPNetwork = IPNetwork(f"{ip6}/{self.ip6_mask}")
|
||||||
mac = self.app.guiconfig.mac
|
mac = self.app.guiconfig.mac
|
||||||
self.mac = EUI(mac, dialect=netaddr.mac_unix_expanded)
|
self.mac: EUI = EUI(mac, dialect=netaddr.mac_unix_expanded)
|
||||||
self.current_mac = None
|
self.current_mac: Optional[EUI] = None
|
||||||
self.current_subnets = None
|
self.current_subnets: Optional[Subnets] = None
|
||||||
self.used_subnets = {}
|
self.used_subnets: Dict[Tuple[IPNetwork, IPNetwork], Subnets] = {}
|
||||||
|
|
||||||
def update_ips(self, ip4: str, ip6: str) -> None:
|
def update_ips(self, ip4: str, ip6: str) -> None:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
@ -84,7 +84,7 @@ class InterfaceManager:
|
||||||
self.current_subnets = None
|
self.current_subnets = None
|
||||||
self.used_subnets.clear()
|
self.used_subnets.clear()
|
||||||
|
|
||||||
def removed(self, links: List["core_pb2.Link"]) -> None:
|
def removed(self, links: List[Link]) -> None:
|
||||||
# get remaining subnets
|
# get remaining subnets
|
||||||
remaining_subnets = set()
|
remaining_subnets = set()
|
||||||
for edge in self.app.core.links.values():
|
for edge in self.app.core.links.values():
|
||||||
|
@ -114,7 +114,7 @@ class InterfaceManager:
|
||||||
subnets.used_indexes.discard(index)
|
subnets.used_indexes.discard(index)
|
||||||
self.current_subnets = None
|
self.current_subnets = None
|
||||||
|
|
||||||
def joined(self, links: List["core_pb2.Link"]) -> None:
|
def joined(self, links: List[Link]) -> None:
|
||||||
ifaces = []
|
ifaces = []
|
||||||
for link in links:
|
for link in links:
|
||||||
if link.HasField("iface1"):
|
if link.HasField("iface1"):
|
||||||
|
@ -132,7 +132,7 @@ class InterfaceManager:
|
||||||
if subnets.key() not in self.used_subnets:
|
if subnets.key() not in self.used_subnets:
|
||||||
self.used_subnets[subnets.key()] = subnets
|
self.used_subnets[subnets.key()] = subnets
|
||||||
|
|
||||||
def next_index(self, node: "core_pb2.Node") -> int:
|
def next_index(self, node: Node) -> int:
|
||||||
if NodeUtils.is_router_node(node):
|
if NodeUtils.is_router_node(node):
|
||||||
index = 1
|
index = 1
|
||||||
else:
|
else:
|
||||||
|
@ -144,13 +144,13 @@ class InterfaceManager:
|
||||||
index += 1
|
index += 1
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def get_ips(self, node: "core_pb2.Node") -> [str, str]:
|
def get_ips(self, node: Node) -> [str, str]:
|
||||||
index = self.next_index(node)
|
index = self.next_index(node)
|
||||||
ip4 = self.current_subnets.ip4[index]
|
ip4 = self.current_subnets.ip4[index]
|
||||||
ip6 = self.current_subnets.ip6[index]
|
ip6 = self.current_subnets.ip6[index]
|
||||||
return str(ip4), str(ip6)
|
return str(ip4), str(ip6)
|
||||||
|
|
||||||
def get_subnets(self, iface: "core_pb2.Interface") -> Subnets:
|
def get_subnets(self, iface: Interface) -> Subnets:
|
||||||
ip4_subnet = self.ip4_subnets
|
ip4_subnet = self.ip4_subnets
|
||||||
if iface.ip4:
|
if iface.ip4:
|
||||||
ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr
|
ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr
|
||||||
|
@ -161,7 +161,7 @@ class InterfaceManager:
|
||||||
return self.used_subnets.get(subnets.key(), subnets)
|
return self.used_subnets.get(subnets.key(), subnets)
|
||||||
|
|
||||||
def determine_subnets(
|
def determine_subnets(
|
||||||
self, canvas_src_node: "CanvasNode", canvas_dst_node: "CanvasNode"
|
self, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
|
||||||
) -> None:
|
) -> None:
|
||||||
src_node = canvas_src_node.core_node
|
src_node = canvas_src_node.core_node
|
||||||
dst_node = canvas_dst_node.core_node
|
dst_node = canvas_dst_node.core_node
|
||||||
|
@ -185,7 +185,7 @@ class InterfaceManager:
|
||||||
logging.info("ignoring subnet change for link between network nodes")
|
logging.info("ignoring subnet change for link between network nodes")
|
||||||
|
|
||||||
def find_subnets(
|
def find_subnets(
|
||||||
self, canvas_node: "CanvasNode", visited: Set[int] = None
|
self, canvas_node: CanvasNode, visited: Set[int] = None
|
||||||
) -> Optional[IPNetwork]:
|
) -> Optional[IPNetwork]:
|
||||||
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
|
logging.info("finding subnet for node: %s", canvas_node.core_node.name)
|
||||||
canvas = self.app.canvas
|
canvas = self.app.canvas
|
||||||
|
|
|
@ -4,9 +4,10 @@ import tkinter as tk
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from core.gui.appconfig import XMLS_PATH
|
from core.gui.appconfig import XMLS_PATH
|
||||||
|
from core.gui.coreclient import CoreClient
|
||||||
from core.gui.dialogs.about import AboutDialog
|
from core.gui.dialogs.about import AboutDialog
|
||||||
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
||||||
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
|
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
|
||||||
|
@ -22,6 +23,7 @@ from core.gui.dialogs.servers import ServersDialog
|
||||||
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
|
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
|
||||||
from core.gui.dialogs.sessions import SessionsDialog
|
from core.gui.dialogs.sessions import SessionsDialog
|
||||||
from core.gui.dialogs.throughput import ThroughputDialog
|
from core.gui.dialogs.throughput import ThroughputDialog
|
||||||
|
from core.gui.graph.graph import CanvasGraph
|
||||||
from core.gui.nodeutils import ICON_SIZE
|
from core.gui.nodeutils import ICON_SIZE
|
||||||
from core.gui.observers import ObserversMenu
|
from core.gui.observers import ObserversMenu
|
||||||
from core.gui.task import ProgressTask
|
from core.gui.task import ProgressTask
|
||||||
|
@ -29,7 +31,7 @@ from core.gui.task import ProgressTask
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
MAX_FILES = 3
|
MAX_FILES: int = 3
|
||||||
|
|
||||||
|
|
||||||
class Menubar(tk.Menu):
|
class Menubar(tk.Menu):
|
||||||
|
@ -42,12 +44,12 @@ class Menubar(tk.Menu):
|
||||||
Create a CoreMenubar instance
|
Create a CoreMenubar instance
|
||||||
"""
|
"""
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.core = app.core
|
self.core: CoreClient = app.core
|
||||||
self.canvas = app.canvas
|
self.canvas: CanvasGraph = app.canvas
|
||||||
self.recent_menu = None
|
self.recent_menu: Optional[tk.Menu] = None
|
||||||
self.edit_menu = None
|
self.edit_menu: Optional[tk.Menu] = None
|
||||||
self.observers_menu = None
|
self.observers_menu: Optional[tk.Menu] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
|
|
|
@ -1,38 +1,36 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, List, Optional, Set
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
from core.api.grpc.core_pb2 import Node, NodeType
|
from core.api.grpc.core_pb2 import Node, NodeType
|
||||||
from core.gui.appconfig import CustomNode, GuiConfig
|
from core.gui.appconfig import CustomNode, GuiConfig
|
||||||
from core.gui.images import ImageEnum, Images, TypeToImage
|
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
ICON_SIZE: int = 48
|
||||||
from core.api.grpc import core_pb2
|
ANTENNA_SIZE: int = 32
|
||||||
from PIL import ImageTk
|
|
||||||
|
|
||||||
ICON_SIZE = 48
|
|
||||||
ANTENNA_SIZE = 32
|
|
||||||
|
|
||||||
|
|
||||||
class NodeDraw:
|
class NodeDraw:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.custom: bool = False
|
self.custom: bool = False
|
||||||
self.image = None
|
self.image: Optional[str] = None
|
||||||
self.image_enum: Optional[ImageEnum] = None
|
self.image_enum: Optional[ImageEnum] = None
|
||||||
self.image_file = None
|
self.image_file: Optional[str] = None
|
||||||
self.node_type: core_pb2.NodeType = None
|
self.node_type: NodeType = None
|
||||||
self.model: Optional[str] = None
|
self.model: Optional[str] = None
|
||||||
self.services: Set[str] = set()
|
self.services: Set[str] = set()
|
||||||
self.label = None
|
self.label: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_setup(
|
def from_setup(
|
||||||
cls,
|
cls,
|
||||||
image_enum: ImageEnum,
|
image_enum: ImageEnum,
|
||||||
node_type: "core_pb2.NodeType",
|
node_type: NodeType,
|
||||||
label: str,
|
label: str,
|
||||||
model: str = None,
|
model: str = None,
|
||||||
tooltip=None,
|
tooltip: str = None,
|
||||||
):
|
) -> "NodeDraw":
|
||||||
node_draw = NodeDraw()
|
node_draw = NodeDraw()
|
||||||
node_draw.image_enum = image_enum
|
node_draw.image_enum = image_enum
|
||||||
node_draw.image = Images.get(image_enum, ICON_SIZE)
|
node_draw.image = Images.get(image_enum, ICON_SIZE)
|
||||||
|
@ -43,7 +41,7 @@ class NodeDraw:
|
||||||
return node_draw
|
return node_draw
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_custom(cls, custom_node: CustomNode):
|
def from_custom(cls, custom_node: CustomNode) -> "NodeDraw":
|
||||||
node_draw = NodeDraw()
|
node_draw = NodeDraw()
|
||||||
node_draw.custom = True
|
node_draw.custom = True
|
||||||
node_draw.image_file = custom_node.image
|
node_draw.image_file = custom_node.image
|
||||||
|
@ -57,17 +55,17 @@ class NodeDraw:
|
||||||
|
|
||||||
|
|
||||||
class NodeUtils:
|
class NodeUtils:
|
||||||
NODES = []
|
NODES: List[NodeDraw] = []
|
||||||
NETWORK_NODES = []
|
NETWORK_NODES: List[NodeDraw] = []
|
||||||
NODE_ICONS = {}
|
NODE_ICONS = {}
|
||||||
CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
CONTAINER_NODES: Set[NodeType] = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC}
|
||||||
IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC}
|
IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC}
|
||||||
WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||||
RJ45_NODES = {NodeType.RJ45}
|
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
||||||
IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER}
|
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER}
|
||||||
NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"}
|
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
|
||||||
ROUTER_NODES = {"router", "mdr"}
|
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
||||||
ANTENNA_ICON = None
|
ANTENNA_ICON: PhotoImage = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_router_node(cls, node: Node) -> bool:
|
def is_router_node(cls, node: Node) -> bool:
|
||||||
|
@ -99,8 +97,8 @@ class NodeUtils:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def node_icon(
|
def node_icon(
|
||||||
cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale=1.0
|
cls, node_type: NodeType, model: str, gui_config: GuiConfig, scale: float = 1.0
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> PhotoImage:
|
||||||
|
|
||||||
image_enum = TypeToImage.get(node_type, model)
|
image_enum = TypeToImage.get(node_type, model)
|
||||||
if image_enum:
|
if image_enum:
|
||||||
|
@ -112,8 +110,8 @@ class NodeUtils:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def node_image(
|
def node_image(
|
||||||
cls, core_node: "core_pb2.Node", gui_config: GuiConfig, scale=1.0
|
cls, core_node: Node, gui_config: GuiConfig, scale: float = 1.0
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> PhotoImage:
|
||||||
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
|
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
|
||||||
if core_node.icon:
|
if core_node.icon:
|
||||||
try:
|
try:
|
||||||
|
@ -141,7 +139,7 @@ class NodeUtils:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup(cls):
|
def setup(cls) -> None:
|
||||||
nodes = [
|
nodes = [
|
||||||
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
(ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"),
|
||||||
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
|
(ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from core.gui.dialogs.observers import ObserverDialog
|
from core.gui.dialogs.observers import ObserverDialog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
OBSERVERS = {
|
OBSERVERS: Dict[str, str] = {
|
||||||
"List Processes": "ps",
|
"List Processes": "ps",
|
||||||
"Show Interfaces": "ip address",
|
"Show Interfaces": "ip address",
|
||||||
"IPV4 Routes": "ip -4 route",
|
"IPV4 Routes": "ip -4 route",
|
||||||
|
@ -23,9 +23,9 @@ OBSERVERS = {
|
||||||
class ObserversMenu(tk.Menu):
|
class ObserversMenu(tk.Menu):
|
||||||
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.observer = tk.StringVar(value=tk.NONE)
|
self.observer: tk.StringVar = tk.StringVar(value=tk.NONE)
|
||||||
self.custom_index = 0
|
self.custom_index: int = 0
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
|
|
|
@ -3,8 +3,9 @@ status bar
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
from core.api.grpc.core_pb2 import ExceptionEvent
|
||||||
from core.gui.dialogs.alerts import AlertsDialog
|
from core.gui.dialogs.alerts import AlertsDialog
|
||||||
from core.gui.themes import Styles
|
from core.gui.themes import Styles
|
||||||
|
|
||||||
|
@ -13,20 +14,19 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(ttk.Frame):
|
class StatusBar(ttk.Frame):
|
||||||
def __init__(self, master: tk.Widget, app: "Application"):
|
def __init__(self, master: tk.Widget, app: "Application") -> None:
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.status = None
|
self.status: Optional[ttk.Label] = None
|
||||||
self.statusvar = tk.StringVar()
|
self.statusvar: tk.StringVar = tk.StringVar()
|
||||||
self.zoom = None
|
self.zoom: Optional[ttk.Label] = None
|
||||||
self.cpu_usage = None
|
self.cpu_usage: Optional[ttk.Label] = None
|
||||||
self.memory = None
|
self.alerts_button: Optional[ttk.Button] = None
|
||||||
self.alerts_button = None
|
self.running: bool = False
|
||||||
self.running = False
|
self.core_alarms: List[ExceptionEvent] = []
|
||||||
self.core_alarms = []
|
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self) -> None:
|
||||||
self.columnconfigure(0, weight=7)
|
self.columnconfigure(0, weight=7)
|
||||||
self.columnconfigure(1, weight=1)
|
self.columnconfigure(1, weight=1)
|
||||||
self.columnconfigure(2, weight=1)
|
self.columnconfigure(2, weight=1)
|
||||||
|
@ -64,9 +64,9 @@ class StatusBar(ttk.Frame):
|
||||||
)
|
)
|
||||||
self.alerts_button.grid(row=0, column=3, sticky="ew")
|
self.alerts_button.grid(row=0, column=3, sticky="ew")
|
||||||
|
|
||||||
def click_alerts(self):
|
def click_alerts(self) -> None:
|
||||||
dialog = AlertsDialog(self.app)
|
dialog = AlertsDialog(self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def set_status(self, message: str):
|
def set_status(self, message: str) -> None:
|
||||||
self.statusvar.set(message)
|
self.statusvar.set(message)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Tuple
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -16,14 +16,14 @@ class ProgressTask:
|
||||||
callback: Callable = None,
|
callback: Callable = None,
|
||||||
args: Tuple[Any] = None,
|
args: Tuple[Any] = None,
|
||||||
):
|
):
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.title = title
|
self.title: str = title
|
||||||
self.task = task
|
self.task: Callable = task
|
||||||
self.callback = callback
|
self.callback: Callable = callback
|
||||||
self.args = args
|
if args is None:
|
||||||
if self.args is None:
|
args = ()
|
||||||
self.args = ()
|
self.args: Tuple[Any] = args
|
||||||
self.time = None
|
self.time: Optional[float] = None
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.app.progress.grid(sticky="ew")
|
self.app.progress.grid(sticky="ew")
|
||||||
|
@ -49,7 +49,7 @@ class ProgressTask:
|
||||||
finally:
|
finally:
|
||||||
self.app.after(0, self.complete)
|
self.app.after(0, self.complete)
|
||||||
|
|
||||||
def complete(self):
|
def complete(self) -> None:
|
||||||
self.app.progress.stop()
|
self.app.progress.stop()
|
||||||
self.app.progress.grid_forget()
|
self.app.progress.grid_forget()
|
||||||
total = time.perf_counter() - self.time
|
total = time.perf_counter() - self.time
|
||||||
|
|
|
@ -1,39 +1,40 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font, ttk
|
from tkinter import font, ttk
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
THEME_DARK = "black"
|
THEME_DARK: str = "black"
|
||||||
PADX = (0, 5)
|
PADX: Tuple[int, int] = (0, 5)
|
||||||
PADY = (0, 5)
|
PADY: Tuple[int, int] = (0, 5)
|
||||||
FRAME_PAD = 5
|
FRAME_PAD: int = 5
|
||||||
DIALOG_PAD = 5
|
DIALOG_PAD: int = 5
|
||||||
|
|
||||||
|
|
||||||
class Styles:
|
class Styles:
|
||||||
tooltip = "Tooltip.TLabel"
|
tooltip: str = "Tooltip.TLabel"
|
||||||
tooltip_frame = "Tooltip.TFrame"
|
tooltip_frame: str = "Tooltip.TFrame"
|
||||||
service_checkbutton = "Service.TCheckbutton"
|
service_checkbutton: str = "Service.TCheckbutton"
|
||||||
picker_button = "Picker.TButton"
|
picker_button: str = "Picker.TButton"
|
||||||
green_alert = "GAlert.TButton"
|
green_alert: str = "GAlert.TButton"
|
||||||
red_alert = "RAlert.TButton"
|
red_alert: str = "RAlert.TButton"
|
||||||
yellow_alert = "YAlert.TButton"
|
yellow_alert: str = "YAlert.TButton"
|
||||||
|
|
||||||
|
|
||||||
class Colors:
|
class Colors:
|
||||||
disabledfg = "DarkGrey"
|
disabledfg: str = "DarkGrey"
|
||||||
frame = "#424242"
|
frame: str = "#424242"
|
||||||
dark = "#222222"
|
dark: str = "#222222"
|
||||||
darker = "#121212"
|
darker: str = "#121212"
|
||||||
darkest = "black"
|
darkest: str = "black"
|
||||||
lighter = "#626262"
|
lighter: str = "#626262"
|
||||||
lightest = "#ffffff"
|
lightest: str = "#ffffff"
|
||||||
selectbg = "#4a6984"
|
selectbg: str = "#4a6984"
|
||||||
selectfg = "#ffffff"
|
selectfg: str = "#ffffff"
|
||||||
white = "white"
|
white: str = "white"
|
||||||
black = "black"
|
black: str = "black"
|
||||||
listboxbg = "#f2f1f0"
|
listboxbg: str = "#f2f1f0"
|
||||||
|
|
||||||
|
|
||||||
def load(style: ttk.Style):
|
def load(style: ttk.Style) -> None:
|
||||||
style.theme_create(
|
style.theme_create(
|
||||||
THEME_DARK,
|
THEME_DARK,
|
||||||
"clam",
|
"clam",
|
||||||
|
@ -139,13 +140,13 @@ def load(style: ttk.Style):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def theme_change_menu(event: tk.Event):
|
def theme_change_menu(event: tk.Event) -> None:
|
||||||
if not isinstance(event.widget, tk.Menu):
|
if not isinstance(event.widget, tk.Menu):
|
||||||
return
|
return
|
||||||
style_menu(event.widget)
|
style_menu(event.widget)
|
||||||
|
|
||||||
|
|
||||||
def style_menu(widget: tk.Widget):
|
def style_menu(widget: tk.Widget) -> None:
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
bg = style.lookup(".", "background")
|
bg = style.lookup(".", "background")
|
||||||
fg = style.lookup(".", "foreground")
|
fg = style.lookup(".", "foreground")
|
||||||
|
@ -157,7 +158,7 @@ def style_menu(widget: tk.Widget):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def style_listbox(widget: tk.Widget):
|
def style_listbox(widget: tk.Widget) -> None:
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
bg = style.lookup(".", "background")
|
bg = style.lookup(".", "background")
|
||||||
fg = style.lookup(".", "foreground")
|
fg = style.lookup(".", "foreground")
|
||||||
|
@ -174,7 +175,7 @@ def style_listbox(widget: tk.Widget):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def theme_change(event: tk.Event):
|
def theme_change(event: tk.Event) -> None:
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
style.configure(Styles.picker_button, font="TkSmallCaptionFont")
|
style.configure(Styles.picker_button, font="TkSmallCaptionFont")
|
||||||
style.configure(
|
style.configure(
|
||||||
|
@ -203,7 +204,7 @@ def theme_change(event: tk.Event):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def scale_fonts(fonts_size, scale):
|
def scale_fonts(fonts_size: Dict[str, int], scale: float) -> None:
|
||||||
for name in font.names():
|
for name in font.names():
|
||||||
f = font.nametofont(name)
|
f = font.nametofont(name)
|
||||||
if name in fonts_size:
|
if name in fonts_size:
|
||||||
|
|
|
@ -3,7 +3,7 @@ import tkinter as tk
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from typing import TYPE_CHECKING, Callable
|
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||||
|
|
||||||
from PIL.ImageTk import PhotoImage
|
from PIL.ImageTk import PhotoImage
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ from core.gui.tooltip import Tooltip
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
|
||||||
TOOLBAR_SIZE = 32
|
TOOLBAR_SIZE: int = 32
|
||||||
PICKER_SIZE = 24
|
PICKER_SIZE: int = 24
|
||||||
|
|
||||||
|
|
||||||
class NodeTypeEnum(Enum):
|
class NodeTypeEnum(Enum):
|
||||||
|
@ -42,8 +42,8 @@ def enable_buttons(frame: ttk.Frame, enabled: bool) -> None:
|
||||||
class PickerFrame(ttk.Frame):
|
class PickerFrame(ttk.Frame):
|
||||||
def __init__(self, app: "Application", button: ttk.Button) -> None:
|
def __init__(self, app: "Application", button: ttk.Button) -> None:
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.button = button
|
self.button: ttk.Button = button
|
||||||
|
|
||||||
def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None:
|
def create_node_button(self, node_draw: NodeDraw, func: Callable) -> None:
|
||||||
self.create_button(
|
self.create_button(
|
||||||
|
@ -85,10 +85,10 @@ class PickerFrame(ttk.Frame):
|
||||||
|
|
||||||
|
|
||||||
class ButtonBar(ttk.Frame):
|
class ButtonBar(ttk.Frame):
|
||||||
def __init__(self, master: tk.Widget, app: "Application"):
|
def __init__(self, master: tk.Widget, app: "Application") -> None:
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.radio_buttons = []
|
self.radio_buttons: List[ttk.Button] = []
|
||||||
|
|
||||||
def create_button(
|
def create_button(
|
||||||
self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
|
self, image_enum: ImageEnum, func: Callable, tooltip: str, radio: bool = False
|
||||||
|
@ -109,14 +109,14 @@ class ButtonBar(ttk.Frame):
|
||||||
|
|
||||||
|
|
||||||
class MarkerFrame(ttk.Frame):
|
class MarkerFrame(ttk.Frame):
|
||||||
PAD = 3
|
PAD: int = 3
|
||||||
|
|
||||||
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
|
||||||
super().__init__(master, padding=self.PAD)
|
super().__init__(master, padding=self.PAD)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.color = "#000000"
|
self.color: str = "#000000"
|
||||||
self.size = tk.DoubleVar()
|
self.size: tk.DoubleVar = tk.DoubleVar()
|
||||||
self.color_frame = None
|
self.color_frame: Optional[tk.Frame] = None
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
|
@ -144,7 +144,7 @@ class MarkerFrame(ttk.Frame):
|
||||||
self.color_frame.bind("<Button-1>", self.click_color)
|
self.color_frame.bind("<Button-1>", self.click_color)
|
||||||
Tooltip(self.color_frame, "Marker Color")
|
Tooltip(self.color_frame, "Marker Color")
|
||||||
|
|
||||||
def click_clear(self):
|
def click_clear(self) -> None:
|
||||||
self.app.canvas.delete(tags.MARKER)
|
self.app.canvas.delete(tags.MARKER)
|
||||||
|
|
||||||
def click_color(self, _event: tk.Event) -> None:
|
def click_color(self, _event: tk.Event) -> None:
|
||||||
|
@ -163,37 +163,37 @@ class Toolbar(ttk.Frame):
|
||||||
Create a CoreToolbar instance
|
Create a CoreToolbar instance
|
||||||
"""
|
"""
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
|
|
||||||
# design buttons
|
# design buttons
|
||||||
self.play_button = None
|
self.play_button: Optional[ttk.Button] = None
|
||||||
self.select_button = None
|
self.select_button: Optional[ttk.Button] = None
|
||||||
self.link_button = None
|
self.link_button: Optional[ttk.Button] = None
|
||||||
self.node_button = None
|
self.node_button: Optional[ttk.Button] = None
|
||||||
self.network_button = None
|
self.network_button: Optional[ttk.Button] = None
|
||||||
self.annotation_button = None
|
self.annotation_button: Optional[ttk.Button] = None
|
||||||
|
|
||||||
# runtime buttons
|
# runtime buttons
|
||||||
self.runtime_select_button = None
|
self.runtime_select_button: Optional[ttk.Button] = None
|
||||||
self.stop_button = None
|
self.stop_button: Optional[ttk.Button] = None
|
||||||
self.runtime_marker_button = None
|
self.runtime_marker_button: Optional[ttk.Button] = None
|
||||||
self.run_command_button = None
|
self.run_command_button: Optional[ttk.Button] = None
|
||||||
|
|
||||||
# frames
|
# frames
|
||||||
self.design_frame = None
|
self.design_frame: Optional[ButtonBar] = None
|
||||||
self.runtime_frame = None
|
self.runtime_frame: Optional[ButtonBar] = None
|
||||||
self.marker_frame = None
|
self.marker_frame: Optional[MarkerFrame] = None
|
||||||
self.picker = None
|
self.picker: Optional[PickerFrame] = None
|
||||||
|
|
||||||
# observers
|
# observers
|
||||||
self.observers_menu = None
|
self.observers_menu: Optional[ObserversMenu] = None
|
||||||
|
|
||||||
# these variables help keep track of what images being drawn so that scaling
|
# these variables help keep track of what images being drawn so that scaling
|
||||||
# is possible since PhotoImage does not have resize method
|
# is possible since PhotoImage does not have resize method
|
||||||
self.current_node = NodeUtils.NODES[0]
|
self.current_node: NodeDraw = NodeUtils.NODES[0]
|
||||||
self.current_network = NodeUtils.NETWORK_NODES[0]
|
self.current_network: NodeDraw = NodeUtils.NETWORK_NODES[0]
|
||||||
self.current_annotation = ShapeType.MARKER
|
self.current_annotation: ShapeType = ShapeType.MARKER
|
||||||
self.annotation_enum = ImageEnum.MARKER
|
self.annotation_enum: ImageEnum = ImageEnum.MARKER
|
||||||
|
|
||||||
# draw components
|
# draw components
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from core.gui.themes import Styles
|
from core.gui.themes import Styles
|
||||||
|
|
||||||
|
@ -9,19 +10,19 @@ class Tooltip(object):
|
||||||
Create tool tip for a given widget
|
Create tool tip for a given widget
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, widget: tk.Widget, text: str = "widget info"):
|
def __init__(self, widget: tk.BaseWidget, text: str = "widget info") -> None:
|
||||||
self.widget = widget
|
self.widget: tk.BaseWidget = widget
|
||||||
self.text = text
|
self.text: str = text
|
||||||
self.widget.bind("<Enter>", self.on_enter)
|
self.widget.bind("<Enter>", self.on_enter)
|
||||||
self.widget.bind("<Leave>", self.on_leave)
|
self.widget.bind("<Leave>", self.on_leave)
|
||||||
self.waittime = 400
|
self.waittime: int = 400
|
||||||
self.id = None
|
self.id: Optional[str] = None
|
||||||
self.tw = None
|
self.tw: Optional[tk.Toplevel] = None
|
||||||
|
|
||||||
def on_enter(self, event: tk.Event = None):
|
def on_enter(self, event: tk.Event = None) -> None:
|
||||||
self.schedule()
|
self.schedule()
|
||||||
|
|
||||||
def on_leave(self, event: tk.Event = None):
|
def on_leave(self, event: tk.Event = None) -> None:
|
||||||
self.unschedule()
|
self.unschedule()
|
||||||
self.close(event)
|
self.close(event)
|
||||||
|
|
||||||
|
@ -39,7 +40,6 @@ class Tooltip(object):
|
||||||
x, y, cx, cy = self.widget.bbox("insert")
|
x, y, cx, cy = self.widget.bbox("insert")
|
||||||
x += self.widget.winfo_rootx()
|
x += self.widget.winfo_rootx()
|
||||||
y += self.widget.winfo_rooty() + 32
|
y += self.widget.winfo_rooty() + 32
|
||||||
|
|
||||||
self.tw = tk.Toplevel(self.widget)
|
self.tw = tk.Toplevel(self.widget)
|
||||||
self.tw.wm_overrideredirect(True)
|
self.tw.wm_overrideredirect(True)
|
||||||
self.tw.wm_geometry("+%d+%d" % (x, y))
|
self.tw.wm_geometry("+%d+%d" % (x, y))
|
||||||
|
|
|
@ -4,16 +4,23 @@ input validation
|
||||||
import re
|
import re
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
from typing import Any, Optional, Pattern
|
||||||
|
|
||||||
SMALLEST_SCALE = 0.5
|
SMALLEST_SCALE: float = 0.5
|
||||||
LARGEST_SCALE = 5.0
|
LARGEST_SCALE: float = 5.0
|
||||||
HEX_REGEX = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$")
|
HEX_REGEX: Pattern = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$")
|
||||||
|
|
||||||
|
|
||||||
class ValidationEntry(ttk.Entry):
|
class ValidationEntry(ttk.Entry):
|
||||||
empty = None
|
empty: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self, master=None, widget=None, empty_enabled=True, **kwargs) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
master: tk.BaseWidget = None,
|
||||||
|
widget: tk.BaseWidget = None,
|
||||||
|
empty_enabled: bool = True,
|
||||||
|
**kwargs: Any
|
||||||
|
) -> None:
|
||||||
super().__init__(master, widget, **kwargs)
|
super().__init__(master, widget, **kwargs)
|
||||||
cmd = self.register(self.is_valid)
|
cmd = self.register(self.is_valid)
|
||||||
self.configure(validate="key", validatecommand=(cmd, "%P"))
|
self.configure(validate="key", validatecommand=(cmd, "%P"))
|
||||||
|
@ -30,7 +37,7 @@ class ValidationEntry(ttk.Entry):
|
||||||
|
|
||||||
|
|
||||||
class PositiveIntEntry(ValidationEntry):
|
class PositiveIntEntry(ValidationEntry):
|
||||||
empty = "0"
|
empty: str = "0"
|
||||||
|
|
||||||
def is_valid(self, s: str) -> bool:
|
def is_valid(self, s: str) -> bool:
|
||||||
if not s:
|
if not s:
|
||||||
|
@ -92,7 +99,7 @@ class HexEntry(ValidationEntry):
|
||||||
|
|
||||||
|
|
||||||
class NodeNameEntry(ValidationEntry):
|
class NodeNameEntry(ValidationEntry):
|
||||||
empty = "noname"
|
empty: str = "noname"
|
||||||
|
|
||||||
def is_valid(self, s: str) -> bool:
|
def is_valid(self, s: str) -> bool:
|
||||||
if len(s) < 0:
|
if len(s) < 0:
|
||||||
|
|
|
@ -1,53 +1,63 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import PosixPath
|
from pathlib import Path
|
||||||
from tkinter import filedialog, font, ttk
|
from tkinter import filedialog, font, ttk
|
||||||
from typing import TYPE_CHECKING, Dict
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type
|
||||||
|
|
||||||
from core.api.grpc import common_pb2, core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
from core.api.grpc.common_pb2 import ConfigOption
|
||||||
|
from core.api.grpc.core_pb2 import ConfigOptionType
|
||||||
from core.gui import themes, validation
|
from core.gui import themes, validation
|
||||||
|
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
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
from core.gui.dialogs.dialog import Dialog
|
|
||||||
|
|
||||||
INT_TYPES = {
|
INT_TYPES: Set[ConfigOptionType] = {
|
||||||
core_pb2.ConfigOptionType.UINT8,
|
ConfigOptionType.UINT8,
|
||||||
core_pb2.ConfigOptionType.UINT16,
|
ConfigOptionType.UINT16,
|
||||||
core_pb2.ConfigOptionType.UINT32,
|
ConfigOptionType.UINT32,
|
||||||
core_pb2.ConfigOptionType.UINT64,
|
ConfigOptionType.UINT64,
|
||||||
core_pb2.ConfigOptionType.INT8,
|
ConfigOptionType.INT8,
|
||||||
core_pb2.ConfigOptionType.INT16,
|
ConfigOptionType.INT16,
|
||||||
core_pb2.ConfigOptionType.INT32,
|
ConfigOptionType.INT32,
|
||||||
core_pb2.ConfigOptionType.INT64,
|
ConfigOptionType.INT64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def file_button_click(value: tk.StringVar, parent: tk.Widget):
|
def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None:
|
||||||
file_path = filedialog.askopenfilename(title="Select File", parent=parent)
|
file_path = filedialog.askopenfilename(title="Select File", parent=parent)
|
||||||
if file_path:
|
if file_path:
|
||||||
value.set(file_path)
|
value.set(file_path)
|
||||||
|
|
||||||
|
|
||||||
class FrameScroll(ttk.Frame):
|
class FrameScroll(ttk.Frame):
|
||||||
def __init__(self, master: tk.Widget, app: "Application", _cls=ttk.Frame, **kw):
|
def __init__(
|
||||||
|
self,
|
||||||
|
master: tk.Widget,
|
||||||
|
app: "Application",
|
||||||
|
_cls: Type[ttk.Frame] = ttk.Frame,
|
||||||
|
**kw: Any
|
||||||
|
) -> None:
|
||||||
super().__init__(master, **kw)
|
super().__init__(master, **kw)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
bg = self.app.style.lookup(".", "background")
|
bg = self.app.style.lookup(".", "background")
|
||||||
self.canvas = tk.Canvas(self, highlightthickness=0, background=bg)
|
self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg)
|
||||||
self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2)
|
self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2)
|
||||||
self.canvas.columnconfigure(0, weight=1)
|
self.canvas.columnconfigure(0, weight=1)
|
||||||
self.canvas.rowconfigure(0, weight=1)
|
self.canvas.rowconfigure(0, weight=1)
|
||||||
self.scrollbar = ttk.Scrollbar(
|
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(
|
||||||
self, orient="vertical", command=self.canvas.yview
|
self, orient="vertical", command=self.canvas.yview
|
||||||
)
|
)
|
||||||
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
self.frame = _cls(self.canvas)
|
self.frame: ttk.Frame = _cls(self.canvas)
|
||||||
self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame)
|
self.frame_id: int = self.canvas.create_window(
|
||||||
|
0, 0, anchor="nw", window=self.frame
|
||||||
|
)
|
||||||
self.canvas.update_idletasks()
|
self.canvas.update_idletasks()
|
||||||
self.canvas.configure(
|
self.canvas.configure(
|
||||||
scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set
|
scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set
|
||||||
|
@ -55,16 +65,16 @@ class FrameScroll(ttk.Frame):
|
||||||
self.frame.bind("<Configure>", self._configure_frame)
|
self.frame.bind("<Configure>", self._configure_frame)
|
||||||
self.canvas.bind("<Configure>", self._configure_canvas)
|
self.canvas.bind("<Configure>", self._configure_canvas)
|
||||||
|
|
||||||
def _configure_frame(self, event: tk.Event):
|
def _configure_frame(self, event: tk.Event) -> None:
|
||||||
req_width = self.frame.winfo_reqwidth()
|
req_width = self.frame.winfo_reqwidth()
|
||||||
if req_width != self.canvas.winfo_reqwidth():
|
if req_width != self.canvas.winfo_reqwidth():
|
||||||
self.canvas.configure(width=req_width)
|
self.canvas.configure(width=req_width)
|
||||||
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||||
|
|
||||||
def _configure_canvas(self, event: tk.Event):
|
def _configure_canvas(self, event: tk.Event) -> None:
|
||||||
self.canvas.itemconfig(self.frame_id, width=event.width)
|
self.canvas.itemconfig(self.frame_id, width=event.width)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self) -> None:
|
||||||
for widget in self.frame.winfo_children():
|
for widget in self.frame.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
|
|
||||||
|
@ -74,15 +84,15 @@ class ConfigFrame(ttk.Notebook):
|
||||||
self,
|
self,
|
||||||
master: tk.Widget,
|
master: tk.Widget,
|
||||||
app: "Application",
|
app: "Application",
|
||||||
config: Dict[str, common_pb2.ConfigOption],
|
config: Dict[str, ConfigOption],
|
||||||
**kw
|
**kw: Any
|
||||||
):
|
) -> None:
|
||||||
super().__init__(master, **kw)
|
super().__init__(master, **kw)
|
||||||
self.app = app
|
self.app: "Application" = app
|
||||||
self.config = config
|
self.config: Dict[str, ConfigOption] = config
|
||||||
self.values = {}
|
self.values: Dict[str, tk.StringVar] = {}
|
||||||
|
|
||||||
def draw_config(self):
|
def draw_config(self) -> None:
|
||||||
group_mapping = {}
|
group_mapping = {}
|
||||||
for key in self.config:
|
for key in self.config:
|
||||||
option = self.config[key]
|
option = self.config[key]
|
||||||
|
@ -142,7 +152,7 @@ class ConfigFrame(ttk.Notebook):
|
||||||
logging.error("unhandled config option type: %s", option.type)
|
logging.error("unhandled config option type: %s", option.type)
|
||||||
self.values[option.name] = value
|
self.values[option.name] = value
|
||||||
|
|
||||||
def parse_config(self):
|
def parse_config(self) -> Dict[str, str]:
|
||||||
for key in self.config:
|
for key in self.config:
|
||||||
option = self.config[key]
|
option = self.config[key]
|
||||||
value = self.values[key]
|
value = self.values[key]
|
||||||
|
@ -169,13 +179,13 @@ class ConfigFrame(ttk.Notebook):
|
||||||
|
|
||||||
|
|
||||||
class ListboxScroll(ttk.Frame):
|
class ListboxScroll(ttk.Frame):
|
||||||
def __init__(self, master: tk.Widget = None, **kw):
|
def __init__(self, master: tk.BaseWidget = None, **kw: Any) -> None:
|
||||||
super().__init__(master, **kw)
|
super().__init__(master, **kw)
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
|
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
|
||||||
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
self.listbox = tk.Listbox(
|
self.listbox: tk.Listbox = tk.Listbox(
|
||||||
self,
|
self,
|
||||||
selectmode=tk.BROWSE,
|
selectmode=tk.BROWSE,
|
||||||
yscrollcommand=self.scrollbar.set,
|
yscrollcommand=self.scrollbar.set,
|
||||||
|
@ -187,12 +197,18 @@ class ListboxScroll(ttk.Frame):
|
||||||
|
|
||||||
|
|
||||||
class CheckboxList(FrameScroll):
|
class CheckboxList(FrameScroll):
|
||||||
def __init__(self, master: ttk.Widget, app: "Application", clicked=None, **kw):
|
def __init__(
|
||||||
|
self,
|
||||||
|
master: ttk.Widget,
|
||||||
|
app: "Application",
|
||||||
|
clicked: Callable = None,
|
||||||
|
**kw: Any
|
||||||
|
) -> None:
|
||||||
super().__init__(master, app, **kw)
|
super().__init__(master, app, **kw)
|
||||||
self.clicked = clicked
|
self.clicked: Callable = clicked
|
||||||
self.frame.columnconfigure(0, weight=1)
|
self.frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
def add(self, name: str, checked: bool):
|
def add(self, name: str, checked: bool) -> None:
|
||||||
var = tk.BooleanVar(value=checked)
|
var = tk.BooleanVar(value=checked)
|
||||||
func = partial(self.clicked, name, var)
|
func = partial(self.clicked, name, var)
|
||||||
checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
|
checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
|
||||||
|
@ -200,16 +216,16 @@ class CheckboxList(FrameScroll):
|
||||||
|
|
||||||
|
|
||||||
class CodeFont(font.Font):
|
class CodeFont(font.Font):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__(font="TkFixedFont", color="green")
|
super().__init__(font="TkFixedFont", color="green")
|
||||||
|
|
||||||
|
|
||||||
class CodeText(ttk.Frame):
|
class CodeText(ttk.Frame):
|
||||||
def __init__(self, master: tk.Widget, **kwargs):
|
def __init__(self, master: tk.BaseWidget, **kwargs: Any) -> None:
|
||||||
super().__init__(master, **kwargs)
|
super().__init__(master, **kwargs)
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.text = tk.Text(
|
self.text: tk.Text = tk.Text(
|
||||||
self,
|
self,
|
||||||
bd=0,
|
bd=0,
|
||||||
bg="black",
|
bg="black",
|
||||||
|
@ -229,14 +245,14 @@ class CodeText(ttk.Frame):
|
||||||
|
|
||||||
|
|
||||||
class Spinbox(ttk.Entry):
|
class Spinbox(ttk.Entry):
|
||||||
def __init__(self, master: tk.Widget = None, **kwargs):
|
def __init__(self, master: tk.BaseWidget = None, **kwargs: Any) -> None:
|
||||||
super().__init__(master, "ttk::spinbox", **kwargs)
|
super().__init__(master, "ttk::spinbox", **kwargs)
|
||||||
|
|
||||||
def set(self, value):
|
def set(self, value: str) -> None:
|
||||||
self.tk.call(self._w, "set", value)
|
self.tk.call(self._w, "set", value)
|
||||||
|
|
||||||
|
|
||||||
def image_chooser(parent: "Dialog", path: PosixPath):
|
def image_chooser(parent: Dialog, path: Path) -> str:
|
||||||
return filedialog.askopenfilename(
|
return filedialog.askopenfilename(
|
||||||
parent=parent,
|
parent=parent,
|
||||||
initialdir=str(path),
|
initialdir=str(path),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue