mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
580 lines
24 KiB
Python
580 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
""" functions for implementing themes in Faceswap's GUI """
|
|
import logging
|
|
import os
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
import numpy as np
|
|
|
|
from lib.serializer import get_serializer
|
|
from lib.utils import FaceswapError
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Style(): # pylint:disable=too-few-public-methods
|
|
""" Set the overarching theme and customize widgets.
|
|
|
|
Parameters
|
|
----------
|
|
default_font: tuple
|
|
The name and size of the default font
|
|
root: :class:`tkinter.Tk`
|
|
The root tkinter object
|
|
path_cache: str
|
|
The path to the GUI's cache
|
|
"""
|
|
def __init__(self, default_font, root, path_cache):
|
|
self._root = root
|
|
self._font = default_font
|
|
default = os.path.join(path_cache, "themes", "default.json")
|
|
self._user_theme = get_serializer("json").load(default)
|
|
self._style = ttk.Style()
|
|
self._widgets = _Widgets(self._style)
|
|
self._set_styles()
|
|
|
|
@property
|
|
def user_theme(self):
|
|
""" dict: The currently selected user theme. """
|
|
return self._user_theme
|
|
|
|
def _set_styles(self):
|
|
""" Configure widget theme and styles """
|
|
self._config_settings_group()
|
|
# Command page
|
|
theme = self._user_theme["command_tabs"]
|
|
self._widgets.notebook("CPanel",
|
|
theme["frame_border"],
|
|
theme["tab_color"],
|
|
theme["tab_selected"],
|
|
theme["tab_hover"])
|
|
|
|
# Settings Popup
|
|
self._style.configure("SPanel.Header1.TLabel",
|
|
font=(self._font[0], self._font[1] + 4, "bold"))
|
|
self._style.configure("SPanel.Header2.TLabel",
|
|
font=(self._font[0], self._font[1] + 2, "bold"))
|
|
# Console
|
|
theme = self._user_theme["console"]
|
|
console_sbar = tuple(tuple(theme[f"scrollbar_{area}_{state}"]
|
|
for state in ("normal", "disabled", "active"))
|
|
for area in ("background", "foreground", "border"))
|
|
self._widgets.scrollbar("Console",
|
|
theme["scrollbar_trough"],
|
|
theme["scrollbar_border"],
|
|
*console_sbar)
|
|
self._widgets.frame("Console",
|
|
theme["background_color"],
|
|
theme["border_color"],
|
|
borderwidth=1)
|
|
|
|
def _config_settings_group(self):
|
|
""" Configures the style of the control panel entry boxes. Used for inputting Faceswap
|
|
options or controlling plugin settings. """
|
|
theme = self._user_theme["group_panel"]
|
|
for panel_type in ("CPanel", "SPanel"):
|
|
if panel_type == "SPanel": # Merge in Settings Panel overrides
|
|
theme = {**theme, **self._user_theme["group_settings"]}
|
|
self._style.configure(f"{panel_type}.Holder.TFrame",
|
|
background=theme["panel_background"])
|
|
# Header Colors on option/group controls
|
|
self._style.configure(f"{panel_type}.Group.TLabelframe.Label",
|
|
foreground=theme["header_color"])
|
|
self._style.configure(f"{panel_type}.Groupheader.TLabel",
|
|
background=theme["header_color"],
|
|
foreground=theme["header_font"],
|
|
font=(self._font[0], self._font[1], "bold"))
|
|
# Widgets and specific areas
|
|
self._group_panel_widgets(panel_type, theme)
|
|
self._group_panel_infoheader(panel_type, theme)
|
|
self._widgets.slider(panel_type,
|
|
theme["control_color"],
|
|
theme["control_active"],
|
|
self._user_theme["group_panel"]["group_background"])
|
|
backgrounds = (theme["control_color"],
|
|
theme["control_disabled"],
|
|
theme["control_active"])
|
|
foregrounds = (theme["control_disabled"],
|
|
theme["control_color"],
|
|
theme["control_disabled"])
|
|
borders = (theme["header_color"], theme["control_color"], theme["header_color"])
|
|
self._widgets.scrollbar(panel_type,
|
|
theme["scrollbar_trough"],
|
|
theme["scrollbar_border"],
|
|
backgrounds,
|
|
foregrounds,
|
|
borders)
|
|
self._widgets.combobox(panel_type,
|
|
theme["control_color"],
|
|
theme["control_active"],
|
|
theme["control_disabled"],
|
|
theme["header_color"],
|
|
theme["group_background"],
|
|
theme["group_font"])
|
|
|
|
def _group_panel_infoheader(self, key, theme):
|
|
""" Set the theme for the information header box that appears at the top of each group
|
|
panel
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the slider will belong to
|
|
theme: dict
|
|
The user configuration theme options
|
|
"""
|
|
self._widgets.frame(f"{key}.InfoHeader",
|
|
theme["info_color"],
|
|
theme["info_border"],
|
|
borderwidth=1)
|
|
|
|
self._style.configure(f"{key}.InfoHeader.TLabel",
|
|
background=theme["info_color"],
|
|
foreground=theme["info_font"],
|
|
font=(self._font[0], self._font[1], "bold"))
|
|
self._style.configure(f"{key}.InfoBody.TLabel",
|
|
background=theme["info_color"],
|
|
foreground=theme["info_font"])
|
|
|
|
def _group_panel_widgets(self, key, theme):
|
|
""" Configure the foreground and background colors of common widgets.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the slider will belong to
|
|
theme: dict
|
|
The user configuration theme options
|
|
"""
|
|
# Put a border on a group's sub-frame
|
|
self._widgets.frame(f"{key}.Subframe.Group",
|
|
theme["group_background"],
|
|
theme["group_border"],
|
|
borderwidth=1)
|
|
|
|
# Background and Foreground of widgets and labels
|
|
for lbl in ["TLabel", "TFrame", "TLabelframe", "TCheckbutton", "TRadiobutton",
|
|
"TLabelframe.Label"]:
|
|
self._style.configure(f"{key}.Group.{lbl}",
|
|
background=theme["group_background"],
|
|
foreground=theme["group_font"])
|
|
|
|
|
|
class _Widgets():
|
|
""" Create custom ttk widget layouts for themed widgets.
|
|
|
|
Parameters
|
|
----------
|
|
style: :class:`ttk.Style`
|
|
The master style object
|
|
"""
|
|
def __init__(self, style):
|
|
self._images = _TkImage()
|
|
self._style = style
|
|
|
|
def combobox(self, key, control_color, active_color, arrow_color, control_border, field_color,
|
|
field_border):
|
|
""" Combo-boxes are fairly complex to style.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the slider will belong to
|
|
control_color: str
|
|
The color of inactive combo pull down button
|
|
active_color: str
|
|
The color of combo pull down button when it is hovered or pressed
|
|
arrow_color: str
|
|
The color of the combo pull down arrow
|
|
control_border: str
|
|
The color of the combo pull down button border
|
|
field_color: str
|
|
The color of the input field's background
|
|
field_border: str
|
|
The color of the input field's border
|
|
"""
|
|
# All the stock down arrow images are bad
|
|
images = {}
|
|
for state in ("active", "normal"):
|
|
images[f"arrow_{state}"] = self._images.get_image(
|
|
(20, 20),
|
|
control_color if state == "normal" else active_color,
|
|
foreground=arrow_color,
|
|
pattern="arrow",
|
|
thickness=2,
|
|
border_width=1,
|
|
border_color=control_border)
|
|
|
|
self._style.element_create(f"{key}.Combobox.downarrow",
|
|
"image",
|
|
images["arrow_normal"],
|
|
("active", images["arrow_active"]),
|
|
("pressed", images["arrow_active"]),
|
|
sticky="e",
|
|
width=20)
|
|
|
|
# None of the themes give us the border control we need, so create an image
|
|
box = self._images.get_image((16, 16),
|
|
field_color,
|
|
border_width=1,
|
|
border_color=field_border)
|
|
self._style.element_create(f"{key}.Combobox.field",
|
|
"image",
|
|
box,
|
|
border=1,
|
|
padding=(6, 0, 0, 0))
|
|
|
|
# Set a layout so we can access required params
|
|
self._style.layout(f"{key}.TCombobox", [
|
|
(f"{key}.Combobox.field", {
|
|
"children": [
|
|
(f"{key}.Combobox.downarrow", {"side": "right", "sticky": "ns"}),
|
|
(f"{key}.Combobox.padding", {
|
|
"expand": "1",
|
|
"sticky": "nswe",
|
|
"children": [(f"{key}.Combobox.focus", {
|
|
"expand": "1",
|
|
"sticky": "nswe",
|
|
"children": [(f"{key}.Combobox.textarea", {"sticky": "nswe"})]})]})],
|
|
"sticky": "nswe"})])
|
|
|
|
def frame(self, key, background, border, borderwidth=1):
|
|
""" Create a custom frame widget for controlling background and border colors.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the Frame will belong to
|
|
background: str
|
|
The hex code for the background of the frame
|
|
border: str
|
|
The hex code for the border of the frame
|
|
"""
|
|
self._style.element_create(f"{key}.Frame.border", "from", "alt")
|
|
self._style.layout(f"{key}.TFrame",
|
|
[(f"{key}.Frame.border", {"sticky": "nswe"})])
|
|
self._style.configure(f"{key}.TFrame",
|
|
background=background,
|
|
relief=tk.SOLID,
|
|
borderwidth=borderwidth,
|
|
bordercolor=border)
|
|
|
|
def notebook(self, key, frame_border, tab_color, tab_selected, tab_hover):
|
|
""" Create a custom notebook widget so we can control the colors.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the scrollbar will belong to
|
|
frame_border: str
|
|
The border color around the tab's contents
|
|
tab_color: str
|
|
The color of non selected tabs
|
|
tab_selected: str
|
|
The color of selected tabs
|
|
tab_hover: str
|
|
The color of hovered tabs
|
|
"""
|
|
# TODO This lags out the GUI, so need to test where this is failing prior to implementing
|
|
client = self._images.get_image((8, 8), frame_border)
|
|
self._style.element_create(f"{key}.Notebook.client", "image", client, border=1)
|
|
|
|
tabs = [self._images.get_image((8, 8), color)
|
|
for color in (tab_color, tab_selected, tab_hover)]
|
|
|
|
self._style.element_create(f"{key}.Notebook.tab",
|
|
"image",
|
|
tabs[0],
|
|
("selected", tabs[1]),
|
|
("active", tabs[2]),
|
|
padding=(0, 2, 0, 0),
|
|
border=3)
|
|
|
|
self._style.layout(f"{key}.TNotebook", [(f"{key}.Notebook.client", {"sticky": "nswe"})])
|
|
self._style.layout(f"{key}.TNotebook.Tab", [
|
|
(f"{key}.Notebook.tab", {
|
|
"sticky": "nswe",
|
|
"children": [
|
|
("Notebook.padding", {
|
|
"side": "top",
|
|
"sticky": "nswe",
|
|
"children": [
|
|
("Notebook.focus", {
|
|
"side": "top",
|
|
"sticky": "nswe",
|
|
"children": [("Notebook.label", {"side": "top", "sticky": ""})]
|
|
})]
|
|
})]
|
|
})])
|
|
|
|
self._style.configure(f"{key}.TNotebook", tabmargins=(0, 2, 0, 0))
|
|
self._style.configure(f"{key}.TNotebook.Tab", padding=(6, 2, 6, 2), expand=(0, 0, 2))
|
|
self._style.configure(f"{key}.TNotebook.Tab", expand=("selected", (1, 2, 4, 2)))
|
|
|
|
def scrollbar(self, key, trough_color, border_color, control_backgrounds, control_foregrounds,
|
|
control_borders):
|
|
""" Create a custom scroll bar widget so we can control the colors.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the scrollbar will belong to
|
|
theme: dict
|
|
The theme options for a scroll bar. The dict should contain the keys: `background`,
|
|
`foreground`, `border`, with each item containing a tuple of the colors for the states
|
|
`normal`, `disabled` and `active` respectively
|
|
trough_color: str
|
|
The hex code for the scrollbar trough color
|
|
border_color: str
|
|
The hex code for the scrollbar border color
|
|
control_backgrounds: tuple
|
|
Tuple of length 3 for the button and slider colors for the states `normal`,
|
|
`disabled`, `active`
|
|
control_foregrounds: tuple
|
|
Tuple of length 3 for the button arrow colors for the states `normal`,
|
|
`disabled`, `active`
|
|
control_borders: tuple
|
|
Tuple of length 3 for the borders of the buttons and slider for the states `normal`,
|
|
`disabled`, `active`
|
|
"""
|
|
logger.debug("Creating scrollbar: (key: %s, trough_color: %s, border_color: %s, "
|
|
"control_backgrounds: %s, control_foregrounds: %s, control_borders: %s)",
|
|
key, trough_color, border_color, control_backgrounds, control_foregrounds,
|
|
control_borders)
|
|
images = {}
|
|
for idx, state in enumerate(("normal", "disabled", "active")):
|
|
# Create arrow and slider widgets for each state
|
|
img_args = ((16, 16), control_backgrounds[idx])
|
|
for dir_ in ("up", "down"):
|
|
images[f"img_{dir_}_{state}"] = self._images.get_image(
|
|
*img_args,
|
|
foreground=control_foregrounds[idx],
|
|
pattern="arrow",
|
|
direction=dir_,
|
|
thickness=4,
|
|
border_width=1,
|
|
border_color=control_borders[idx])
|
|
images[f"img_thumb_{state}"] = self._images.get_image(
|
|
*img_args,
|
|
border_width=1,
|
|
border_color=control_borders[idx])
|
|
|
|
for element in ("thumb", "uparrow", "downarrow"):
|
|
# Create the elements with the new images
|
|
lookup = element.replace("arrow", "")
|
|
args = (f"{key}.Vertical.Scrollbar.{element}",
|
|
"image",
|
|
images[f"img_{lookup}_normal"],
|
|
("disabled", images[f"img_{lookup}_disabled"]),
|
|
("pressed !disabled", images[f"img_{lookup}_active"]),
|
|
("active !disabled", images[f"img_{lookup}_active"]))
|
|
kwargs = dict(border=1, sticky="ns") if element == "thumb" else {}
|
|
self._style.element_create(*args, **kwargs)
|
|
|
|
# Get a configurable trough
|
|
self._style.element_create(f"{key}.Vertical.Scrollbar.trough", "from", "clam")
|
|
|
|
self._style.layout(
|
|
f"{key}.Vertical.TScrollbar",
|
|
[(f"{key}.Vertical.Scrollbar.trough", {
|
|
"sticky": "ns",
|
|
"children": [
|
|
(f"{key}.Vertical.Scrollbar.uparrow", {"side": "top", "sticky": ""}),
|
|
(f"{key}.Vertical.Scrollbar.downarrow", {"side": "bottom", "sticky": ""}),
|
|
(f"{key}.Vertical.Scrollbar.thumb", {"expand": "1", "sticky": "nswe"})
|
|
]
|
|
})])
|
|
self._style.configure(f"{key}.Vertical.TScrollbar",
|
|
troughcolor=trough_color,
|
|
bordercolor=border_color,
|
|
troughrelief=tk.SOLID,
|
|
troughborderwidth=1)
|
|
|
|
def slider(self, key, control_color, active_color, trough_color):
|
|
""" Take a copy of the default ttk.Scale widget and replace the slider element with a
|
|
version we can control the color and shape of.
|
|
|
|
Parameters
|
|
----------
|
|
key: str
|
|
The section that the slider will belong to
|
|
control_color: str
|
|
The color of inactive slider and up down buttons
|
|
active_color: str
|
|
The color of slider and up down buttons when they are hovered or pressed
|
|
trough_color: str
|
|
The color of the scroll bar's trough
|
|
"""
|
|
img_slider = self._images.get_image((10, 25), control_color)
|
|
img_slider_alt = self._images.get_image((10, 25), active_color)
|
|
|
|
self._style.element_create(f"{key}.Horizontal.Scale.trough", "from", "alt")
|
|
self._style.element_create(f"{key}.Horizontal.Scale.slider",
|
|
"image",
|
|
img_slider,
|
|
("active", img_slider_alt))
|
|
|
|
self._style.layout(
|
|
f"{key}.Horizontal.TScale",
|
|
[(f"{key}.Scale.focus", {
|
|
"expand": "1",
|
|
"sticky": "nswe",
|
|
"children": [
|
|
(f"{key}.Horizontal.Scale.trough", {
|
|
"expand": "1",
|
|
"sticky": "nswe",
|
|
"children": [
|
|
(f"{key}.Horizontal.Scale.track", {"sticky": "we"}),
|
|
(f"{key}.Horizontal.Scale.slider", {"side": "left", "sticky": ""})
|
|
]
|
|
})
|
|
]
|
|
})])
|
|
|
|
self._style.configure(f"{key}.Horizontal.TScale",
|
|
background=trough_color,
|
|
groovewidth=4,
|
|
troughcolor=trough_color)
|
|
|
|
|
|
class _TkImage(): # pylint:disable=too-few-public-methods
|
|
""" Create a tk image for a given pattern and shape.
|
|
"""
|
|
def __init__(self):
|
|
self._cache = [] # We need to keep a reference to every image created
|
|
|
|
# Numpy array patterns
|
|
@classmethod
|
|
def _get_solid(cls, dimensions):
|
|
""" Return a solid background color pattern.
|
|
|
|
Parameters
|
|
----------
|
|
dimensions: tuple
|
|
The (`width`, `height`) of the desired tk image
|
|
|
|
Returns
|
|
-------
|
|
:class:`numpy.ndarray`
|
|
A 2D, UINT8 array of shape (height, width) of all zeros
|
|
"""
|
|
return np.zeros((dimensions[1], dimensions[0]), dtype="uint8")
|
|
|
|
@classmethod
|
|
def _get_arrow(cls, dimensions, thickness, direction):
|
|
""" Return a background color with a "v" arrow in foreground color
|
|
|
|
Parameters
|
|
----------
|
|
dimensions: tuple
|
|
The (`width`, `height`) of the desired tk image
|
|
thickness: int
|
|
The thickness of the pattern to be drawn
|
|
direction: ["left", "up", "right", "down"]
|
|
The direction that the pattern should be facing
|
|
|
|
Returns
|
|
-------
|
|
:class:`numpy.ndarray`
|
|
A 2D, UINT8 array of shape (height, width) of all zeros
|
|
"""
|
|
square_size = min(dimensions[1], dimensions[0])
|
|
if square_size < 16 or any(dim % 2 != 0 for dim in dimensions):
|
|
raise FaceswapError("For arrow image, the minimum size across any axis must be 8 and "
|
|
"dimensions must all be divisible by 2")
|
|
crop_size = (square_size // 16) * 16
|
|
draw_rows = int(6 * crop_size / 16)
|
|
start_row = dimensions[1] // 2 - draw_rows // 2
|
|
initial_indent = (2 * (crop_size // 16) + (dimensions[0] - crop_size) // 2)
|
|
|
|
retval = np.zeros((dimensions[1], dimensions[0]), dtype="uint8")
|
|
for i in range(start_row, start_row + draw_rows):
|
|
indent = initial_indent + i - start_row
|
|
join = (min(indent + thickness, dimensions[0] // 2),
|
|
max(dimensions[0] - indent - thickness, dimensions[0] // 2))
|
|
retval[i, np.r_[indent:join[0], join[1]:dimensions[0] - indent]] = 1
|
|
if direction in ("right", "left"):
|
|
retval = np.rot90(retval)
|
|
if direction in ("up", "left"):
|
|
retval = np.flip(retval)
|
|
return retval
|
|
|
|
def get_image(self,
|
|
dimensions,
|
|
background,
|
|
foreground=None,
|
|
pattern="solid",
|
|
border_width=0,
|
|
border_color=None,
|
|
thickness=2,
|
|
direction="down"):
|
|
""" Obtain a tk image.
|
|
|
|
Generates the requested image and stores in cache.
|
|
|
|
Parameters
|
|
----------
|
|
dimensions: tuple
|
|
The (`width`, `height`) of the desired tk image
|
|
background: str
|
|
The hex code for the background (main) color
|
|
foreground: str, optional
|
|
The hex code for the background (secondary) color. If ``None`` is provided then a
|
|
solid background color image will be returned. Default: ``None``
|
|
pattern: ["solid", "arrow"], optional
|
|
The pattern to generate for the tk image. Default: `"solid"`
|
|
border_width: int, optional
|
|
The thickness of foreground border to apply. Default: 0
|
|
border_color: int, optional
|
|
The color of the border, if one is to be created. Default: ``None`` (use foreground
|
|
color)
|
|
thickness: int, optional
|
|
The thickness of the pattern to be drawn. Default: `2`
|
|
direction: ["left", "up", "right", "down"], optional
|
|
The direction that the pattern should be facing. Default: `"down"`
|
|
"""
|
|
foreground = foreground if foreground else background
|
|
border_color = border_color if border_color else foreground
|
|
|
|
args = [dimensions]
|
|
if pattern.lower() == "arrow":
|
|
args.extend([thickness, direction])
|
|
if pattern.lower() == "border":
|
|
args.extend([thickness])
|
|
pattern = getattr(self, f"_get_{pattern.lower()}")(*args)
|
|
|
|
if border_width > 0:
|
|
border = np.ones_like(pattern) + 1
|
|
border[border_width:-border_width,
|
|
border_width:-border_width] = pattern[border_width:-border_width,
|
|
border_width:-border_width]
|
|
pattern = border
|
|
|
|
return self._create_photoimage(background, foreground, border_color, pattern)
|
|
|
|
def _create_photoimage(self, background, foreground, border, pattern):
|
|
""" Create a tkinter PhotoImage and populate it with the requested color pattern.
|
|
|
|
Parameters
|
|
----------
|
|
background: str
|
|
The hex code for the background (main) color
|
|
foreground: str
|
|
The hex code for the foreground (secondary) color
|
|
border: str
|
|
The hex code for the border color
|
|
pattern: class:`numpy.ndarray`
|
|
The pattern for the final image with background colors marked as 0 and foreground
|
|
colors marked as 1
|
|
"""
|
|
image = tk.PhotoImage(width=pattern.shape[1], height=pattern.shape[0])
|
|
self._cache.append(image)
|
|
|
|
pixels = "} {".join(" ".join(foreground
|
|
if pxl == 1 else border if pxl == 2 else background
|
|
for pxl in row)
|
|
for row in pattern)
|
|
image.put("{" + pixels + "}")
|
|
return image
|