1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-07 10:43:27 -04:00
faceswap/lib/gui/project.py
2024-04-03 14:03:54 +01:00

1031 lines
40 KiB
Python

#!/usr/bin/env python3
""" Handling of Faceswap GUI Projects, Tasks and Last Session """
import logging
import os
import tkinter as tk
from tkinter import messagebox
from lib.serializer import get_serializer
logger = logging.getLogger(__name__)
class _GuiSession(): # pylint:disable=too-few-public-methods
""" Parent class for GUI Session Handlers.
Parameters
----------
config: :class:`lib.gui.utils.Config`
The master GUI config
file_handler: :class:`lib.gui.utils.FileHandler`
A file handler object
"""
def __init__(self, config, file_handler=None):
# NB file_handler has to be passed in to avoid circular imports
logger.debug("Initializing: %s: (config: %s, file_handler: %s)",
self.__class__.__name__, config, file_handler)
self._serializer = get_serializer("json")
self._config = config
self._options = None
self._file_handler = file_handler
self._filename = None
self._saved_tasks = None
self._modified = False
logger.debug("Initialized: %s", self.__class__.__name__)
@property
def _active_tab(self):
""" str: The name of the currently selected :class:`lib.gui.command.CommandNotebook`
tab. """
notebook = self._config.command_notebook
toolsbook = self._config.tools_notebook
command = notebook.tab(notebook.select(), "text").lower()
if command == "tools":
command = toolsbook.tab(toolsbook.select(), "text").lower()
logger.debug("Active tab: %s", command)
return command
@property
def _modified_vars(self):
""" dict: The tkinter Boolean vars indicating the modified state for each tab. """
return self._config.modified_vars
@property
def _file_exists(self):
""" bool: ``True`` if :attr:`_filename` exists otherwise ``False``. """
return self._filename is not None and os.path.isfile(self._filename)
@property
def _cli_options(self):
""" dict: the raw cli options from :attr:`_options` with project fields removed. """
return {key: val for key, val in self._options.items() if isinstance(val, dict)}
@property
def _default_options(self):
""" dict: The default options for all tabs """
return self._config.default_options
@property
def _dirname(self):
""" str: The folder name that :attr:`_filename` resides in. Returns ``None`` if
filename is ``None``. """
return os.path.dirname(self._filename) if self._filename is not None else None
@property
def _basename(self):
""" str: The base name of :attr:`_filename`. Returns ``None`` if filename is ``None``. """
return os.path.basename(self._filename) if self._filename is not None else None
@property
def _stored_tab_name(self):
"""str: The tab_name stored in :attr:`_options` or ``None`` if it does not exist """
if self._options is None:
return None
return self._options.get("tab_name", None)
@property
def _selected_to_choices(self):
""" dict: The selected value and valid choices for multi-option, radio or combo options.
"""
valid_choices = {cmd: {opt: dict(choices=val["cpanel_option"].choices,
is_multi=val["cpanel_option"].is_multi_option)
for opt, val in data.items()
if isinstance(val, dict) and "cpanel_option" in val
and val["cpanel_option"].choices is not None}
for cmd, data in self._config.cli_opts.opts.items()}
logger.trace("valid_choices: %s", valid_choices)
retval = {command: {option: {"value": value,
"is_multi": valid_choices[command][option]["is_multi"],
"choices": valid_choices[command][option]["choices"]}
for option, value in options.items()
if value and command in valid_choices
and option in valid_choices[command]}
for command, options in self._options.items()
if isinstance(options, dict)}
logger.trace("returning: %s", retval)
return retval
def _current_gui_state(self, command=None):
""" The current state of the GUI.
Parameters
----------
command: str, optional
If provided, returns the state of just the given tab command. If ``None`` returns options
for all tabs. Default ``None``
Returns
-------
dict: The options currently set in the GUI
"""
return self._config.cli_opts.get_option_values(command)
def _set_filename(self, filename=None, sess_type="project"):
""" Set the :attr:`_filename` attribute.
:attr:`_filename` is set either from a given filename or the result from
a :attr:`_file_handler`.
Parameters
----------
filename: str, optional
An optional filename. If given then this filename will be used otherwise it will be
collected by a :attr:`_file_handler`
sess_type: {all, project, task}, optional
The session type that the filename is being set for. Dictates the type of file handler
that is opened.
Returns
-------
bool: `True` if filename has been successfully set otherwise ``False``
"""
logger.debug("filename: '%s', sess_type: '%s'", filename, sess_type)
handler = f"config_{sess_type}"
if filename is None:
logger.debug("Popping file handler")
cfgfile = self._file_handler("open", handler).return_file
if not cfgfile:
logger.debug("No filename given")
return False
filename = cfgfile.name
cfgfile.close()
if not os.path.isfile(filename):
msg = f"File does not exist: '{filename}'"
logger.error(msg)
return False
ext = os.path.splitext(filename)[1]
if (sess_type == "project" and ext != ".fsw") or (sess_type == "task" and ext != ".fst"):
logger.debug("Invalid file extension for session type: (sess_type: '%s', "
"extension: '%s')", sess_type, ext)
return False
logger.debug("Setting filename: '%s'", filename)
self._filename = filename
return True
# GUI STATE SETTING
def _set_options(self, command=None):
""" Set the GUI options based on the currently stored properties of :attr:`_options`
and sets the active tab.
Parameters
----------
command: str, optional
The tab to set the options for. If None then sets options for all tabs.
Default: ``None``
"""
opts = self._get_options_for_command(command) if command else self._cli_options
logger.debug("command: %s, opts: %s", command, opts)
if opts is None:
logger.debug("No options found. Returning")
return
for cmd, opt in opts.items():
self._set_gui_state_for_command(cmd, opt)
tab_name = self._options.get("tab_name", None) if command is None else command
tab_name = tab_name if tab_name is not None else "extract"
logger.debug("tab_name: %s", tab_name)
self._config.set_active_tab_by_name(tab_name)
def _get_options_for_command(self, command):
""" Return a single command's options from :attr:`_options` formatted consistently with
an all options dict.
Parameters
----------
command: str
The command to return the options for
Returns
-------
dict: The options for a single command in the format {command: options}. If the command
is not found then returns ``None``
"""
logger.debug(command)
opts = self._options.get(command, None)
retval = {command: opts}
if not opts:
self._config.tk_vars.console_clear.set(True)
logger.info("No %s section found in file", command)
retval = None
logger.debug(retval)
return retval
def _set_gui_state_for_command(self, command, options):
""" Set the GUI state for the given command.
Parameters
----------
command: str
The tab to set the options for
options: dict
The option values to set the GUI to
"""
logger.debug("command: %s: options: %s", command, options)
if not options:
logger.debug("No options provided, not updating GUI")
return
for srcopt, srcval in options.items():
optvar = self._config.cli_opts.get_one_option_variable(command, srcopt)
if not optvar:
continue
logger.trace("setting option: (srcopt: %s, optvar: %s, srcval: %s)",
srcopt, optvar, srcval)
optvar.set(srcval)
def _reset_modified_var(self, command=None):
""" Reset :attr:`_modified_vars` variables back to unmodified (`False`) for all
commands or for the given command.
Parameters
----------
command: str, optional
The command to reset the modified tkinter variable for. If ``None`` then all tkinter
modified variables are reset to `False`. Default: ``None``
"""
for key, tk_var in self._modified_vars.items():
if (command is None or command == key) and tk_var.get():
logger.debug("Reset modified state for: (command: %s key: %s)", command, key)
tk_var.set(False)
# RECENT FILE HANDLING
def _add_to_recent(self, command=None):
""" Add the file for this session to the recent files list.
Parameters
----------
command: str, optional
The command that this session relates to. If `None` then the whole project is added.
Default: ``None``
"""
logger.debug(command)
if self._filename is None:
logger.debug("No filename for selected file. Not adding to recent.")
return
recent_filename = os.path.join(self._config.pathcache, ".recent.json")
logger.debug("Adding to recent files '%s': (%s, %s)",
recent_filename, self._filename, command)
if not os.path.exists(recent_filename) or os.path.getsize(recent_filename) == 0:
logger.debug("Starting with empty recent_files list")
recent_files = []
else:
logger.debug("loading recent_files list: %s", recent_filename)
recent_files = self._serializer.load(recent_filename)
logger.debug("Initial recent files: %s", recent_files)
recent_files = self._del_from_recent(self._filename, recent_files)
ftype = "project" if command is None else command
recent_files.insert(0, (self._filename, ftype))
recent_files = recent_files[:20]
logger.debug("Final recent files: %s", recent_files)
self._serializer.save(recent_filename, recent_files)
def _del_from_recent(self, filename, recent_files=None, save=False):
""" Remove an item from the recent files list.
Parameters
----------
filename: str
The filename to be removed from the recent files list
recent_files: list, optional
If the recent files list has already been loaded, it can be passed in to avoid
loading again. If ``None`` then load the recent files list from disk. Default: ``None``
save: bool, optional
Whether the recent files list should be saved after removing the file. ``True`` saves
the file, ``False`` does not. Default: ``False``
"""
recent_filename = os.path.join(self._config.pathcache, ".recent.json")
if recent_files is None:
logger.debug("Loading file list from disk: %s", recent_filename)
if not os.path.exists(recent_filename) or os.path.getsize(recent_filename) == 0:
logger.debug("No recent file list")
return None
recent_files = self._serializer.load(recent_filename)
filenames = [recent[0] for recent in recent_files]
if filename in filenames:
idx = filenames.index(filename)
logger.debug("Removing from recent file list: %s", filename)
del recent_files[idx]
if save:
logger.debug("Saving recent files list: %s", recent_filename)
self._serializer.save(recent_filename, recent_files)
else:
logger.debug("Filename '%s' does not appear in recent file list", filename)
return recent_files
def _get_lone_task(self):
""" Get the sole command name from :attr:`_options`.
Returns
-------
str: The only existing command name in the current :attr:`_options` dict or ``None`` if
there are multiple commands stored.
"""
command = None
if len(self._cli_options) == 1:
command = list(self._cli_options.keys())[0]
logger.debug(command)
return command
# DISK IO
def _load(self):
""" Load GUI options from :attr:`_filename` location and set to :attr:`_options`.
Returns
-------
bool: ``True`` if successfully loaded otherwise ``False``
"""
if self._file_exists:
logger.debug("Loading config")
self._options = self._serializer.load(self._filename)
self._check_valid_choices()
retval = True
else:
logger.debug("File doesn't exist. Aborting")
retval = False
return retval
def _check_valid_choices(self):
""" Check whether the loaded file has any selected combo/radio/multi-option values that are
no longer valid and remove them so that they are not passed into faceswap. """
for command, options in self._selected_to_choices.items():
for option, data in options.items():
if ((data["is_multi"] and all(v in data["choices"] for v in data["value"].split()))
or not data["is_multi"] and data["value"] in data["choices"]):
continue
if data["is_multi"]:
val = " ".join([v for v in data["value"].split() if v in data["choices"]])
else:
val = ""
val = self._default_options[command][option] if not val else val
logger.debug("Updating invalid value to default: (command: '%s', option: '%s', "
"original value: '%s', new value: '%s')", command, option,
self._options[command][option], val)
self._options[command][option] = val
def _save_as_to_filename(self, session_type):
""" Set :attr:`_filename` from a save as dialog.
Parameters
----------
session_type: ['all', 'task', 'project']
The type of session to pop the save as dialog for. Limits the allowed filetypes
Returns
-------
bool:
True if :attr:`filename` successfully set otherwise ``False``
"""
logger.debug("Popping save as file handler. session_type: '%s'", session_type)
title = f"Save {f'{session_type.title()} ' if session_type != 'all' else ''}As..."
cfgfile = self._file_handler("save",
f"config_{session_type}",
title=title,
initial_folder=self._dirname).return_file
if not cfgfile:
logger.debug("No filename provided. session_type: '%s'", session_type)
return False
self._filename = cfgfile.name
logger.debug("Set filename: (session_type: '%s', filename: '%s'",
session_type, self._filename)
cfgfile.close()
return True
def _save(self, command=None):
""" Collect the options in the current GUI state and save.
Obtains the current options set in the GUI with the selected tab and applies them to
:attr:`_options`. Saves :attr:`_options` to :attr:`_filename`. Resets :attr:_modified_vars
for either the given command or all commands,
Parameters
----------
command: str, optional
The tab to collect the current state for. If ``None`` then collects the current
state for all tabs. Default: ``None``
"""
self._options = self._current_gui_state(command)
self._options["tab_name"] = self._active_tab
logger.debug("Saving options: (filename: %s, options: %s", self._filename, self._options)
self._serializer.save(self._filename, self._options)
self._reset_modified_var(command)
self._add_to_recent(command)
class Tasks(_GuiSession):
""" Faceswap ``.fst`` Task File handling.
Faceswap tasks handle the management of each individual task tab in the GUI. Unlike
:class:`Projects`, Tasks contains all the active tasks currently running, rather than an
individual task.
Parameters
----------
config: :class:`lib.gui.utils.Config`
The master GUI config
file_handler: :class:`lib.gui.utils.FileHandler`
A file handler object
"""
def __init__(self, config, file_handler):
super().__init__(config, file_handler)
self._tasks = {}
@property
def _is_project(self):
""" str: ``True`` if all tasks are from an overarching session project else ``False``."""
retval = False if not self._tasks else all(v.get("is_project", False)
for v in self._tasks.values())
return retval
@property
def _project_filename(self):
""" str: The overarching session project filename."""
fname = None
if not self._is_project:
return fname
for val in self._tasks.values():
fname = val["filename"]
break
return fname
def load(self, *args, # pylint:disable=unused-argument
filename=None, current_tab=True):
""" Load a task into this :class:`Tasks` class.
Tasks can be loaded from project ``.fsw`` files or task ``.fst`` files, depending on where
this function is being called from.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
filename: str, optional
If a filename is passed in, This will be used, otherwise a file handler will be
launched to select the relevant file.
current_tab: bool, optional
``True`` if the task to be loaded must be for the currently selected tab. ``False``
if loading a task into any tab. If current_tab is `True` then tasks can be loaded from
``.fsw`` and ``.fst`` files, otherwise they can only be loaded from ``.fst`` files.
Default: ``True``
"""
logger.debug("Loading task config: (filename: '%s', current_tab: '%s')",
filename, current_tab)
# Option to load specific task from project files:
sess_type = "all" if current_tab else "task"
is_legacy = (not self._is_project and
filename is not None and sess_type == "task" and
os.path.splitext(filename)[1] == ".fsw")
if is_legacy:
logger.debug("Legacy task found: '%s'", filename)
filename = self._update_legacy_task(filename)
filename_set = self._set_filename(filename, sess_type=sess_type)
if not filename_set:
return
loaded = self._load()
if not loaded:
return
command = self._active_tab if current_tab else self._stored_tab_name
command = self._get_lone_task() if command is None else command
if command is None:
logger.error("Unable to determine task from the given file: '%s'", filename)
return
if command not in self._options:
logger.error("No '%s' task in '%s'", command, self._filename)
return
self._set_options(command)
self._add_to_recent(command)
if self._is_project:
self._filename = self._project_filename
elif self._filename.endswith(".fsw"):
self._filename = None
self._add_task(command)
if is_legacy:
self.save()
logger.debug("Loaded task config: (command: '%s', filename: '%s')", command, filename)
def _update_legacy_task(self, filename):
""" Update legacy ``.fsw`` tasks to ``.fst`` tasks.
Tasks loaded from the recent files menu may be passed in with a ``.fsw`` extension.
This renames the file and removes it from the recent file list.
Parameters
----------
filename: str
The filename of the `.fsw` file that needs converting
Returns
-------
str:
The new filename of the updated tasks file
"""
# TODO remove this code after a period of time. Implemented November 2019
logger.debug("original filename: '%s'", filename)
fname, ext = os.path.splitext(filename)
if ext != ".fsw":
logger.debug("Not a .fsw file: '%s'", filename)
return filename
new_filename = f"{fname}.fst"
logger.debug("Renaming '%s' to '%s'", filename, new_filename)
os.rename(filename, new_filename)
self._del_from_recent(filename, save=True)
logger.debug("new filename: '%s'", new_filename)
return new_filename
def save(self, save_as=False):
""" Save the current GUI state for the active tab to a ``.fst`` faceswap task file.
Parameters
----------
save_as: bool, optional
Whether to save to the stored filename, or pop open a file handler to ask for a
location. If there is no stored filename, then a file handler will automatically be
popped.
"""
logger.debug("Saving config...")
self._set_active_task()
save_as = save_as or self._is_project or self._filename is None
if save_as and not self._save_as_to_filename("task"):
return
command = self._active_tab
self._save(command=command)
self._add_task(command)
if not save_as:
logger.info("Saved project to: '%s'", self._filename)
else:
logger.debug("Saved project to: '%s'", self._filename)
def clear(self):
""" Reset all GUI options to their default values for the active tab. """
self._config.cli_opts.reset(self._active_tab)
def reload(self):
""" Reset currently selected tab GUI options to their last saved state. """
self._set_active_task()
if self._options is None:
logger.info("No active task to reload")
return
logger.debug("Reloading task")
self.load(filename=self._filename, current_tab=True)
if self._is_project:
self._reset_modified_var(self._active_tab)
def _add_task(self, command):
""" Add the currently active task to the internal :attr:`_tasks` dict.
If the currently stored task is from an overarching session project, then
only the options are updated. When resetting a tab to saved a project will always
be preferred to a task loaded into the project, so the original reference file name
stays with the project.
Parameters
----------
command: str
The tab that pertains to the currently active task
"""
self._tasks[command] = dict(filename=self._filename,
options=self._options,
is_project=self._is_project)
def clear_tasks(self):
""" Clears all of the stored tasks.
This is required when loading a task stored in a legacy project file, and is only to be
called by :class:`Project` when a project has been loaded which is in fact a task.
"""
logger.debug("Clearing stored tasks")
self._tasks = {}
def add_project_task(self, filename, command, options):
""" Add an individual task from a loaded :class:`Project` to the internal :attr:`_tasks`
dict.
Project tasks take priority over any other tasks, so the individual tasks from a new
project must be placed in the _tasks dict.
Parameters
----------
filename: str
The filename of the session project file
command: str
The tab that this task's options belong to
options: dict
The options for this task loaded from the project
"""
self._tasks[command] = dict(filename=filename, options=options, is_project=True)
def _set_active_task(self, command=None):
""" Set the active :attr:`_filename` and :attr:`_options` to currently selected tab's
options.
Parameters
----------
command: str, optional
If a command is passed in then set the given tab to active, If this is none set the tab
which currently has focus to active. Default: ``None``
"""
logger.debug(command)
command = self._active_tab if command is None else command
task = self._tasks.get(command, None)
if task is None:
self._filename, self._options = (None, None)
else:
self._filename, self._options = (task.get("filename", None), task.get("options", None))
logger.debug("tab: %s, filename: %s, options: %s",
self._active_tab, self._filename, self._options)
class Project(_GuiSession):
""" Faceswap ``.fsw`` Project File handling.
Faceswap projects handle the management of all task tabs in the GUI and updates
the main Faceswap title bar with the project name and modified state.
Parameters
----------
config: :class:`lib.gui.utils.Config`
The master GUI config
file_handler: :class:`lib.gui.utils.FileHandler`
A file handler object
"""
def __init__(self, config, file_handler):
super().__init__(config, file_handler)
self._update_root_title()
@property
def filename(self):
""" str: The currently active project filename. """
return self._filename
@property
def cli_options(self):
""" dict: the raw cli options from :attr:`_options` with project fields removed. """
return self._cli_options
@property
def _project_modified(self):
"""bool: ``True`` if the project has been modified otherwise ``False``. """
return any(var.get() for var in self._modified_vars.values())
@property
def _tasks(self):
""" :class:`Tasks`: The current session's :class:``Tasks``. """
return self._config.tasks
def set_default_options(self):
""" Set the default options. The Default GUI options are stored on Faceswap startup.
Exposed as the :attr:`_default_options` for a project cannot be set until after the main
Command Tabs have been loaded.
"""
logger.debug("Setting options to default")
self._options = self._default_options
# MODIFIED STATE CALLBACK
def set_modified_callback(self):
""" Adds a callback to each of the :attr:`_modified_vars` tkinter variables
When one of these variables is changed, triggers :func:`_modified_callback`
with the command that was changed.
This is exposed as the callback can only be added after the main Command Tabs have
been drawn, and their options' initial values have been set.
"""
for key, tkvar in self._modified_vars.items():
logger.debug("Adding callback for tab: %s", key)
tkvar.trace("w", self._modified_callback)
def _modified_callback(self, *args): # pylint:disable=unused-argument
""" Update the project modified state on a GUI modification change and
update the Faceswap title bar. """
if self._project_modified and self._current_gui_state() == self._cli_options:
logger.debug("Project is same as stored. Setting modified to False")
self._reset_modified_var()
if self._modified != self._project_modified:
logger.debug("Updating project state from variable: (modified: %s)",
self._project_modified)
self._modified = self._project_modified
self._update_root_title()
def load(self, *args, # pylint:disable=unused-argument
filename=None, last_session=False):
""" Load a project from a saved ``.fsw`` project file.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
filename: str, optional
If a filename is passed in, This will be used, otherwise a file handler will be
launched to select the relevant file.
last_session: bool, optional
``True`` if the project is being loaded from the last opened session ``False`` if the
project is being loaded directly from disk. Default: ``False``
"""
logger.debug("Loading project config: (filename: '%s', last_session: %s)",
filename, last_session)
filename_set = self._set_filename(filename, sess_type="project")
if not filename_set:
logger.debug("No filename set")
return
loaded = self._load()
if not loaded:
logger.debug("Options not loaded")
return
# Legacy .fsw files could store projects or tasks. Check if this is a legacy file
# and hand off file to Tasks if necessary
command = self._get_lone_task()
legacy = command is not None
if legacy:
self._handoff_legacy_task()
return
if not last_session:
self._set_options() # Options will be set by last session. Don't set now
self._update_tasks()
self._add_to_recent()
self._reset_modified_var()
self._update_root_title()
logger.debug("Loaded project config: (command: '%s', filename: '%s')", command, filename)
def _handoff_legacy_task(self):
""" Update legacy tasks saved with the old file extension ``.fsw`` to tasks ``.fst``.
Hands off file handling to :class:`Tasks` and resets project to default.
"""
logger.debug("Updating legacy task '%s", self._filename)
filename = self._filename
self._filename = None
self.set_default_options()
self._tasks.clear_tasks()
self._tasks.load(filename=filename, current_tab=False)
logger.debug("Updated legacy task and reset project")
def _update_tasks(self):
""" Add the tasks from the loaded project to the :class:`Tasks` class. """
for key, val in self._cli_options.items():
opts = {key: val}
opts["tab_name"] = key
self._tasks.add_project_task(self._filename, key, opts)
def reload(self, *args): # pylint:disable=unused-argument
""" Reset all GUI's option tabs to their last saved state.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
"""
if self._options is None:
logger.info("No active project to reload")
return
logger.debug("Reloading project")
self._set_options()
self._update_tasks()
self._reset_modified_var()
self._update_root_title()
def _update_root_title(self):
""" Update the root Window title with the project name. Add a asterisk
if the file is modified. """
text = "<untitled project>" if self._basename is None else self._basename
text += "*" if self._modified else ""
self._config.set_root_title(text=text)
def save(self, *args, save_as=False): # pylint:disable=unused-argument
""" Save the current GUI state to a ``.fsw`` project file.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
save_as: bool, optional
Whether to save to the stored filename, or pop open a file handler to ask for a
location. If there is no stored filename, then a file handler will automatically be
popped.
"""
logger.debug("Saving config as...")
save_as = save_as or self._filename is None
if save_as and not self._save_as_to_filename("project"):
return
self._save()
self._update_tasks()
self._update_root_title()
if not save_as:
logger.info("Saved project to: '%s'", self._filename)
else:
logger.debug("Saved project to: '%s'", self._filename)
def new(self, *args): # pylint:disable=unused-argument
""" Create a new project with default options.
Pops a file handler to select location.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
"""
logger.debug("Creating new project")
if not self.confirm_close():
logger.debug("Creating new project cancelled")
return
cfgfile = self._file_handler("save",
"config_project",
title="New Project...",
initial_folder=self._basename).return_file
if not cfgfile:
logger.debug("No filename selected")
return
self._filename = cfgfile.name
cfgfile.close()
self.set_default_options()
self._config.cli_opts.reset()
self._save()
self._update_root_title()
def close(self, *args): # pylint:disable=unused-argument
""" Clear the current project and set all options to default.
Parameters
----------
*args: tuple
Unused, but needs to be present for arguments passed by tkinter event handling
"""
logger.debug("Close requested")
if not self.confirm_close():
logger.debug("Close cancelled")
return
self._config.cli_opts.reset()
self._filename = None
self.set_default_options()
self._reset_modified_var()
self._update_root_title()
self._config.set_active_tab_by_name(self._config.user_config_dict["tab"])
def confirm_close(self):
""" Pop a message box to get confirmation that an unsaved project should be closed
Returns
-------
bool: ``True`` if user confirms close, ``False`` if user cancels close
"""
if not self._modified:
logger.debug("Project is not modified")
return True
confirmtxt = "You have unsaved changes.\n\nAre you sure you want to close the project?"
if messagebox.askokcancel("Close", confirmtxt, default="cancel", icon="warning"):
logger.debug("Close Cancelled")
return True
logger.debug("Close confirmed")
return False
class LastSession(_GuiSession):
""" Faceswap Last Session handling.
Faceswap :class:`LastSession` handles saving the state of the Faceswap GUI at close and
reloading the state at launch.
Last Session behavior can be configured in :file:`config.gui.ini`.
Parameters
----------
config: :class:`lib.gui.utils.Config`
The master GUI config
"""
def __init__(self, config):
super().__init__(config)
self._filename = os.path.join(self._config.pathcache, ".last_session.json")
if not self._enabled:
return
if self._save_option == "prompt":
self.ask_load()
elif self._save_option == "always":
self.load()
@property
def _save_option(self):
""" str: The user config autosave option. """
return self._config.user_config_dict.get("autosave_last_session", "never")
@property
def _enabled(self):
""" bool: ``True`` if autosave is enabled otherwise ``False``. """
return self._save_option != "never"
def from_dict(self, options):
""" Set the :attr:`_options` property based on the given options dictionary
and update the GUI to use these values.
This function is required for reloading the GUI state when the GUI has been force
refreshed on a config change.
Parameters
----------
options: dict
The options to set. Should be the output of :func:`to_dict`
"""
logger.debug("Setting options from dict: %s", options)
self._options = options
self._set_options()
def to_dict(self):
""" Collect the current GUI options and place them in a dict for retrieval or storage.
This function is required for reloading the GUI state when the GUI has been force
refreshed on a config change.
Returns
-------
dict: The current cli options ready for saving or retrieval by :func:`from_dict`
"""
opts = self._current_gui_state()
logger.debug("Collected opts: %s", opts)
if not opts or opts == self._default_options:
logger.debug("Default session, or no opts found. Not saving last session.")
return None
opts["tab_name"] = self._active_tab
opts["project"] = self._config.project.filename
logger.debug("Added project items: %s", {k: v for k, v in opts.items()
if k in ("tab_name", "project")})
return opts
def ask_load(self):
""" Pop a message box to ask the user if they wish to load their last session. """
if not self._file_exists:
logger.debug("No last session file found")
elif tk.messagebox.askyesno("Last Session", "Load last session?"):
logger.debug("Loading last session at user request")
self.load()
else:
logger.debug("Not loading last session at user request")
logger.debug("Deleting LastSession file")
os.remove(self._filename)
def load(self):
""" Load the last session.
Loads the last saved session options. Checks if a previous project was loaded
and whether there have been changes since the last saved version of the project.
Sets the display and :class:`Project` and :class:`Task` objects accordingly.
"""
loaded = self._load()
if not loaded:
return
self._set_project()
self._set_options()
def _set_project(self):
""" Set the :class:`Project` if session is resuming from one. """
if self._options.get("project", None) is None:
logger.debug("No project stored")
else:
logger.debug("Loading stored project")
self._config.project.load(filename=self._options["project"], last_session=True)
def save(self):
""" Save a snapshot of currently set GUI config options.
Called on Faceswap shutdown.
"""
if not self._enabled:
logger.debug("LastSession not enabled")
if os.path.exists(self._filename):
logger.debug("Deleting existing LastSession file")
os.remove(self._filename)
return
opts = self.to_dict()
if opts is None and os.path.exists(self._filename):
logger.debug("Last session default or blank. Clearing saved last session.")
os.remove(self._filename)
if opts is not None:
self._serializer.save(self._filename, opts)
logger.debug("Saved last session. (filename: '%s', opts: %s", self._filename, opts)