#!/usr/bin python3 """ Global configuration optiopns for the Faceswap GUI """ import logging import os import sys import tkinter as tk from dataclasses import dataclass, field from typing import Any, cast, Dict, Optional, Tuple, TYPE_CHECKING from lib.gui._config import Config as UserConfig from lib.gui.project import Project, Tasks from lib.gui.theme import Style from .file_handler import FileHandler if TYPE_CHECKING: from lib.gui.options import CliOptions from lib.gui.custom_widgets import StatusBar from lib.gui.command import CommandNotebook from lib.gui.command import ToolsNotebook logger = logging.getLogger(__name__) # pylint: disable=invalid-name PATHCACHE = os.path.join(os.path.realpath(os.path.dirname(sys.argv[0])), "lib", "gui", ".cache") _CONFIG: Optional["Config"] = None def initialize_config(root: tk.Tk, cli_opts: Optional["CliOptions"], statusbar: Optional["StatusBar"]) -> Optional["Config"]: """ Initialize the GUI Master :class:`Config` and add to global constant. This should only be called once on first GUI startup. Future access to :class:`Config` should only be executed through :func:`get_config`. Parameters ---------- root: :class:`tkinter.Tk` The root Tkinter object cli_opts: :class:`lib.gui.options.CliOptions` or ``None`` The command line options object. Must be provided for main GUI. Must be ``None`` for tools statusbar: :class:`lib.gui.custom_widgets.StatusBar` or ``None`` The GUI Status bar. Must be provided for main GUI. Must be ``None`` for tools Returns ------- :class:`Config` or ``None`` ``None`` if the config has already been initialized otherwise the global configuration options """ global _CONFIG # pylint: disable=global-statement if _CONFIG is not None: return None logger.debug("Initializing config: (root: %s, cli_opts: %s, " "statusbar: %s)", root, cli_opts, statusbar) _CONFIG = Config(root, cli_opts, statusbar) return _CONFIG def get_config() -> "Config": """ Get the Master GUI configuration. Returns ------- :class:`Config` The Master GUI Config """ assert _CONFIG is not None return _CONFIG class GlobalVariables(): """ Global tkinter variables accessible from all parts of the GUI. Should only be accessed from :attr:`get_config().tk_vars` """ def __init__(self) -> None: logger.debug("Initializing %s", self.__class__.__name__) self._display = tk.StringVar() self._running_task = tk.BooleanVar() self._is_training = tk.BooleanVar() self._action_command = tk.StringVar() self._generate_command = tk.StringVar() self._console_clear = tk.BooleanVar() self._refresh_graph = tk.BooleanVar() self._analysis_folder = tk.StringVar() self._initialize_variables() logger.debug("Initialized %s", self.__class__.__name__) @property def display(self) -> tk.StringVar: """ :class:`tkinter.StringVar`: The current Faceswap command running """ return self._display @property def running_task(self) -> tk.BooleanVar: """ :class:`tkinter.BooleanVar`: ``True`` if a Faceswap task is running otherwise ``False`` """ return self._running_task @property def is_training(self) -> tk.BooleanVar: """ :class:`tkinter.BooleanVar`: ``True`` if Faceswap is currently training otherwise ``False`` """ return self._is_training @property def action_command(self) -> tk.StringVar: """ :class:`tkinter.StringVar`: The command line action to perform """ return self._action_command @property def generate_command(self) -> tk.StringVar: """ :class:`tkinter.StringVar`: The command line action to generate """ return self._generate_command @property def console_clear(self) -> tk.BooleanVar: """ :class:`tkinter.BooleanVar`: ``True`` if the console should be cleared otherwise ``False`` """ return self._console_clear @property def refresh_graph(self) -> tk.BooleanVar: """ :class:`tkinter.BooleanVar`: ``True`` if the training graph should be refreshed otherwise ``False`` """ return self._refresh_graph @property def analysis_folder(self) -> tk.StringVar: """ :class:`tkinter.StringVar`: Full path the analysis folder""" return self._analysis_folder def _initialize_variables(self) -> None: """ Initialize the default variable values""" self._display.set("") self._running_task.set(False) self._is_training.set(False) self._action_command.set("") self._generate_command.set("") self._console_clear.set(False) self._refresh_graph.set(False) self._analysis_folder.set("") @dataclass class _GuiObjects: """ Data class for commonly accessed GUI Objects """ cli_opts: Optional["CliOptions"] tk_vars: GlobalVariables project: Project tasks: Tasks status_bar: Optional["StatusBar"] default_options: Dict[str, Dict[str, Any]] = field(default_factory=dict) command_notebook: Optional["CommandNotebook"] = None class Config(): """ The centralized configuration class for holding items that should be made available to all parts of the GUI. This class should be initialized on GUI startup through :func:`initialize_config`. Any further access to this class should be through :func:`get_config`. Parameters ---------- root: :class:`tkinter.Tk` The root Tkinter object cli_opts: :class:`lib.gui.options.CliOptions` or ``None`` The command line options object. Must be provided for main GUI. Must be ``None`` for tools statusbar: :class:`lib.gui.custom_widgets.StatusBar` or ``None`` The GUI Status bar. Must be provided for main GUI. Must be ``None`` for tools """ def __init__(self, root: tk.Tk, cli_opts: Optional["CliOptions"], statusbar: Optional["StatusBar"]) -> None: logger.debug("Initializing %s: (root %s, cli_opts: %s, statusbar: %s)", self.__class__.__name__, root, cli_opts, statusbar) self._default_font = cast(dict, tk.font.nametofont("TkDefaultFont").configure())["family"] self._constants = dict( root=root, scaling_factor=self._get_scaling(root), default_font=self._default_font) self._gui_objects = _GuiObjects( cli_opts=cli_opts, tk_vars=GlobalVariables(), project=Project(self, FileHandler), tasks=Tasks(self, FileHandler), status_bar=statusbar) self._user_config = UserConfig(None) self._style = Style(self.default_font, root, PATHCACHE) self._user_theme = self._style.user_theme logger.debug("Initialized %s", self.__class__.__name__) # Constants @property def root(self) -> tk.Tk: """ :class:`tkinter.Tk`: The root tkinter window. """ return self._constants["root"] @property def scaling_factor(self) -> float: """ float: The scaling factor for current display. """ return self._constants["scaling_factor"] @property def pathcache(self) -> str: """ str: The path to the GUI cache folder """ return PATHCACHE # GUI Objects @property def cli_opts(self) -> "CliOptions": """ :class:`lib.gui.options.CliOptions`: The command line options for this GUI Session. """ # This should only be None when a separate tool (not main GUI) is used, at which point # cli_opts do not exist assert self._gui_objects.cli_opts is not None return self._gui_objects.cli_opts @property def tk_vars(self) -> GlobalVariables: """ dict: The global tkinter variables. """ return self._gui_objects.tk_vars @property def project(self) -> Project: """ :class:`lib.gui.project.Project`: The project session handler. """ return self._gui_objects.project @property def tasks(self) -> Tasks: """ :class:`lib.gui.project.Tasks`: The session tasks handler. """ return self._gui_objects.tasks @property def default_options(self) -> Dict[str, Dict[str, Any]]: """ dict: The default options for all tabs """ return self._gui_objects.default_options @property def statusbar(self) -> "StatusBar": """ :class:`lib.gui.custom_widgets.StatusBar`: The GUI StatusBar :class:`tkinter.ttk.Frame`. """ # This should only be None when a separate tool (not main GUI) is used, at which point # this statusbar does not exist assert self._gui_objects.status_bar is not None return self._gui_objects.status_bar @property def command_notebook(self) -> Optional["CommandNotebook"]: """ :class:`lib.gui.command.CommandNotebook`: The main Faceswap Command Notebook. """ return self._gui_objects.command_notebook # Convenience GUI Objects @property def tools_notebook(self) -> "ToolsNotebook": """ :class:`lib.gui.command.ToolsNotebook`: The Faceswap Tools sub-Notebook. """ assert self.command_notebook is not None return self.command_notebook.tools_notebook @property def modified_vars(self) -> Dict[str, "tk.BooleanVar"]: """ dict: The command notebook modified tkinter variables. """ assert self.command_notebook is not None return self.command_notebook.modified_vars @property def _command_tabs(self) -> Dict[str, int]: """ dict: Command tab titles with their IDs. """ assert self.command_notebook is not None return self.command_notebook.tab_names @property def _tools_tabs(self) -> Dict[str, int]: """ dict: Tools command tab titles with their IDs. """ assert self.command_notebook is not None return self.command_notebook.tools_tab_names # Config @property def user_config(self) -> UserConfig: """ dict: The GUI config in dict form. """ return self._user_config @property def user_config_dict(self) -> Dict[str, Any]: # TODO Dataclass """ dict: The GUI config in dict form. """ return self._user_config.config_dict @property def user_theme(self) -> Dict[str, Any]: # TODO Dataclass """ dict: The GUI theme selection options. """ return self._user_theme @property def default_font(self) -> Tuple[str, int]: """ tuple: The selected font as configured in user settings. First item is the font (`str`) second item the font size (`int`). """ font = self.user_config_dict["font"] font = self._default_font if font == "default" else font return (font, self.user_config_dict["font_size"]) @staticmethod def _get_scaling(root) -> float: """ Get the display DPI. Returns ------- float: The scaling factor """ dpi = root.winfo_fpixels("1i") scaling = dpi / 72.0 logger.debug("dpi: %s, scaling: %s'", dpi, scaling) return scaling def set_default_options(self) -> None: """ Set the default options for :mod:`lib.gui.projects` The Default GUI options are stored on Faceswap startup. Exposed as the :attr:`_default_opts` for a project cannot be set until after the main Command Tabs have been loaded. """ default = self.cli_opts.get_option_values() logger.debug(default) self._gui_objects.default_options = default self.project.set_default_options() def set_command_notebook(self, notebook: "CommandNotebook") -> None: """ Set the command notebook to the :attr:`command_notebook` attribute and enable the modified callback for :attr:`project`. Parameters ---------- notebook: :class:`lib.gui.command.CommandNotebook` The main command notebook for the Faceswap GUI """ logger.debug("Setting commane notebook: %s", notebook) self._gui_objects.command_notebook = notebook self.project.set_modified_callback() def set_active_tab_by_name(self, name: str) -> None: """ Sets the :attr:`command_notebook` or :attr:`tools_notebook` to active based on given name. Parameters ---------- name: str The name of the tab to set active """ assert self.command_notebook is not None name = name.lower() if name in self._command_tabs: tab_id = self._command_tabs[name] logger.debug("Setting active tab to: (name: %s, id: %s)", name, tab_id) self.command_notebook.select(tab_id) elif name in self._tools_tabs: self.command_notebook.select(self._command_tabs["tools"]) tab_id = self._tools_tabs[name] logger.debug("Setting active Tools tab to: (name: %s, id: %s)", name, tab_id) self.tools_notebook.select() else: logger.debug("Name couldn't be found. Setting to id 0: %s", name) self.command_notebook.select(0) def set_modified_true(self, command: str) -> None: """ Set the modified variable to ``True`` for the given command in :attr:`modified_vars`. Parameters ---------- command: str The command to set the modified state to ``True`` """ tkvar = self.modified_vars.get(command, None) if tkvar is None: logger.debug("No tkvar for command: '%s'", command) return tkvar.set(True) logger.debug("Set modified var to True for: '%s'", command) def refresh_config(self) -> None: """ Reload the user config from file. """ self._user_config = UserConfig(None) def set_cursor_busy(self, widget: Optional[tk.Widget] = None) -> None: """ Set the root or widget cursor to busy. Parameters ---------- widget: tkinter object, optional The widget to set busy cursor for. If the provided value is ``None`` then sets the cursor busy for the whole of the GUI. Default: ``None``. """ logger.debug("Setting cursor to busy. widget: %s", widget) component = self.root if widget is None else widget component.config(cursor="watch") # type: ignore component.update_idletasks() def set_cursor_default(self, widget: Optional[tk.Widget] = None) -> None: """ Set the root or widget cursor to default. Parameters ---------- widget: tkinter object, optional The widget to set default cursor for. If the provided value is ``None`` then sets the cursor busy for the whole of the GUI. Default: ``None`` """ logger.debug("Setting cursor to default. widget: %s", widget) component = self.root if widget is None else widget component.config(cursor="") # type: ignore component.update_idletasks() def set_root_title(self, text: Optional[str] = None) -> None: """ Set the main title text for Faceswap. The title will always begin with 'Faceswap.py'. Additional text can be appended. Parameters ---------- text: str, optional Additional text to be appended to the GUI title bar. Default: ``None`` """ title = "Faceswap.py" title += f" - {text}" if text is not None and text else "" self.root.title(title) def set_geometry(self, width: int, height: int, fullscreen: bool = False) -> None: """ Set the geometry for the root tkinter object. Parameters ---------- width: int The width to set the window to (prior to scaling) height: int The height to set the window to (prior to scaling) fullscreen: bool, optional Whether to set the window to full-screen mode. If ``True`` then :attr:`width` and :attr:`height` are ignored. Default: ``False`` """ self.root.tk.call("tk", "scaling", self.scaling_factor) if fullscreen: initial_dimensions = (self.root.winfo_screenwidth(), self.root.winfo_screenheight()) else: initial_dimensions = (round(width * self.scaling_factor), round(height * self.scaling_factor)) if fullscreen and sys.platform in ("win32", "darwin"): self.root.state('zoomed') elif fullscreen: self.root.attributes('-zoomed', True) else: self.root.geometry(f"{str(initial_dimensions[0])}x{str(initial_dimensions[1])}+80+80") logger.debug("Geometry: %sx%s", *initial_dimensions)