1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-07 10:43:27 -04:00
faceswap/lib/gui/popup_configure.py
torzdf 6a3b674bef
Rebase code (#1326)
* Remove tensorflow_probability requirement

* setup.py - fix progress bars

* requirements.txt: Remove pre python 3.9 packages

* update apple requirements.txt

* update INSTALL.md

* Remove python<3.9 code

* setup.py - fix Windows Installer

* typing: python3.9 compliant

* Update pytest and readthedocs python versions

* typing fixes

* Python Version updates
  - Reduce max version to 3.10
  - Default to 3.10 in installers
  - Remove incompatible 3.11 tests

* Update dependencies

* Downgrade imageio dep for Windows

* typing: merge optional unions and fixes

* Updates
  - min python version 3.10
  - typing to python 3.10 spec
  - remove pre-tf2.10 code
  - Add conda tests

* train: re-enable optimizer saving

* Update dockerfiles

* Update setup.py
  - Apple Conda deps to setup.py
  - Better Cuda + dependency handling

* bugfix: Patch logging to prevent Autograph errors

* Update dockerfiles

* Setup.py - Setup.py - stdout to utf-8

* Add more OSes to github Actions

* suppress mac-os end to end test
2023-06-27 11:27:47 +01:00

859 lines
34 KiB
Python

#!/usr/bin python3
""" The pop-up window of the Faceswap GUI for the setting of configuration options. """
from __future__ import annotations
from collections import OrderedDict
from configparser import ConfigParser
import gettext
import logging
import os
import sys
import tkinter as tk
from tkinter import ttk
import typing as T
from importlib import import_module
from lib.serializer import get_serializer
from .control_helper import ControlPanel, ControlPanelOption
from .custom_widgets import Tooltip
from .utils import FileHandler, get_config, get_images, PATHCACHE
if T.TYPE_CHECKING:
from lib.config import FaceswapConfig
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
# LOCALES
_LANG = gettext.translation("gui.tooltips", localedir="locales", fallback=True)
_ = _LANG.gettext
class _State():
""" Holds the existing config files and the current state of the popup window. """
def __init__(self):
self._popup = None
# The GUI Config cannot be scanned until GUI is launched, so this is populated
# on the first call to load the settings
self._configs = {}
def open_popup(self, name=None):
""" Launch the popup, ensuring only one instance is ever open
Parameters
----------
name: str, Optional
The name of the configuration file. Used for selecting the correct section if required.
Set to ``None`` if no initial section should be selected. Default: ``None``
"""
self._scan_for_configs()
logger.debug("name: %s", name)
if self._popup is not None:
logger.debug("Restoring existing popup")
self._popup.update()
self._popup.deiconify()
self._popup.lift()
return
self._popup = _ConfigurePlugins(name, self._configs)
def close_popup(self):
""" Destroy the open popup and remove it from tracking. """
if self._popup is None:
logger.info("No popup to close. Returning")
return
self._popup.destroy()
del self._popup
self._popup = None
def _scan_for_configs(self):
""" Scan the plugin folders for configuration settings. Add in the GUI configuration also.
Populates the attribute :attr:`_configs`.
"""
root_path = os.path.abspath(os.path.dirname(sys.argv[0]))
plugins_path = os.path.join(root_path, "plugins")
logger.debug("Scanning path: '%s'", plugins_path)
for dirpath, _, filenames in os.walk(plugins_path):
if "_config.py" in filenames:
plugin_type = os.path.split(dirpath)[-1]
config = self._load_config(plugin_type)
self._configs[plugin_type] = config
self._configs["gui"] = get_config().user_config
logger.debug("Configs loaded: %s", sorted(list(self._configs.keys())))
@classmethod
def _load_config(cls, plugin_type):
""" Load the config from disk. If the file doesn't exist, then it will be generated.
Parameters
----------
plugin_type: str
The plugin type (i.e. extract, train convert) that the config should be loaded for
Returns
-------
:class:`lib.config.FaceswapConfig`
The Configuration for the selected plugin
"""
# Load config to generate default if doesn't exist
mod = ".".join(("plugins", plugin_type, "_config"))
module = import_module(mod)
config = module.Config(None)
logger.debug("Found '%s' config at '%s'", plugin_type, config.configfile)
return config
_STATE = _State()
open_popup = _STATE.open_popup # pylint:disable=invalid-name
class _ConfigurePlugins(tk.Toplevel):
""" Pop-up window for the setting of Faceswap Configuration Options.
Parameters
----------
name: str
The name of the section that is being navigated to. Used for opening on the correct
page in the Tree View.
configurations: dict
Dictionary containing the :class:`~lib.config.FaceswapConfig` object for each
configuration section for the requested pop-up window
"""
def __init__(self, name, configurations):
logger.debug("Initializing %s: (name: %s, configurations: %s)",
self.__class__.__name__, name, configurations)
super().__init__()
self._root = get_config().root
self._set_geometry()
self._tk_vars = {"header": tk.StringVar()}
theme = {**get_config().user_theme["group_panel"],
**get_config().user_theme["group_settings"]}
header_frame = self._build_header()
content_frame = ttk.Frame(self)
self._tree = _Tree(content_frame, configurations, name, theme).tree
self._tree.bind("<ButtonRelease-1>", self._select_item)
self._opts_frame = DisplayArea(self, content_frame, configurations, self._tree, theme)
self._opts_frame.pack(fill=tk.BOTH, expand=True, side=tk.RIGHT)
footer_frame = self._build_footer()
header_frame.pack(fill=tk.X, padx=5, pady=5, side=tk.TOP)
content_frame.pack(fill=tk.BOTH, padx=5, pady=(0, 5), expand=True, side=tk.TOP)
footer_frame.pack(fill=tk.X, padx=5, pady=(0, 5), side=tk.BOTTOM)
select = name if name else self._tree.get_children()[0]
self._tree.selection_set(select)
self._tree.focus(select)
self._select_item(0)
self.title("Configure Settings")
self.tk.call('wm', 'iconphoto', self._w, get_images().icons["favicon"])
self.protocol("WM_DELETE_WINDOW", _STATE.close_popup)
logger.debug("Initialized %s", self.__class__.__name__)
def _set_geometry(self):
""" Set the geometry of the pop-up window """
scaling_factor = get_config().scaling_factor
pos_x = self._root.winfo_x() + 80
pos_y = self._root.winfo_y() + 80
width = int(600 * scaling_factor)
height = int(536 * scaling_factor)
logger.debug("Pop up Geometry: %sx%s, %s+%s", width, height, pos_x, pos_y)
self.geometry(f"{width}x{height}+{pos_x}+{pos_y}")
def _build_header(self):
""" Build the main header text and separator. """
header_frame = ttk.Frame(self)
lbl_frame = ttk.Frame(header_frame)
self._tk_vars["header"].set("Settings")
lbl_header = ttk.Label(lbl_frame,
textvariable=self._tk_vars["header"],
anchor=tk.W,
style="SPanel.Header1.TLabel")
lbl_header.pack(fill=tk.X, expand=True, side=tk.LEFT)
sep = ttk.Frame(header_frame, height=2, relief=tk.RIDGE)
lbl_frame.pack(fill=tk.X, expand=True, side=tk.TOP)
sep.pack(fill=tk.X, pady=(1, 0), side=tk.BOTTOM)
return header_frame
def _build_footer(self):
""" Build the main footer buttons and separator. """
logger.debug("Adding action buttons")
frame = ttk.Frame(self)
left_frame = ttk.Frame(frame)
right_frame = ttk.Frame(frame)
btn_saveall = ttk.Button(left_frame,
text="Save All",
width=10,
command=self._opts_frame.save)
btn_rstall = ttk.Button(left_frame,
text="Reset All",
width=10,
command=self._opts_frame.reset)
btn_cls = ttk.Button(right_frame, text="Cancel", width=10, command=_STATE.close_popup)
btn_save = ttk.Button(right_frame,
text="Save",
width=10,
command=lambda: self._opts_frame.save(page_only=True))
btn_rst = ttk.Button(right_frame,
text="Reset",
width=10,
command=lambda: self._opts_frame.reset(page_only=True))
Tooltip(btn_cls, text=_("Close without saving"), wrap_length=720)
Tooltip(btn_save, text=_("Save this page's config"), wrap_length=720)
Tooltip(btn_rst, text=_("Reset this page's config to default values"), wrap_length=720)
Tooltip(btn_saveall,
text=_("Save all settings for the currently selected config"),
wrap_length=720)
Tooltip(btn_rstall,
text=_("Reset all settings for the currently selected config to default values"),
wrap_length=720)
btn_cls.pack(padx=2, side=tk.RIGHT)
btn_save.pack(padx=2, side=tk.RIGHT)
btn_rst.pack(padx=2, side=tk.RIGHT)
btn_saveall.pack(padx=2, side=tk.RIGHT)
btn_rstall.pack(padx=2, side=tk.RIGHT)
left_frame.pack(side=tk.LEFT)
right_frame.pack(side=tk.RIGHT)
logger.debug("Added action buttons")
return frame
def _select_item(self, event): # pylint:disable=unused-argument
""" Update the session summary info with the selected item or launch graph.
If the mouse is clicked on the graph icon, then the session summary pop-up graph is
launched. Otherwise the selected ID is stored.
Parameters
----------
event: :class:`tkinter.Event`
The tkinter mouse button release event. Unused.
"""
selection = self._tree.focus()
section = selection.split("|")[0]
subsections = selection.split("|")[1:] if "|" in selection else []
self._tk_vars["header"].set(f"{section.title()} Settings")
self._opts_frame.select_options(section, subsections)
class _Tree(ttk.Frame): # pylint:disable=too-many-ancestors
""" Frame that holds the Tree View Navigator and scroll bar for the configuration pop-up.
Parameters
----------
parent: :class:`tkinter.ttk.Frame`
The parent frame to the Tree View area
configurations: dict
Dictionary containing the :class:`~lib.config.FaceswapConfig` object for each
configuration section for the requested pop-up window
name: str
The name of the section that is being navigated to. Used for opening on the correct
page in the Tree View. ``None`` if no specific area is being navigated to
theme: dict
The color mapping for the settings pop-up theme
"""
def __init__(self, parent, configurations, name, theme):
super().__init__(parent)
self._fix_styles(theme)
frame = ttk.Frame(self, relief=tk.SOLID, borderwidth=1)
self._tree = self._build_tree(frame, configurations, name)
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=self._tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self._tree.pack(fill=tk.Y, expand=True)
self._tree.configure(yscrollcommand=scrollbar.set)
frame.pack(expand=True, fill=tk.Y)
self.pack(side=tk.LEFT, fill=tk.Y)
@property
def tree(self):
""" :class:`tkinter.ttk.TreeView` The Tree View held within the frame """
return self._tree
@classmethod
def _fix_styles(cls, theme):
""" Tkinter has a bug when setting the background style on certain OSes. This fixes the
issue so we can set different colored backgrounds.
We also set some default styles for our tree view.
Parameters
----------
theme: dict
The color mapping for the settings pop-up theme
"""
style = ttk.Style()
# Fix a bug in Tree-view that doesn't show alternate foreground on selection
fix_map = lambda o: [elm for elm in style.map("Treeview", query_opt=o) # noqa[E731] # pylint:disable=C3001
if elm[:2] != ("!disabled", "!selected")]
# Remove the Borders
style.configure("ConfigNav.Treeview", bd=0, background="#F0F0F0")
style.layout("ConfigNav.Treeview", [('ConfigNav.Treeview.treearea', {'sticky': 'nswe'})])
# Set colors
style.map("ConfigNav.Treeview",
foreground=fix_map("foreground"),
background=fix_map("background"))
style.map('ConfigNav.Treeview', background=[('selected', theme["tree_select"])])
def _build_tree(self, parent, configurations, name):
""" Build the configuration pop-up window.
Parameters
----------
configurations: dict
Dictionary containing the :class:`~lib.config.FaceswapConfig` object for each
configuration section for the requested pop-up window
name: str
The name of the section that is being navigated to. Used for opening on the correct
page in the Tree View. ``None`` if no specific area is being navigated to
Returns
-------
:class:`tkinter.ttk.TreeView`
The populated tree view
"""
logger.debug("Building Tree View Navigator")
tree = ttk.Treeview(parent, show="tree", style="ConfigNav.Treeview")
data = {category: [sect.split(".") for sect in sorted(conf.config.sections())]
for category, conf in configurations.items()}
ordered = sorted(list(data.keys()))
categories = ["extract", "train", "convert"]
categories += [x for x in ordered if x not in categories]
for cat in categories:
img = get_images().icons.get(f"settings_{cat}", "")
text = cat.replace("_", " ").title()
text = " " + text if img else text
is_open = tk.TRUE if name is None or name == cat else tk.FALSE
tree.insert("", "end", cat, text=text, image=img, open=is_open, tags="category")
self._process_sections(tree, data[cat], cat, name == cat)
tree.tag_configure('category', background='#DFDFDF')
tree.tag_configure('section', background='#E8E8E8')
tree.tag_configure('option', background='#F0F0F0')
logger.debug("Tree View Navigator")
return tree
@classmethod
def _process_sections(cls, tree, sections, category, is_open):
""" Process the sections of a category's configuration.
Creates a category's sections, then the sub options for that category
Parameters
----------
tree: :class:`tkinter.ttk.TreeView`
The tree view to insert sections into
sections: list
The sections to insert into the Tree View
category: str
The category node that these sections sit in
is_open: bool
``True`` if the node should be created in "open" mode. ``False`` if it should be
closed.
"""
seen = set()
for section in sections:
if section[-1] == "global": # Global categories get escalated to parent
continue
sect = section[0]
section_id = f"{category}|{sect}"
if sect not in seen:
seen.add(sect)
text = sect.replace("_", " ").title()
tree.insert(category, "end", section_id, text=text, open=is_open, tags="section")
if len(section) == 2:
opt = section[-1]
opt_id = f"{section_id}|{opt}"
opt_text = opt.replace("_", " ").title()
tree.insert(section_id, "end", opt_id, text=opt_text, open=is_open, tags="option")
class DisplayArea(ttk.Frame): # pylint:disable=too-many-ancestors
""" The option configuration area of the pop up options.
Parameters
----------
top_level: :class:``tk.Toplevel``
The tkinter Top Level widget
parent: :class:`tkinter.ttk.Frame`
The parent frame that holds the Display Area of the pop up configuration window
tree: :class:`tkinter.ttk.TreeView`
The Tree View navigator for the pop up configuration window
configurations: dict
Dictionary containing the :class:`~lib.config.FaceswapConfig` object for each
configuration section for the requested pop-up window
theme: dict
The color mapping for the settings pop-up theme
"""
def __init__(self, top_level, parent, configurations, tree, theme):
super().__init__(parent)
self._configs: dict[str, FaceswapConfig] = configurations
self._theme = theme
self._tree = tree
self._vars = {}
self._cache = {}
self._config_cpanel_dict = self._get_config()
self._displayed_frame = None
self._displayed_key = None
self._presets = _Presets(self, top_level)
self._build_header()
@property
def displayed_key(self):
""" str: The current display page's lookup key for configuration options. """
return self._displayed_key
@property
def config_dict(self):
""" dict: The configuration dictionary for all display pages. """
return self._config_cpanel_dict
def _get_config(self):
""" Format the configuration options stored in :attr:`_config` into a dict of
:class:`~lib.gui.control_helper.ControlPanelOption's for placement into option frames.
Returns
-------
dict
A dictionary of section names to :class:`~lib.gui.control_helper.ControlPanelOption`
objects
"""
logger.debug("Formatting Config for GUI")
retval = {}
for plugin, conf in self._configs.items():
for section in conf.config.sections():
conf.section = section
category = section.split(".")[0]
sect = section.split(".")[-1]
# Elevate global to root
key = plugin if sect == "global" else f"{plugin}|{category}|{sect}"
retval[key] = {"helptext": None, "options": OrderedDict()}
retval[key]["helptext"] = conf.defaults[section].helptext
for option, params in conf.defaults[section].items.items():
initial_value = conf.config_dict[option]
initial_value = "none" if initial_value is None else initial_value
if params.datatype == list and isinstance(initial_value, list):
# Split multi-select lists into space separated strings for tk variables
initial_value = " ".join(initial_value)
retval[key]["options"][option] = ControlPanelOption(
title=option,
dtype=params.datatype,
group=params.group,
default=params.default,
initial_value=initial_value,
choices=params.choices,
is_radio=params.gui_radio,
is_multi_option=params.datatype == list,
rounding=params.rounding,
min_max=params.min_max,
helptext=params.helptext)
logger.debug("Formatted Config for GUI: %s", retval)
return retval
def _build_header(self):
""" Build the dynamic header text. """
header_frame = ttk.Frame(self)
lbl_frame = ttk.Frame(header_frame)
var = tk.StringVar()
lbl = ttk.Label(lbl_frame, textvariable=var, anchor=tk.W, style="SPanel.Header2.TLabel")
lbl.pack(fill=tk.X, expand=True, side=tk.TOP)
self._build_presets_buttons(header_frame)
lbl_frame.pack(fill=tk.X, side=tk.LEFT, expand=True)
header_frame.pack(fill=tk.X, padx=5, pady=5, side=tk.TOP)
self._vars["header"] = var
def _build_presets_buttons(self, frame):
""" Build the section that holds the preset load and save buttons.
Parameters
----------
frame: :class:`ttk.Frame`
The frame that holds the preset buttons
"""
presets_frame = ttk.Frame(frame)
for lbl in ("load", "save"):
btn = ttk.Button(presets_frame,
image=get_images().icons[lbl],
command=getattr(self._presets, lbl))
Tooltip(btn, text=_(f"{lbl.title()} preset for this plugin"), wrap_length=720)
btn.pack(padx=2, side=tk.LEFT)
presets_frame.pack(side=tk.RIGHT)
def select_options(self, section, subsections):
""" Display the page for the given section and subsections.
Parameters
----------
section: str
The main section to be navigated to (or root node)
subsections: list
The full list of subsections ending on the required node
"""
labels = ["global"] if not subsections else subsections
self._vars["header"].set(" - ".join(sect.replace("_", " ").title() for sect in labels))
self._set_display(section, subsections)
def _set_display(self, section, subsections):
""" Set the correct display page for the given section and subsections.
Parameters
----------
section: str
The main section to be navigated to (or root node)
subsections: list
The full list of subsections ending on the required node
"""
key = "|".join([section] + subsections)
if self._displayed_frame is not None:
self._displayed_frame.pack_forget()
if key not in self._cache:
self._cache_page(key)
self._displayed_frame = self._cache[key]
self._displayed_key = key
self._displayed_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
def _cache_page(self, key):
""" Create the control panel options for the requested configuration and cache.
Parameters
----------
key: str
The lookup key to the settings cache
"""
info = self._config_cpanel_dict.get(key, None)
if info is None:
logger.debug("key '%s' does not exist in options. Creating links page.", key)
self._cache[key] = self._create_links_page(key)
else:
self._cache[key] = ControlPanel(self,
list(info["options"].values()),
header_text=info["helptext"],
columns=1,
max_columns=1,
option_columns=4,
style="SPanel",
blank_nones=False)
def _create_links_page(self, key):
""" For headings which don't have settings, build a links page to the subsections.
Parameters
----------
key: str
The lookup key to set the links page for
"""
frame = ttk.Frame(self)
links = {item.replace(key, "")[1:].split("|")[0]
for item in self._config_cpanel_dict
if item.startswith(key)}
if not links:
return frame
header_lbl = ttk.Label(frame, text=_("Select a plugin to configure:"))
header_lbl.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5, 10))
for link in sorted(links):
lbl = ttk.Label(frame,
text=link.replace("_", " ").title(),
anchor=tk.W,
foreground=self._theme["link_color"],
cursor="hand2")
lbl.pack(side=tk.TOP, fill=tk.X, padx=10, pady=(0, 5))
bind = f"{key}|{link}"
lbl.bind("<Button-1>", lambda e, x=bind: self._link_callback(x))
return frame
def _link_callback(self, identifier):
""" Set the tree view to the selected item and display the requested page on a link click.
Parameters
----------
identifier: str
The identifier from the tree view for the page to display
"""
parent = "|".join(identifier.split("|")[:-1])
self._tree.item(parent, open=True)
self._tree.selection_set(identifier)
self._tree.focus(identifier)
split = identifier.split("|")
section = split[0]
subsections = split[1:] if len(split) > 1 else []
self.select_options(section, subsections)
def reset(self, page_only=False):
""" Reset all configuration options to their default values.
Parameters
----------
page_only: bool, optional
``True`` resets just the currently selected page's options to default, ``False`` resets
all plugins within the currently selected config to default. Default: ``False``
"""
logger.debug("Resetting config, page_only: %s", page_only)
selection = self._tree.focus()
if page_only:
if selection not in self._config_cpanel_dict:
logger.info("No configuration options to reset for current page: %s", selection)
return
items = list(self._config_cpanel_dict[selection]["options"].values())
else:
items = [opt
for key, val in self._config_cpanel_dict.items()
for opt in val["options"].values()
if key.startswith(selection.split("|")[0])]
for item in items:
logger.debug("Resetting item '%s' from '%s' to default '%s'",
item.title, item.get(), item.default)
item.set(item.default)
logger.debug("Reset config")
def _get_new_config(self,
page_only: bool,
config: FaceswapConfig,
category: str,
lookup: str) -> ConfigParser:
""" Obtain a new configuration file for saving
Parameters
----------
page_only: bool
``True`` saves just the currently selected page's options, ``False`` saves all the
plugins options within the currently selected config.
config: :class:`~lib.config.FaceswapConfig`
The original config that is to be addressed
category: str
The configuration category to update
lookup: str
The section of the configuration to update
Returns
-------
:class:`configparse.ConfigParser`
The newly created configuration object for saving
"""
new_config = ConfigParser(allow_no_value=True)
for section_name, section in config.defaults.items():
logger.debug("Adding section: '%s')", section_name)
config.insert_config_section(section_name, section.helptext, config=new_config)
for item, options in section.items.items():
if item == "helptext":
continue
if page_only and section_name != lookup:
# Keep existing values for pages we are not updating
new_opt = config.get(section_name, item)
logger.debug("Retain existing value '%s' for %s",
new_opt, ".".join([section_name, item]))
else:
# Get currently selected value
key = category
if section_name != "global":
key += f"|{section_name.replace('.', '|')}"
new_opt = self._config_cpanel_dict[key]["options"][item].get()
logger.debug("Updating value to '%s' for %s",
new_opt, ".".join([section_name, item]))
helptext = config.format_help(options.helptext, is_section=False)
new_config.set(section_name, helptext)
if options.datatype == list: # Comma separated multi select options
assert isinstance(new_opt, (list, str))
new_opt = ", ".join(new_opt if isinstance(new_opt, list) else new_opt.split())
new_config.set(section_name, item, str(new_opt))
return new_config
def save(self, page_only=False):
""" Save the configuration file to disk.
Parameters
----------
page_only: bool, optional
``True`` saves just the currently selected page's options, ``False`` saves all the
plugins options within the currently selected config. Default: ``False``
"""
logger.debug("Saving config")
selection = self._tree.focus()
category = selection.split("|")[0]
config = self._configs[category]
# Create a new config to pull through any defaults change
if "|" in selection:
lookup = ".".join(selection.split("|")[1:])
else: # Expand global out from root node
lookup = "global"
if page_only and lookup not in config.config.sections():
logger.info("No settings to save for the current page")
return
config.config = self._get_new_config(page_only, config, category, lookup)
config.save_config()
logger.info("Saved config: '%s'", config.configfile)
if category == "gui":
if not get_config().tk_vars.running_task.get():
get_config().root.rebuild()
else:
logger.info("Can't redraw GUI whilst a task is running. GUI Settings will be "
"applied at the next restart.")
logger.debug("Saved config")
class _Presets():
""" Handles the file dialog and loading and saving of plugin preset files.
Parameters
----------
parent: :class:`ttk.Frame`
The parent display area frame
top_level: :class:`tkinter.Toplevel`
The top level pop up window
"""
def __init__(self, parent, top_level):
logger.debug("Initializing: %s (top_level: %s)", self.__class__.__name__, top_level)
self._parent = parent
self._popup = top_level
self._base_path = os.path.join(PATHCACHE, "presets")
self._serializer = get_serializer("json")
logger.debug("Initialized: %s", self.__class__.__name__)
@property
def _preset_path(self):
""" str: The path to the default preset folder for the currently displayed plugin. """
return os.path.join(self._base_path, self._parent.displayed_key.split("|")[0])
@property
def _full_key(self):
""" str: The full extrapolated lookup key for the currently displayed page. """
full_key = self._parent.displayed_key
return full_key if "|" in full_key else f"{full_key}|global"
def load(self):
""" Action to perform when load preset button is pressed.
Loads parameters from a saved json file and updates the displayed page.
"""
filename = self._get_filename("load")
if not filename:
return
opts = self._serializer.load(filename)
if opts.get("__filetype") != "faceswap_preset":
logger.warning("'%s' is not a valid plugin preset file", filename)
return
if opts.get("__section") != self._full_key:
logger.warning("You are attempting to load a preset for '%s' into '%s'. Aborted.",
opts.get("__section", "no section"), self._full_key)
return
logger.debug("Loaded preset: %s", opts)
exist = self._parent.config_dict[self._parent.displayed_key]["options"]
for key, val in opts.items():
if key.startswith("__") or key not in exist:
logger.debug("Skipping non-existent item: '%s'", key)
continue
logger.debug("Setting '%s' to '%s'", key, val)
exist[key].set(val)
logger.info("Preset loaded from: '%s'", os.path.basename(filename))
def save(self):
""" Action to perform when save preset button is pressed.
Compiles currently displayed configuration options into a json file and saves into selected
location.
"""
filename = self._get_filename("save")
if not filename:
return
opts = self._parent.config_dict[self._parent.displayed_key]["options"]
preset = {opt: val.get() for opt, val in opts.items()}
preset["__filetype"] = "faceswap_preset"
preset["__section"] = self._full_key
self._serializer.save(filename, preset)
logger.info("Preset '%s' saved to: '%s'", self._full_key, filename)
def _get_filename(self, action):
""" Obtain the filename for load and save preset actions.
Parameters
----------
action: ["load", "save"]
The preset action that is being performed
Returns
-------
str: The requested preset filename
"""
if not self._parent.config_dict.get(self._parent.displayed_key):
logger.info("No settings to %s for the current page.", action)
return None
args = ("save_filename", "json") if action == "save" else ("filename", "json")
kwargs = {"title": f"{action.title()} Preset...",
"initial_folder": self._preset_path,
"parent": self._parent}
if action == "save":
kwargs["initial_file"] = self._get_initial_filename()
filename = FileHandler(*args, **kwargs).return_file
if not filename:
logger.debug("%s cancelled", action.title())
self._raise_toplevel()
return filename
def _get_initial_filename(self):
""" Obtain the initial filename for saving a preset.
The name is based on the plugin's display key. A scan of the default presets folder is done
to ensure no filename clash. If a filename does clash, then an integer is added to the end.
Returns
-------
str
The initial preset filename
"""
_, key = self._full_key.split("|", 1)
base_filename = f"{key.replace('|', '_')}_preset"
i = 0
filename = f"{base_filename}.json"
while True:
if not os.path.exists(os.path.join(self._preset_path, filename)):
break
logger.debug("File pre-exists: %s", filename)
filename = f"{base_filename}_{i}.json"
i += 1
logger.debug("Initial filename: %s", filename)
return filename
def _raise_toplevel(self):
""" Opening a file dialog tends to hide the top level pop up, so bring back to the
fore. """
self._popup.update()
self._popup.deiconify()
self._popup.lift()