pygui: refactored images.py and fixed issue with recreating a default config.yaml every time the gui was started

This commit is contained in:
Blake Harnden 2021-02-18 21:04:16 -08:00
parent 47ac4c850d
commit a6fadb76cc
16 changed files with 96 additions and 103 deletions

View file

@ -7,7 +7,7 @@ from typing import Any, Dict, Optional, Type
import grpc
from core.gui import appconfig
from core.gui import appconfig, images
from core.gui import nodeutils as nutils
from core.gui import themes
from core.gui.appconfig import GuiConfig
@ -16,7 +16,7 @@ from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
from core.gui.graph.manager import CanvasManager
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.menubar import Menubar
from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
@ -78,7 +78,7 @@ class Application(ttk.Frame):
self.master.title("CORE")
self.center()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
image = Images.get(ImageEnum.CORE, 16)
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
self.master.tk.call("wm", "iconphoto", self.master._w, image)
self.master.option_add("*tearOff", tk.FALSE)
self.setup_file_dialogs()
@ -197,10 +197,10 @@ class Application(ttk.Frame):
self.toolbar.set_design()
def get_icon(self, image_enum: ImageEnum, width: int) -> PhotoImage:
return Images.get(image_enum, int(width * self.app_scale))
return images.from_enum(image_enum, width=width, scale=self.app_scale)
def get_custom_icon(self, image_file: str, width: int) -> PhotoImage:
return Images.get_custom(image_file, int(width * self.app_scale))
return images.from_name(image_file, width=width, scale=self.app_scale)
def close(self) -> None:
self.master.destroy()

View file

@ -206,20 +206,19 @@ def check_directory() -> None:
MOBILITY_PATH.mkdir(exist_ok=True)
XMLS_PATH.mkdir(exist_ok=True)
SCRIPT_PATH.mkdir(exist_ok=True)
copy_files(LOCAL_ICONS_PATH, ICONS_PATH)
copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH)
copy_files(LOCAL_XMLS_PATH, XMLS_PATH)
copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH)
terminal = find_terminal()
if "EDITOR" in os.environ:
editor = EDITORS[0]
else:
editor = EDITORS[1]
preferences = PreferencesConfig(editor, terminal)
config = GuiConfig(preferences=preferences)
save(config)
if not CONFIG_PATH.exists():
terminal = find_terminal()
if "EDITOR" in os.environ:
editor = EDITORS[0]
else:
editor = EDITORS[1]
preferences = PreferencesConfig(editor, terminal)
config = GuiConfig(preferences=preferences)
save(config)
def read() -> GuiConfig:

View file

@ -6,10 +6,10 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, List, Optional
from core.gui import images
from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import Images
from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser
@ -132,7 +132,7 @@ class CanvasWallpaperDialog(Dialog):
self.draw_preview()
def draw_preview(self) -> None:
image = Images.create(self.filename.get(), 250, 135)
image = images.from_file(self.filename.get(), width=250, height=135)
self.image_label.config(image=image)
self.image_label.image = image

View file

@ -6,10 +6,9 @@ from typing import TYPE_CHECKING, Optional, Set
from PIL.ImageTk import PhotoImage
from core.gui import nodeutils
from core.gui import images
from core.gui.appconfig import ICONS_PATH, CustomNode
from core.gui.dialogs.dialog import Dialog
from core.gui.images import Images
from core.gui.nodeutils import NodeDraw
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser
@ -190,7 +189,7 @@ class CustomNodesDialog(Dialog):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE)
image = images.from_file(file_path, width=images.NODE_SIZE)
self.image = image
self.image_file = file_path
self.image_button.config(image=self.image)

View file

@ -2,7 +2,8 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.images import ImageEnum, Images
from core.gui import images
from core.gui.images import ImageEnum
from core.gui.themes import DIALOG_PAD
if TYPE_CHECKING:
@ -25,7 +26,7 @@ class Dialog(tk.Toplevel):
self.modal: bool = modal
self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy)
image = Images.get(ImageEnum.CORE, 16)
image = images.from_enum(ImageEnum.CORE, width=images.DIALOG_SIZE)
self.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)

View file

@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Dict, List, Optional
import grpc
from core.api.grpc.wrappers import ConfigOption, Node
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -143,7 +144,7 @@ class EmaneConfigDialog(Dialog):
)
label.grid(pady=PADY)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
button = ttk.Button(
self.top,
image=image,
@ -181,7 +182,7 @@ class EmaneConfigDialog(Dialog):
for i in range(2):
frame.columnconfigure(i, weight=1)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
self.emane_model_button = ttk.Button(
frame,
text=f"{self.emane_model.get()} options",
@ -192,7 +193,7 @@ class EmaneConfigDialog(Dialog):
self.emane_model_button.image = image
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.EDITNODE, 16)
image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE)
button = ttk.Button(
frame,
text="EMANE options",

View file

@ -2,8 +2,9 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import PADY
from core.gui.widgets import CodeText
@ -22,7 +23,7 @@ class ErrorDialog(Dialog):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1)
image = Images.get(ImageEnum.ERROR, 24)
image = images.from_enum(ImageEnum.ERROR, width=images.ERROR_SIZE)
label = ttk.Label(
self.top, text=self.title, image=image, compound=tk.LEFT, anchor=tk.CENTER
)

View file

@ -8,12 +8,12 @@ import netaddr
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Interface, Node
from core.gui import images
from core.gui import nodeutils as nutils
from core.gui import validation
from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.emaneconfig import EmaneModelDialog
from core.gui.images import Images
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll, image_chooser
@ -371,7 +371,7 @@ class NodeConfigDialog(Dialog):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
self.image = Images.create(file_path, nutils.ICON_SIZE)
self.image = images.from_file(file_path, width=images.NODE_SIZE)
self.image_button.config(image=self.image)
self.image_file = file_path

View file

@ -8,9 +8,10 @@ import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Node, NodeServiceData, ServiceValidationMode
from core.gui import images
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
@ -179,7 +180,7 @@ class ServiceConfigDialog(Dialog):
button.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, state=tk.DISABLED)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)
@ -194,11 +195,11 @@ class ServiceConfigDialog(Dialog):
value=2,
)
button.grid(row=0, column=0, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=1)
image = Images.get(ImageEnum.DOCUMENTSAVE, 16)
image = images.from_enum(ImageEnum.DOCUMENTSAVE, width=images.BUTTON_SIZE)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)

View file

@ -6,8 +6,9 @@ from typing import TYPE_CHECKING, List, Optional
import grpc
from core.api.grpc.wrappers import SessionState, SessionSummary
from core.gui import images
from core.gui.dialogs.dialog import Dialog
from core.gui.images import ImageEnum, Images
from core.gui.images import ImageEnum
from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY
@ -108,14 +109,14 @@ class SessionsDialog(Dialog):
frame.columnconfigure(i, weight=1)
frame.grid(sticky=tk.EW)
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
image = images.from_enum(ImageEnum.DOCUMENTNEW, width=images.BUTTON_SIZE)
b = ttk.Button(
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
)
b.image = image
b.grid(row=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
self.connect_button = ttk.Button(
frame,
image=image,
@ -127,7 +128,7 @@ class SessionsDialog(Dialog):
self.connect_button.image = image
self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.DELETE, 16)
image = images.from_enum(ImageEnum.DELETE, width=images.BUTTON_SIZE)
self.delete_button = ttk.Button(
frame,
image=image,
@ -139,7 +140,7 @@ class SessionsDialog(Dialog):
self.delete_button.image = image
self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.CANCEL, 16)
image = images.from_enum(ImageEnum.CANCEL, width=images.BUTTON_SIZE)
if self.is_start_app:
b = ttk.Button(
frame,

View file

@ -5,6 +5,7 @@ from tkinter import BooleanVar, messagebox, ttk
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, ValuesView
from core.api.grpc.wrappers import Link, LinkType, Node, Session, ThroughputsEvent
from core.gui import images
from core.gui import nodeutils as nutils
from core.gui.graph import tags
from core.gui.graph.edges import (
@ -18,7 +19,7 @@ from core.gui.graph.graph import CanvasGraph
from core.gui.graph.node import CanvasNode
from core.gui.graph.shapeutils import ShapeType
from core.gui.images import ImageEnum
from core.gui.nodeutils import ICON_SIZE, NodeDraw
from core.gui.nodeutils import NodeDraw
if TYPE_CHECKING:
from core.gui.app import Application
@ -303,7 +304,7 @@ class CanvasManager:
# if the gui can't find node's image, default to the "edit-node" image
image = nutils.get_icon(core_node, self.app.guiconfig, self.app.app_scale)
if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
image = self.app.get_icon(ImageEnum.EDITNODE, images.NODE_SIZE)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, canvas, x, y, core_node, image)

View file

@ -9,6 +9,7 @@ from PIL.ImageTk import PhotoImage
from core.api.grpc.services_pb2 import ServiceAction
from core.api.grpc.wrappers import Interface, Node, NodeType
from core.gui import images
from core.gui import nodeutils as nutils
from core.gui import themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
@ -21,8 +22,7 @@ from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, ICON_SIZE
from core.gui.images import ImageEnum
if TYPE_CHECKING:
from core.gui.app import Application
@ -95,7 +95,7 @@ class CanvasNode:
def add_antenna(self) -> None:
x, y = self.position()
offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
img = self.app.get_icon(ImageEnum.ANTENNA, images.ANTENNA_SIZE)
antenna_id = self.canvas.create_image(
x - 16 + offset,
y - int(23 * self.app.app_scale),
@ -389,7 +389,7 @@ class CanvasNode:
def scale_antennas(self) -> None:
for i in range(len(self.antennas)):
antenna_id = self.antennas[i]
image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
image = self.app.get_icon(ImageEnum.ANTENNA, images.ANTENNA_SIZE)
self.canvas.itemconfig(antenna_id, image=image)
self.antenna_images[antenna_id] = image
node_x, node_y = self.canvas.coords(self.id)
@ -403,7 +403,7 @@ class CanvasNode:
logging.error(f"node icon does not exist: {icon_path}")
return
self.core_node.icon = icon_path
self.image = Images.create(icon_path)
self.image = images.from_file(icon_path, width=images.NODE_SIZE)
self.canvas.itemconfig(self.id, image=self.image)
def is_linkable(self, node: "CanvasNode") -> bool:
@ -492,7 +492,7 @@ class ShadowNode:
self.node: "CanvasNode" = node
self.id: Optional[int] = None
self.text_id: Optional[int] = None
self.image: PhotoImage = self.app.get_icon(ImageEnum.SHADOW, ICON_SIZE)
self.image: PhotoImage = self.app.get_icon(ImageEnum.SHADOW, images.NODE_SIZE)
self.draw()
self.setup_bindings()

View file

@ -1,5 +1,4 @@
from enum import Enum
from tkinter import messagebox
from typing import Dict, Optional, Tuple
from PIL import Image
@ -8,52 +7,43 @@ from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import NodeType
from core.gui.appconfig import LOCAL_ICONS_PATH
ICON_SIZE: int = 48
NODE_SIZE: int = 48
ANTENNA_SIZE: int = 32
BUTTON_SIZE: int = 16
ERROR_SIZE: int = 24
DIALOG_SIZE: int = 16
IMAGES: Dict[str, str] = {}
class Images:
images: Dict[str, str] = {}
def load_all() -> None:
for image in LOCAL_ICONS_PATH.glob("*"):
IMAGES[image.stem] = str(image)
@classmethod
def create(
cls, file_path: str, width: int = None, height: int = None
) -> PhotoImage:
if width is None:
width = ICON_SIZE
if height is None:
height = width
image = Image.open(file_path)
image = image.resize((width, height), Image.ANTIALIAS)
return PhotoImage(image)
@classmethod
def load_all(cls) -> None:
for image in LOCAL_ICONS_PATH.glob("*"):
cls.images[image.stem] = str(image)
def from_file(
file_path: str, *, width: int, height: int = None, scale: float = 1.0
) -> PhotoImage:
if height is None:
height = width
width = int(width * scale)
height = int(height * scale)
image = Image.open(file_path)
image = image.resize((width, height), Image.ANTIALIAS)
return PhotoImage(image)
@classmethod
def get(cls, image_enum: Enum, width: int, height: int = None) -> PhotoImage:
file_path = cls.images[image_enum.value]
return cls.create(file_path, width, height)
@classmethod
def get_with_image_file(
cls, stem: str, width: int, height: int = None
) -> PhotoImage:
file_path = cls.images[stem]
return cls.create(file_path, width, height)
def from_enum(
image_enum: "ImageEnum", *, width: int, height: int = None, scale: float = 1.0
) -> PhotoImage:
file_path = IMAGES[image_enum.value]
return from_file(file_path, width=width, height=height, scale=scale)
@classmethod
def get_custom(cls, name: str, width: int, height: int = None) -> PhotoImage:
try:
file_path = cls.images[name]
return cls.create(file_path, width, height)
except KeyError:
messagebox.showwarning(
"Missing image file",
f"{name}.png is missing at daemon/core/gui/data/icons, drop image "
f"file at daemon/core/gui/data/icons and restart the gui",
)
def from_name(
name: str, *, width: int, height: int = None, scale: float = 1.0
) -> PhotoImage:
file_path = IMAGES[name]
return from_file(file_path, width=width, height=height, scale=scale)
class ImageEnum(Enum):

View file

@ -6,6 +6,7 @@ from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional
from core.gui import images
from core.gui.coreclient import CoreClient
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
@ -23,7 +24,6 @@ from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.graph.manager import CanvasManager
from core.gui.nodeutils import ICON_SIZE
from core.gui.observers import ObserversMenu
from core.gui.task import ProgressTask
@ -461,8 +461,8 @@ class Menubar(tk.Menu):
def click_autogrid(self) -> None:
width, height = self.manager.current_dimensions
padding = (ICON_SIZE / 2) + 10
layout_size = padding + ICON_SIZE
padding = (images.NODE_SIZE / 2) + 10
layout_size = padding + images.NODE_SIZE
col_count = width // layout_size
logging.info(
"auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count

View file

@ -4,11 +4,9 @@ from typing import List, Optional, Set
from PIL.ImageTk import PhotoImage
from core.api.grpc.wrappers import Node, NodeType
from core.gui import images
from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum, Images, TypeToImage
ICON_SIZE: int = 48
ANTENNA_SIZE: int = 32
from core.gui.images import ImageEnum, TypeToImage
NODES: List["NodeDraw"] = []
NETWORK_NODES: List["NodeDraw"] = []
@ -52,7 +50,7 @@ def setup() -> None:
node_draw = NodeDraw.from_setup(image_enum, node_type, label)
NETWORK_NODES.append(node_draw)
NODE_ICONS[(node_type, None)] = node_draw.image
ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE)
ANTENNA_ICON = images.from_enum(ImageEnum.ANTENNA, width=images.ANTENNA_SIZE)
def is_bridge(node: Node) -> bool:
@ -114,19 +112,21 @@ def get_icon(node: Node, config: GuiConfig, scale: float) -> Optional[PhotoImage
# node has defined a custom icon
if node.icon:
try:
image = Images.create(node.icon, int(ICON_SIZE * scale))
image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale)
except OSError:
logging.error("invalid icon: %s", node.icon)
else:
# attempt to find default type/model image
image_enum = TypeToImage.get(node.type, node.model)
if image_enum:
image = Images.get(image_enum, int(ICON_SIZE * scale))
image = images.from_enum(image_enum, width=images.NODE_SIZE, scale=scale)
# attempt to find custom image file
else:
image_file = _get_image_file(config, node.model)
if image_file:
image = Images.get_with_image_file(image_file, int(ICON_SIZE * scale))
image = images.from_name(
image_file, width=images.NODE_SIZE, scale=scale
)
return image
@ -152,7 +152,7 @@ class NodeDraw:
) -> "NodeDraw":
node_draw = NodeDraw()
node_draw.image_enum = image_enum
node_draw.image = Images.get(image_enum, ICON_SIZE)
node_draw.image = images.from_enum(image_enum, width=images.NODE_SIZE)
node_draw.node_type = node_type
node_draw.label = label
node_draw.model = model
@ -164,7 +164,7 @@ class NodeDraw:
node_draw = NodeDraw()
node_draw.custom = True
node_draw.image_file = custom_node.image
node_draw.image = Images.get_custom(custom_node.image, ICON_SIZE)
node_draw.image = images.from_name(custom_node.image, width=images.NODE_SIZE)
node_draw.node_type = NodeType.DEFAULT
node_draw.services = custom_node.services
node_draw.label = custom_node.name

View file

@ -3,9 +3,8 @@ import argparse
import logging
from logging.handlers import TimedRotatingFileHandler
from core.gui import appconfig
from core.gui import appconfig, images
from core.gui.app import Application
from core.gui.images import Images
if __name__ == "__main__":
# parse flags
@ -28,6 +27,6 @@ if __name__ == "__main__":
logging.getLogger("PIL").setLevel(logging.ERROR)
# start app
Images.load_all()
images.load_all()
app = Application(args.proxy, args.session)
app.mainloop()