mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
566 lines
22 KiB
Python
566 lines
22 KiB
Python
#!/usr/bin python3
|
|
""" The Menu Bars for faceswap GUI """
|
|
|
|
import gettext
|
|
import locale
|
|
import logging
|
|
import os
|
|
import sys
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import webbrowser
|
|
|
|
from subprocess import Popen, PIPE, STDOUT
|
|
|
|
from lib.multithreading import MultiThread
|
|
from lib.serializer import get_serializer
|
|
from lib.utils import FaceswapError
|
|
import update_deps
|
|
|
|
from .popup_configure import open_popup
|
|
from .custom_widgets import Tooltip
|
|
from .utils import get_config, get_images
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
# LOCALES
|
|
_LANG = gettext.translation("gui.tooltips", localedir="locales", fallback=True)
|
|
_ = _LANG.gettext
|
|
|
|
_WORKING_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
|
|
_RESOURCES = [(_("faceswap.dev - Guides and Forum"), "https://www.faceswap.dev"),
|
|
(_("Patreon - Support this project"), "https://www.patreon.com/faceswap"),
|
|
(_("Discord - The FaceSwap Discord server"), "https://discord.gg/VasFUAy"),
|
|
(_("Github - Our Source Code"), "https://github.com/deepfakes/faceswap")]
|
|
|
|
|
|
class MainMenuBar(tk.Menu): # pylint:disable=too-many-ancestors
|
|
""" GUI Main Menu Bar """
|
|
def __init__(self, master=None):
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
super().__init__(master)
|
|
self.root = master
|
|
|
|
self.file_menu = FileMenu(self)
|
|
self.settings_menu = SettingsMenu(self)
|
|
self.help_menu = HelpMenu(self)
|
|
|
|
self.add_cascade(label="File", menu=self.file_menu, underline=0)
|
|
self.add_cascade(label="Settings", menu=self.settings_menu, underline=0)
|
|
self.add_cascade(label="Help", menu=self.help_menu, underline=0)
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
|
|
class SettingsMenu(tk.Menu): # pylint:disable=too-many-ancestors
|
|
""" Settings menu items and functions """
|
|
def __init__(self, parent):
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
super().__init__(parent, tearoff=0)
|
|
self.root = parent.root
|
|
self.build()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def build(self):
|
|
""" Add the settings menu to the menu bar """
|
|
# pylint: disable=cell-var-from-loop
|
|
logger.debug("Building settings menu")
|
|
self.add_command(label="Configure Settings...",
|
|
underline=0,
|
|
command=open_popup)
|
|
logger.debug("Built settings menu")
|
|
|
|
|
|
class FileMenu(tk.Menu): # pylint:disable=too-many-ancestors
|
|
""" File menu items and functions """
|
|
def __init__(self, parent):
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
super().__init__(parent, tearoff=0)
|
|
self.root = parent.root
|
|
self._config = get_config()
|
|
self.recent_menu = tk.Menu(self, tearoff=0, postcommand=self.refresh_recent_menu)
|
|
self.build()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def build(self):
|
|
""" Add the file menu to the menu bar """
|
|
logger.debug("Building File menu")
|
|
self.add_command(label="New Project...",
|
|
underline=0,
|
|
accelerator="Ctrl+N",
|
|
command=self._config.project.new)
|
|
self.root.bind_all("<Control-n>", self._config.project.new)
|
|
self.add_command(label="Open Project...",
|
|
underline=0,
|
|
accelerator="Ctrl+O",
|
|
command=self._config.project.load)
|
|
self.root.bind_all("<Control-o>", self._config.project.load)
|
|
self.add_command(label="Save Project",
|
|
underline=0,
|
|
accelerator="Ctrl+S",
|
|
command=lambda: self._config.project.save(save_as=False))
|
|
self.root.bind_all("<Control-s>", lambda e: self._config.project.save(e, save_as=False))
|
|
self.add_command(label="Save Project as...",
|
|
underline=13,
|
|
accelerator="Ctrl+Alt+S",
|
|
command=lambda: self._config.project.save(save_as=True))
|
|
self.root.bind_all("<Control-Alt-s>", lambda e: self._config.project.save(e, save_as=True))
|
|
self.add_command(label="Reload Project from Disk",
|
|
underline=0,
|
|
accelerator="F5",
|
|
command=self._config.project.reload)
|
|
self.root.bind_all("<F5>", self._config.project.reload)
|
|
self.add_command(label="Close Project",
|
|
underline=0,
|
|
accelerator="Ctrl+W",
|
|
command=self._config.project.close)
|
|
self.root.bind_all("<Control-w>", self._config.project.close)
|
|
self.add_separator()
|
|
self.add_command(label="Open Task...",
|
|
underline=5,
|
|
accelerator="Ctrl+Alt+T",
|
|
command=lambda: self._config.tasks.load(current_tab=False))
|
|
self.root.bind_all("<Control-Alt-t>",
|
|
lambda e: self._config.tasks.load(e, current_tab=False))
|
|
self.add_separator()
|
|
self.add_cascade(label="Open recent", underline=6, menu=self.recent_menu)
|
|
self.add_separator()
|
|
self.add_command(label="Quit",
|
|
underline=0,
|
|
accelerator="Alt+F4",
|
|
command=self.root.close_app)
|
|
self.root.bind_all("<Alt-F4>", self.root.close_app)
|
|
logger.debug("Built File menu")
|
|
|
|
def build_recent_menu(self):
|
|
""" Load recent files into menu bar """
|
|
logger.debug("Building Recent Files menu")
|
|
serializer = get_serializer("json")
|
|
menu_file = os.path.join(self._config.pathcache, ".recent.json")
|
|
if not os.path.isfile(menu_file) or os.path.getsize(menu_file) == 0:
|
|
self.clear_recent_files(serializer, menu_file)
|
|
try:
|
|
recent_files = serializer.load(menu_file)
|
|
except FaceswapError as err:
|
|
if "Error unserializing data for type" in str(err):
|
|
# Some reports of corruption breaking menus
|
|
logger.warning("There was an error opening the recent files list so it has been "
|
|
"reset.")
|
|
self.clear_recent_files(serializer, menu_file)
|
|
recent_files = []
|
|
|
|
logger.debug("Loaded recent files: %s", recent_files)
|
|
removed_files = []
|
|
for recent_item in recent_files:
|
|
filename, command = recent_item
|
|
if not os.path.isfile(filename):
|
|
logger.debug("File does not exist. Flagging for removal: '%s'", filename)
|
|
removed_files.append(recent_item)
|
|
continue
|
|
# Legacy project files didn't have a command stored
|
|
command = command if command else "project"
|
|
logger.debug("processing: ('%s', %s)", filename, command)
|
|
if command.lower() == "project":
|
|
load_func = self._config.project.load
|
|
lbl = command
|
|
kwargs = dict(filename=filename)
|
|
else:
|
|
load_func = self._config.tasks.load
|
|
lbl = f"{command} Task"
|
|
kwargs = dict(filename=filename, current_tab=False)
|
|
self.recent_menu.add_command(
|
|
label=f"{filename} ({lbl.title()})",
|
|
command=lambda kw=kwargs, fn=load_func: fn(**kw))
|
|
if removed_files:
|
|
for recent_item in removed_files:
|
|
logger.debug("Removing from recent files: `%s`", recent_item[0])
|
|
recent_files.remove(recent_item)
|
|
serializer.save(menu_file, recent_files)
|
|
self.recent_menu.add_separator()
|
|
self.recent_menu.add_command(
|
|
label="Clear recent files",
|
|
underline=0,
|
|
command=lambda srl=serializer, mnu=menu_file: self.clear_recent_files(srl, mnu))
|
|
|
|
logger.debug("Built Recent Files menu")
|
|
|
|
@staticmethod
|
|
def clear_recent_files(serializer, menu_file):
|
|
""" Creates or clears recent file list """
|
|
logger.debug("clearing recent files list: '%s'", menu_file)
|
|
serializer.save(menu_file, [])
|
|
|
|
def refresh_recent_menu(self):
|
|
""" Refresh recent menu on save/load of files """
|
|
self.recent_menu.delete(0, "end")
|
|
self.build_recent_menu()
|
|
|
|
|
|
class HelpMenu(tk.Menu): # pylint:disable=too-many-ancestors
|
|
""" Help menu items and functions """
|
|
def __init__(self, parent):
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
super().__init__(parent, tearoff=0)
|
|
self.root = parent.root
|
|
self.recources_menu = tk.Menu(self, tearoff=0)
|
|
self._branches_menu = tk.Menu(self, tearoff=0)
|
|
self.build()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def build(self):
|
|
""" Build the help menu """
|
|
logger.debug("Building Help menu")
|
|
|
|
self.add_command(label="Check for updates...",
|
|
underline=0,
|
|
command=lambda action="check": self.in_thread(action))
|
|
self.add_command(label="Update Faceswap...",
|
|
underline=0,
|
|
command=lambda action="update": self.in_thread(action))
|
|
if self._build_branches_menu():
|
|
self.add_cascade(label="Switch Branch", underline=7, menu=self._branches_menu)
|
|
self.add_separator()
|
|
self._build_recources_menu()
|
|
self.add_cascade(label="Resources", underline=0, menu=self.recources_menu)
|
|
self.add_separator()
|
|
self.add_command(label="Output System Information",
|
|
underline=0,
|
|
command=lambda action="output_sysinfo": self.in_thread(action))
|
|
logger.debug("Built help menu")
|
|
|
|
def _build_branches_menu(self):
|
|
""" Build branch selection menu.
|
|
|
|
Queries git for available branches and builds a menu based on output.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if menu was successfully built otherwise ``False``
|
|
"""
|
|
stdout = self._get_branches()
|
|
if stdout is None:
|
|
return False
|
|
|
|
branches = self._filter_branches(stdout)
|
|
if not branches:
|
|
return False
|
|
|
|
for branch in branches:
|
|
self._branches_menu.add_command(
|
|
label=branch,
|
|
command=lambda b=branch: self._switch_branch(b))
|
|
return True
|
|
|
|
@staticmethod
|
|
def _get_branches():
|
|
""" Get the available github branches
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The list of branches available. If no branches were found or there was an
|
|
error then `None` is returned
|
|
"""
|
|
gitcmd = "git branch -a"
|
|
with Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR) as cmd:
|
|
stdout, _ = cmd.communicate()
|
|
retcode = cmd.poll()
|
|
if retcode != 0:
|
|
logger.debug("Unable to list git branches. return code: %s, message: %s",
|
|
retcode, stdout.decode().strip().replace("\n", " - "))
|
|
return None
|
|
return stdout.decode(locale.getpreferredencoding())
|
|
|
|
@staticmethod
|
|
def _filter_branches(stdout):
|
|
""" Filter the branches, remove duplicates and the current branch and return a sorted
|
|
list.
|
|
|
|
Parameters
|
|
----------
|
|
stdout: str
|
|
The output from the git branch query converted to a string
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
Unique list of available branches sorted in alphabetical order
|
|
"""
|
|
current = None
|
|
branches = set()
|
|
for line in stdout.splitlines():
|
|
branch = line[line.rfind("/") + 1:] if "/" in line else line.strip()
|
|
if branch.startswith("*"):
|
|
branch = branch.replace("*", "").strip()
|
|
current = branch
|
|
continue
|
|
branches.add(branch)
|
|
logger.debug("Found branches: %s", branches)
|
|
if current in branches:
|
|
logger.debug("Removing current branch from output: %s", current)
|
|
branches.remove(current)
|
|
|
|
branches = sorted(list(branches), key=str.casefold)
|
|
logger.debug("Final branches: %s", branches)
|
|
return branches
|
|
|
|
@staticmethod
|
|
def _switch_branch(branch):
|
|
""" Change the currently checked out branch, and return a notification.
|
|
|
|
Parameters
|
|
----------
|
|
str
|
|
The branch to switch to
|
|
"""
|
|
logger.info("Switching branch to '%s'...", branch)
|
|
gitcmd = f"git checkout {branch}"
|
|
with Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR) as cmd:
|
|
stdout, _ = cmd.communicate()
|
|
retcode = cmd.poll()
|
|
if retcode != 0:
|
|
logger.error("Unable to switch branch. return code: %s, message: %s",
|
|
retcode, stdout.decode().strip().replace("\n", " - "))
|
|
return
|
|
logger.info("Succesfully switched to '%s'. You may want to check for updates to make sure "
|
|
"that you have the latest code.", branch)
|
|
logger.info("Please restart Faceswap to complete the switch.")
|
|
|
|
def _build_recources_menu(self):
|
|
""" Build resources menu """
|
|
# pylint: disable=cell-var-from-loop
|
|
logger.debug("Building Resources Files menu")
|
|
for resource in _RESOURCES:
|
|
self.recources_menu.add_command(
|
|
label=resource[0],
|
|
command=lambda link=resource[1]: webbrowser.open_new(link))
|
|
logger.debug("Built resources menu")
|
|
|
|
def in_thread(self, action):
|
|
""" Perform selected action inside a thread """
|
|
logger.debug("Performing help action: %s", action)
|
|
thread = MultiThread(getattr(self, action), thread_count=1)
|
|
thread.start()
|
|
logger.debug("Performed help action: %s", action)
|
|
|
|
@staticmethod
|
|
def clear_console():
|
|
""" Clear the console window """
|
|
get_config().tk_vars["console_clear"].set(True)
|
|
|
|
def output_sysinfo(self):
|
|
""" Output system information to console """
|
|
logger.debug("Obtaining system information")
|
|
self.root.config(cursor="watch")
|
|
self.clear_console()
|
|
try:
|
|
from lib.sysinfo import sysinfo # pylint:disable=import-outside-toplevel
|
|
info = sysinfo
|
|
except Exception as err: # pylint:disable=broad-except
|
|
info = f"Error obtaining system info: {str(err)}"
|
|
self.clear_console()
|
|
logger.debug("Obtained system information: %s", info)
|
|
print(info)
|
|
self.root.config(cursor="")
|
|
|
|
def check(self):
|
|
""" Check for updates and clone repository """
|
|
logger.debug("Checking for updates...")
|
|
self.root.config(cursor="watch")
|
|
encoding = locale.getpreferredencoding()
|
|
logger.debug("Encoding: %s", encoding)
|
|
self.check_for_updates(encoding, check=True)
|
|
self.root.config(cursor="")
|
|
|
|
def update(self):
|
|
""" Check for updates and clone repository """
|
|
logger.debug("Updating Faceswap...")
|
|
self.root.config(cursor="watch")
|
|
encoding = locale.getpreferredencoding()
|
|
logger.debug("Encoding: %s", encoding)
|
|
success = False
|
|
if self.check_for_updates(encoding):
|
|
success = self.do_update(encoding)
|
|
update_deps.main(is_gui=True)
|
|
if success:
|
|
logger.info("Please restart Faceswap to complete the update.")
|
|
self.root.config(cursor="")
|
|
|
|
@staticmethod
|
|
def check_for_updates(encoding, check=False):
|
|
""" Check whether an update is required """
|
|
# Do the check
|
|
logger.info("Checking for updates...")
|
|
update = False
|
|
msg = ""
|
|
gitcmd = "git remote update && git status -uno"
|
|
with Popen(gitcmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd=_WORKING_DIR) as cmd:
|
|
stdout, _ = cmd.communicate()
|
|
retcode = cmd.poll()
|
|
if retcode != 0:
|
|
msg = ("Git is not installed or you are not running a cloned repo. "
|
|
"Unable to check for updates")
|
|
else:
|
|
chk = stdout.decode(encoding).splitlines()
|
|
for line in chk:
|
|
if line.lower().startswith("your branch is ahead"):
|
|
msg = "Your branch is ahead of the remote repo. Not updating"
|
|
break
|
|
if line.lower().startswith("your branch is up to date"):
|
|
msg = "Faceswap is up to date."
|
|
break
|
|
if line.lower().startswith("your branch is behind"):
|
|
msg = "There are updates available"
|
|
update = True
|
|
break
|
|
if "have diverged" in line.lower():
|
|
msg = "Your branch has diverged from the remote repo. Not updating"
|
|
break
|
|
if not update or check:
|
|
logger.info(msg)
|
|
logger.debug("Checked for update. Update required: %s", update)
|
|
return update
|
|
|
|
@staticmethod
|
|
def do_update(encoding):
|
|
""" Update Faceswap """
|
|
logger.info("A new version is available. Updating...")
|
|
gitcmd = "git pull"
|
|
with Popen(gitcmd,
|
|
shell=True,
|
|
stdout=PIPE,
|
|
stderr=STDOUT,
|
|
bufsize=1,
|
|
cwd=_WORKING_DIR) as cmd:
|
|
while True:
|
|
output = cmd.stdout.readline().decode(encoding)
|
|
if output == "" and cmd.poll() is not None:
|
|
break
|
|
if output:
|
|
logger.debug("'%s' output: '%s'", gitcmd, output.strip())
|
|
print(output.strip())
|
|
retcode = cmd.poll()
|
|
logger.debug("'%s' returncode: %s", gitcmd, retcode)
|
|
if retcode != 0:
|
|
logger.info("An error occurred during update. return code: %s", retcode)
|
|
retval = False
|
|
else:
|
|
retval = True
|
|
return retval
|
|
|
|
|
|
class TaskBar(ttk.Frame): # pylint: disable=too-many-ancestors
|
|
""" Task bar buttons """
|
|
def __init__(self, parent):
|
|
super().__init__(parent)
|
|
self._config = get_config()
|
|
self.pack(side=tk.TOP, anchor=tk.W, fill=tk.X, expand=False)
|
|
self._btn_frame = ttk.Frame(self)
|
|
self._btn_frame.pack(side=tk.TOP, pady=2, anchor=tk.W, fill=tk.X, expand=False)
|
|
|
|
self._project_btns()
|
|
self._group_separator()
|
|
self._task_btns()
|
|
self._group_separator()
|
|
self._settings_btns()
|
|
self._section_separator()
|
|
|
|
def _project_btns(self):
|
|
frame = ttk.Frame(self._btn_frame)
|
|
frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)
|
|
|
|
for btntype in ("new", "load", "save", "save_as", "reload"):
|
|
logger.debug("Adding button: '%s'", btntype)
|
|
|
|
loader, kwargs = self._loader_and_kwargs(btntype)
|
|
cmd = getattr(self._config.project, loader)
|
|
btn = ttk.Button(frame,
|
|
image=get_images().icons[btntype],
|
|
command=lambda fn=cmd, kw=kwargs: fn(**kw))
|
|
btn.pack(side=tk.LEFT, anchor=tk.W)
|
|
hlp = self.set_help(btntype)
|
|
Tooltip(btn, text=hlp, wrap_length=200)
|
|
|
|
def _task_btns(self):
|
|
frame = ttk.Frame(self._btn_frame)
|
|
frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)
|
|
|
|
for loadtype in ("load", "save", "save_as", "clear", "reload"):
|
|
btntype = f"{loadtype}2"
|
|
logger.debug("Adding button: '%s'", btntype)
|
|
|
|
loader, kwargs = self._loader_and_kwargs(loadtype)
|
|
if loadtype == "load":
|
|
kwargs["current_tab"] = True
|
|
cmd = getattr(self._config.tasks, loader)
|
|
btn = ttk.Button(
|
|
frame,
|
|
image=get_images().icons[btntype],
|
|
command=lambda fn=cmd, kw=kwargs: fn(**kw))
|
|
btn.pack(side=tk.LEFT, anchor=tk.W)
|
|
hlp = self.set_help(btntype)
|
|
Tooltip(btn, text=hlp, wrap_length=200)
|
|
|
|
@staticmethod
|
|
def _loader_and_kwargs(btntype):
|
|
if btntype == "save":
|
|
loader = btntype
|
|
kwargs = dict(save_as=False)
|
|
elif btntype == "save_as":
|
|
loader = "save"
|
|
kwargs = dict(save_as=True)
|
|
else:
|
|
loader = btntype
|
|
kwargs = {}
|
|
logger.debug("btntype: %s, loader: %s, kwargs: %s", btntype, loader, kwargs)
|
|
return loader, kwargs
|
|
|
|
def _settings_btns(self):
|
|
# pylint: disable=cell-var-from-loop
|
|
frame = ttk.Frame(self._btn_frame)
|
|
frame.pack(side=tk.LEFT, anchor=tk.W, expand=False, padx=2)
|
|
for name in ("extract", "train", "convert"):
|
|
btntype = f"settings_{name}"
|
|
btntype = btntype if btntype in get_images().icons else "settings"
|
|
logger.debug("Adding button: '%s'", btntype)
|
|
btn = ttk.Button(
|
|
frame,
|
|
image=get_images().icons[btntype],
|
|
command=lambda n=name: open_popup(name=n))
|
|
btn.pack(side=tk.LEFT, anchor=tk.W)
|
|
hlp = _("Configure {} settings...").format(name.title())
|
|
Tooltip(btn, text=hlp, wrap_length=200)
|
|
|
|
@staticmethod
|
|
def set_help(btntype):
|
|
""" Set the helptext for option buttons """
|
|
logger.debug("Setting help")
|
|
hlp = ""
|
|
task = _("currently selected Task") if btntype[-1] == "2" else _("Project")
|
|
if btntype.startswith("reload"):
|
|
hlp = _("Reload {} from disk").format(task)
|
|
if btntype == "new":
|
|
hlp = _("Create a new {}...").format(task)
|
|
if btntype.startswith("clear"):
|
|
hlp = _("Reset {} to default").format(task)
|
|
elif btntype.startswith("save") and "_" not in btntype:
|
|
hlp = _("Save {}").format(task)
|
|
elif btntype.startswith("save_as"):
|
|
hlp = _("Save {} as...").format(task)
|
|
elif btntype.startswith("load"):
|
|
msg = task
|
|
if msg.endswith("Task"):
|
|
msg += _(" from a task or project file")
|
|
hlp = _("Load {}...").format(msg)
|
|
return hlp
|
|
|
|
def _group_separator(self):
|
|
separator = ttk.Separator(self._btn_frame, orient="vertical")
|
|
separator.pack(padx=(2, 1), fill=tk.Y, side=tk.LEFT)
|
|
|
|
def _section_separator(self):
|
|
frame = ttk.Frame(self)
|
|
frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
separator = ttk.Separator(frame, orient="horizontal")
|
|
separator.pack(fill=tk.X, side=tk.LEFT, expand=True)
|