pygui consolidated menubar and menuaction code into one file, small updates to observer widgets to avoid using ifconfig

This commit is contained in:
Blake Harnden 2020-04-18 00:33:22 -07:00
parent c43afa4b40
commit 7da7ea5d62
6 changed files with 231 additions and 294 deletions

View file

@ -6,7 +6,6 @@ from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient
from core.gui.graph.graph import CanvasGraph
from core.gui.images import ImageEnum, Images
from core.gui.menuaction import MenuAction
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
@ -104,8 +103,7 @@ class Application(tk.Frame):
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
def on_closing(self):
menu_action = MenuAction(self, self.master)
menu_action.on_quit()
self.menubar.prompt_save_running_session(True)
def save_config(self):
appconfig.save(self.guiconfig)

View file

@ -32,15 +32,15 @@ if TYPE_CHECKING:
GUI_SOURCE = "gui"
OBSERVERS = {
"processes": "ps",
"ifconfig": "ifconfig",
"List Processes": "ps",
"Show Interfaces": "ip address",
"IPV4 Routes": "ip -4 ro",
"IPV6 Routes": "ip -6 ro",
"Listening sockets": "netstat -tuwnl",
"IPv4 MFC entries": "ip -4 mroute show",
"IPv6 MFC entries": "ip -6 mroute show",
"firewall rules": "iptables -L",
"IPSec policies": "setkey -DP",
"Listening Sockets": "netstat -tuwnl",
"IPv4 MFC Entries": "ip -4 mroute show",
"IPv6 MFC Entries": "ip -6 mroute show",
"Firewall Rules": "iptables -L",
"IPSec Policies": "setkey -DP",
}
@ -100,10 +100,8 @@ class CoreClient:
self.mobility_players = {}
self.handling_throughputs = None
self.handling_events = None
self.xml_dir = None
self.xml_file = None
self.modified_service_nodes = set()
@property
@ -454,7 +452,8 @@ class CoreClient:
response = self.client.delete_session(session_id)
logging.info("deleted session(%s), Result: %s", session_id, response)
except grpc.RpcError as e:
# use the right master widget so the error dialog displays right on top of it
# use the right master widget so the error dialog displays
# right on top of it
master = self.app
if parent_frame:
master = parent_frame

View file

@ -1,203 +0,0 @@
"""
The actions taken when each menubar option is clicked
"""
import logging
import os
import tkinter as tk
import webbrowser
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import grpc
from core.gui.appconfig import XMLS_PATH
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
from core.gui.dialogs.hooks import HooksDialog
from core.gui.dialogs.observers import ObserverDialog
from core.gui.dialogs.preferences import PreferencesDialog
from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.task import BackgroundTask
MAX_FILES = 3
if TYPE_CHECKING:
from core.gui.app import Application
class MenuAction:
def __init__(self, app: "Application", master: tk.Tk):
self.master = master
self.app = app
self.canvas = app.canvas
def cleanup_old_session(self, session_id: int):
try:
res = self.app.core.client.get_session(session_id)
logging.debug("retrieve session(%s), %s", session_id, res)
stop_response = self.app.core.stop_session()
logging.debug("stop session(%s), result: %s", session_id, stop_response)
delete_response = self.app.core.delete_session(session_id)
logging.debug(
"deleted session(%s), result: %s", session_id, delete_response
)
except grpc.RpcError:
logging.debug("session is not alive")
def prompt_save_running_session(self, quitapp: bool = False):
"""
Prompt use to stop running session before application is closed
"""
result = True
if self.app.core.is_runtime():
result = messagebox.askyesnocancel("Exit", "Stop the running session?")
if result:
callback = None
if quitapp:
callback = self.app.quit
task = BackgroundTask(
self.app,
self.cleanup_old_session,
callback,
(self.app.core.session_id,),
)
task.start()
elif quitapp:
self.app.quit()
def on_quit(self, event: tk.Event = None):
"""
Prompt user whether so save running session, and then close the application
"""
self.prompt_save_running_session(quitapp=True)
def file_save_as_xml(self, event: tk.Event = None):
init_dir = self.app.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.asksaveasfilename(
initialdir=init_dir,
title="Save As",
filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")),
defaultextension=".xml",
)
if file_path:
self.add_recent_file_to_gui_config(file_path)
self.app.core.save_xml(file_path)
self.app.core.xml_file = file_path
def file_open_xml(self, event: tk.Event = None):
init_dir = self.app.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.askopenfilename(
initialdir=init_dir,
title="Open",
filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
)
self.open_xml_task(file_path)
def open_xml_task(self, filename):
if filename:
self.add_recent_file_to_gui_config(filename)
self.app.core.xml_file = filename
self.app.core.xml_dir = str(os.path.dirname(filename))
self.prompt_save_running_session()
self.app.statusbar.progress_bar.start(5)
task = BackgroundTask(self.app, self.app.core.open_xml, args=(filename,))
task.start()
def gui_preferences(self):
dialog = PreferencesDialog(self.app, self.app)
dialog.show()
def canvas_size_and_scale(self):
dialog = SizeAndScaleDialog(self.app, self.app)
dialog.show()
def canvas_set_wallpaper(self):
dialog = CanvasWallpaperDialog(self.app, self.app)
dialog.show()
def help_core_github(self):
webbrowser.open_new("https://github.com/coreemu/core")
def help_core_documentation(self):
webbrowser.open_new("http://coreemu.github.io/core/")
def session_options(self):
logging.debug("Click options")
dialog = SessionOptionsDialog(self.app, self.app)
if not dialog.has_error:
dialog.show()
def session_change_sessions(self):
logging.debug("Click change sessions")
dialog = SessionsDialog(self.app, self.app)
dialog.show()
def session_hooks(self):
logging.debug("Click hooks")
dialog = HooksDialog(self.app, self.app)
dialog.show()
def session_servers(self):
logging.debug("Click emulation servers")
dialog = ServersDialog(self.app, self.app)
dialog.show()
def edit_observer_widgets(self) -> None:
dialog = ObserverDialog(self.app, self.app)
dialog.show()
def show_about(self) -> None:
dialog = AboutDialog(self.app, self.app)
dialog.show()
def throughput(self) -> None:
if not self.app.core.handling_throughputs:
self.app.core.enable_throughputs()
else:
self.app.core.cancel_throughputs()
def copy(self, event: tk.Event = None) -> None:
self.app.canvas.copy()
def paste(self, event: tk.Event = None) -> None:
self.app.canvas.paste()
def delete(self, event: tk.Event = None) -> None:
self.app.canvas.delete_selected_objects()
def config_throughput(self) -> None:
dialog = ThroughputDialog(self.app, self.app)
dialog.show()
def add_recent_file_to_gui_config(self, file_path) -> None:
recent_files = self.app.guiconfig["recentfiles"]
num_files = len(recent_files)
if num_files == 0:
recent_files.insert(0, file_path)
elif 0 < num_files <= MAX_FILES:
if file_path in recent_files:
recent_files.remove(file_path)
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config()
self.app.menubar.update_recent_files()
def new_session(self):
self.prompt_save_running_session()
self.app.core.create_new_session()
self.app.core.xml_file = None

View file

@ -1,35 +1,50 @@
import logging
import os
import tkinter as tk
import webbrowser
from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import core.gui.menuaction as action
from core.gui.appconfig import XMLS_PATH
from core.gui.coreclient import OBSERVERS
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
from core.gui.dialogs.canvaswallpaper import CanvasWallpaperDialog
from core.gui.dialogs.executepython import ExecutePythonDialog
from core.gui.dialogs.hooks import HooksDialog
from core.gui.dialogs.observers import ObserverDialog
from core.gui.dialogs.preferences import PreferencesDialog
from core.gui.dialogs.servers import ServersDialog
from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.task import BackgroundTask
if TYPE_CHECKING:
from core.gui.app import Application
MAX_FILES = 3
class Menubar(tk.Menu):
"""
Core menubar
"""
def __init__(self, master: tk.Tk, app: "Application", cnf={}, **kwargs):
def __init__(self, master: tk.Tk, app: "Application", **kwargs) -> None:
"""
Create a CoreMenubar instance
"""
super().__init__(master, cnf, **kwargs)
super().__init__(master, **kwargs)
self.master.config(menu=self)
self.app = app
self.menuaction = action.MenuAction(app, master)
self.core = app.core
self.recent_menu = None
self.edit_menu = None
self.draw()
def draw(self):
def draw(self) -> None:
"""
Create core menubar and bind the hot keys to their matching command
"""
@ -42,24 +57,22 @@ class Menubar(tk.Menu):
self.draw_session_menu()
self.draw_help_menu()
def draw_file_menu(self):
def draw_file_menu(self) -> None:
"""
Create file menu
"""
menu = tk.Menu(self)
menu.add_command(
label="New Session",
accelerator="Ctrl+N",
command=self.menuaction.new_session,
label="New Session", accelerator="Ctrl+N", command=self.click_new
)
self.app.bind_all("<Control-n>", lambda e: self.app.core.create_new_session())
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.save)
self.app.bind_all("<Control-s>", self.save)
menu.add_command(label="Save As...", command=self.menuaction.file_save_as_xml)
self.app.bind_all("<Control-n>", lambda e: self.click_new())
menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save)
self.app.bind_all("<Control-s>", self.click_save)
menu.add_command(label="Save As...", command=self.click_save_xml)
menu.add_command(
label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O"
label="Open...", command=self.click_open_xml, accelerator="Ctrl+O"
)
self.app.bind_all("<Control-o>", self.menuaction.file_open_xml)
self.app.bind_all("<Control-o>", self.click_open_xml)
self.recent_menu = tk.Menu(menu)
for i in self.app.guiconfig["recentfiles"]:
self.recent_menu.add_command(
@ -70,51 +83,47 @@ class Menubar(tk.Menu):
menu.add_command(label="Execute Python Script...", command=self.execute_python)
menu.add_separator()
menu.add_command(
label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit
label="Quit",
accelerator="Ctrl+Q",
command=lambda: self.prompt_save_running_session(True),
)
self.app.bind_all(
"<Control-q>", lambda _: self.prompt_save_running_session(True)
)
self.app.bind_all("<Control-q>", self.menuaction.on_quit)
self.add_cascade(label="File", menu=menu)
def draw_edit_menu(self):
def draw_edit_menu(self) -> None:
"""
Create edit menu
"""
menu = tk.Menu(self)
menu.add_command(label="Preferences", command=self.menuaction.gui_preferences)
menu.add_command(label="Preferences", command=self.click_preferences)
menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED)
menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED)
menu.add_separator()
menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED)
menu.add_command(label="Copy", accelerator="Ctrl+C", command=self.click_copy)
menu.add_command(label="Paste", accelerator="Ctrl+V", command=self.click_paste)
menu.add_command(
label="Copy", accelerator="Ctrl+C", command=self.menuaction.copy
)
menu.add_command(
label="Paste", accelerator="Ctrl+V", command=self.menuaction.paste
)
menu.add_command(
label="Delete", accelerator="Ctrl+D", command=self.menuaction.delete
label="Delete", accelerator="Ctrl+D", command=self.click_delete
)
self.add_cascade(label="Edit", menu=menu)
self.app.master.bind_all("<Control-c>", self.menuaction.copy)
self.app.master.bind_all("<Control-v>", self.menuaction.paste)
self.app.master.bind_all("<Control-d>", self.menuaction.delete)
self.app.master.bind_all("<Control-c>", self.click_copy)
self.app.master.bind_all("<Control-v>", self.click_paste)
self.app.master.bind_all("<Control-d>", self.click_delete)
self.edit_menu = menu
def draw_canvas_menu(self):
def draw_canvas_menu(self) -> None:
"""
Create canvas menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Size / Scale", command=self.menuaction.canvas_size_and_scale
)
menu.add_command(
label="Wallpaper", command=self.menuaction.canvas_set_wallpaper
)
menu.add_command(label="Size / Scale", command=self.click_canvas_size_and_scale)
menu.add_command(label="Wallpaper", command=self.click_canvas_wallpaper)
self.add_cascade(label="Canvas", menu=menu)
def draw_view_menu(self):
def draw_view_menu(self) -> None:
"""
Create view menu
"""
@ -130,7 +139,7 @@ class Menubar(tk.Menu):
menu.add_command(label="Grid", state=tk.DISABLED)
self.add_cascade(label="View", menu=menu)
def draw_tools_menu(self):
def draw_tools_menu(self) -> None:
"""
Create tools menu
"""
@ -140,7 +149,7 @@ class Menubar(tk.Menu):
menu.add_command(label="MAC Addresses", state=tk.DISABLED)
self.add_cascade(label="Tools", menu=menu)
def create_observer_widgets_menu(self, widget_menu: tk.Menu):
def create_observer_widgets_menu(self, widget_menu: tk.Menu) -> None:
"""
Create observer widget menu item and create the sub menu items inside
"""
@ -148,14 +157,14 @@ class Menubar(tk.Menu):
menu = tk.Menu(widget_menu)
menu.var = var
menu.add_command(
label="Edit Observers", command=self.menuaction.edit_observer_widgets
label="Edit Observers", command=self.click_edit_observer_widgets
)
menu.add_separator()
menu.add_radiobutton(
label="None",
variable=var,
value="none",
command=lambda: self.app.core.set_observer(None),
command=lambda: self.core.set_observer(None),
)
for name in sorted(OBSERVERS):
cmd = OBSERVERS[name]
@ -163,19 +172,19 @@ class Menubar(tk.Menu):
label=name,
variable=var,
value=name,
command=partial(self.app.core.set_observer, cmd),
command=partial(self.core.set_observer, cmd),
)
for name in sorted(self.app.core.custom_observers):
observer = self.app.core.custom_observers[name]
for name in sorted(self.core.custom_observers):
observer = self.core.custom_observers[name]
menu.add_radiobutton(
label=name,
variable=var,
value=name,
command=partial(self.app.core.set_observer, observer.cmd),
command=partial(self.core.set_observer, observer.cmd),
)
widget_menu.add_cascade(label="Observer Widgets", menu=menu)
def create_adjacency_menu(self, widget_menu: tk.Menu):
def create_adjacency_menu(self, widget_menu: tk.Menu) -> None:
"""
Create adjacency menu item and the sub menu items inside
"""
@ -187,17 +196,15 @@ class Menubar(tk.Menu):
menu.add_command(label="Enable OSLRv2?", state=tk.DISABLED)
widget_menu.add_cascade(label="Adjacency", menu=menu)
def create_throughput_menu(self, widget_menu: tk.Menu):
def create_throughput_menu(self, widget_menu: tk.Menu) -> None:
menu = tk.Menu(widget_menu)
menu.add_command(
label="Configure Throughput", command=self.menuaction.config_throughput
)
menu.add_checkbutton(
label="Enable Throughput?", command=self.menuaction.throughput
label="Configure Throughput", command=self.click_config_throughput
)
menu.add_checkbutton(label="Enable Throughput?", command=self.click_throughput)
widget_menu.add_cascade(label="Throughput", menu=menu)
def draw_widgets_menu(self):
def draw_widgets_menu(self) -> None:
"""
Create widget menu
"""
@ -207,60 +214,107 @@ class Menubar(tk.Menu):
self.create_throughput_menu(menu)
self.add_cascade(label="Widgets", menu=menu)
def draw_session_menu(self):
def draw_session_menu(self) -> None:
"""
Create session menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Sessions", command=self.menuaction.session_change_sessions
)
menu.add_command(label="Servers", command=self.menuaction.session_servers)
menu.add_command(label="Options", command=self.menuaction.session_options)
menu.add_command(label="Hooks", command=self.menuaction.session_hooks)
menu.add_command(label="Sessions", command=self.click_sessions)
menu.add_command(label="Servers", command=self.click_servers)
menu.add_command(label="Options", command=self.click_session_options)
menu.add_command(label="Hooks", command=self.click_hooks)
self.add_cascade(label="Session", menu=menu)
def draw_help_menu(self):
def draw_help_menu(self) -> None:
"""
Create help menu
"""
menu = tk.Menu(self)
menu.add_command(
label="Core GitHub (www)", command=self.menuaction.help_core_github
)
menu.add_command(
label="Core Documentation (www)",
command=self.menuaction.help_core_documentation,
)
menu.add_command(label="About", command=self.menuaction.show_about)
menu.add_command(label="Core GitHub (www)", command=self.click_core_github)
menu.add_command(label="Core Documentation (www)", command=self.click_core_doc)
menu.add_command(label="About", command=self.click_about)
self.add_cascade(label="Help", menu=menu)
def open_recent_files(self, filename: str):
def open_recent_files(self, filename: str) -> None:
if os.path.isfile(filename):
logging.debug("Open recent file %s", filename)
self.menuaction.open_xml_task(filename)
self.open_xml_task(filename)
else:
logging.warning("File does not exist %s", filename)
def update_recent_files(self):
def update_recent_files(self) -> None:
self.recent_menu.delete(0, tk.END)
for i in self.app.guiconfig["recentfiles"]:
self.recent_menu.add_command(
label=i, command=partial(self.open_recent_files, i)
)
def save(self, event=None):
xml_file = self.app.core.xml_file
def click_save(self, _event=None) -> None:
xml_file = self.core.xml_file
if xml_file:
self.app.core.save_xml(xml_file)
self.core.save_xml(xml_file)
else:
self.menuaction.file_save_as_xml()
self.click_save_xml()
def click_save_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.asksaveasfilename(
initialdir=init_dir,
title="Save As",
filetypes=(("XML files", "*.xml"), ("All files", "*")),
defaultextension=".xml",
)
if file_path:
self.add_recent_file_to_gui_config(file_path)
self.core.save_xml(file_path)
self.core.xml_file = file_path
def click_open_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
file_path = filedialog.askopenfilename(
initialdir=init_dir,
title="Open",
filetypes=(("XML Files", "*.xml"), ("All Files", "*")),
)
if file_path:
self.open_xml_task(file_path)
def open_xml_task(self, filename: str) -> None:
self.add_recent_file_to_gui_config(filename)
self.core.xml_file = filename
self.core.xml_dir = str(os.path.dirname(filename))
self.prompt_save_running_session()
self.app.statusbar.progress_bar.start(5)
task = BackgroundTask(self.app, self.core.open_xml, args=(filename,))
task.start()
def execute_python(self):
dialog = ExecutePythonDialog(self.app, self.app)
dialog.show()
def change_menubar_item_state(self, is_runtime: bool):
def add_recent_file_to_gui_config(self, file_path) -> None:
recent_files = self.app.guiconfig["recentfiles"]
num_files = len(recent_files)
if num_files == 0:
recent_files.insert(0, file_path)
elif 0 < num_files <= MAX_FILES:
if file_path in recent_files:
recent_files.remove(file_path)
recent_files.insert(0, file_path)
else:
if num_files == MAX_FILES:
recent_files.pop()
recent_files.insert(0, file_path)
else:
logging.error("unexpected number of recent files")
self.app.save_config()
self.app.menubar.update_recent_files()
def change_menubar_item_state(self, is_runtime: bool) -> None:
for i in range(self.edit_menu.index("end")):
try:
label_name = self.edit_menu.entrycget(i, "label")
@ -271,3 +325,92 @@ class Menubar(tk.Menu):
self.edit_menu.entryconfig(i, state="normal")
except tk.TclError:
logging.debug("Ignore separators")
def prompt_save_running_session(self, quit_app: bool = False) -> None:
"""
Prompt use to stop running session before application is closed
:param quit_app: True to quit app, False otherwise
"""
result = True
if self.core.is_runtime():
result = messagebox.askyesnocancel("Exit", "Stop the running session?")
if result:
callback = None
if quit_app:
callback = self.app.quit
task = BackgroundTask(self.app, self.core.delete_session, callback)
task.start()
elif quit_app:
self.app.quit()
def click_new(self) -> None:
self.prompt_save_running_session()
self.core.create_new_session()
self.core.xml_file = None
def click_preferences(self) -> None:
dialog = PreferencesDialog(self.app, self.app)
dialog.show()
def click_canvas_size_and_scale(self) -> None:
dialog = SizeAndScaleDialog(self.app, self.app)
dialog.show()
def click_canvas_wallpaper(self) -> None:
dialog = CanvasWallpaperDialog(self.app, self.app)
dialog.show()
def click_core_github(self) -> None:
webbrowser.open_new("https://github.com/coreemu/core")
def click_core_doc(self) -> None:
webbrowser.open_new("http://coreemu.github.io/core/")
def click_about(self) -> None:
dialog = AboutDialog(self.app, self.app)
dialog.show()
def click_throughput(self) -> None:
if not self.core.handling_throughputs:
self.core.enable_throughputs()
else:
self.core.cancel_throughputs()
def click_config_throughput(self) -> None:
dialog = ThroughputDialog(self.app, self.app)
dialog.show()
def click_copy(self, _event: tk.Event = None) -> None:
self.app.canvas.copy()
def click_paste(self, _event: tk.Event = None) -> None:
self.app.canvas.paste()
def click_delete(self, _event: tk.Event = None) -> None:
self.app.canvas.delete_selected_objects()
def click_session_options(self) -> None:
logging.debug("Click options")
dialog = SessionOptionsDialog(self.app, self.app)
if not dialog.has_error:
dialog.show()
def click_sessions(self) -> None:
logging.debug("Click change sessions")
dialog = SessionsDialog(self.app, self.app)
dialog.show()
def click_hooks(self) -> None:
logging.debug("Click hooks")
dialog = HooksDialog(self.app, self.app)
dialog.show()
def click_servers(self) -> None:
logging.debug("Click emulation servers")
dialog = ServersDialog(self.app, self.app)
dialog.show()
def click_edit_observer_widgets(self) -> None:
dialog = ObserverDialog(self.app, self.app)
dialog.show()

View file

@ -21,7 +21,8 @@ class BackgroundTask:
def run(self):
result = self.task(*self.args)
logging.info("task completed")
# if start session fails, a response with Result: False and a list of exceptions is returned
# if start session fails, a response with Result: False and a list of
# exceptions is returned
if not getattr(result, "result", True):
if len(getattr(result, "exceptions", [])) > 0:
self.master.after(

View file

@ -499,7 +499,6 @@ class Toolbar(ttk.Frame):
def click_two_node_button(self):
logging.debug("Click TWONODE button")
# def scale_button(cls, button, image_enum, scale):
def scale_button(self, button, image_enum):
image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale))
button.config(image=image)