mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
* 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
1372 lines
59 KiB
Python
1372 lines
59 KiB
Python
#!/usr/bin/env python3
|
|
""" Helper functions and classes for GUI controls """
|
|
import gettext
|
|
import logging
|
|
import re
|
|
|
|
import tkinter as tk
|
|
import typing as T
|
|
from tkinter import colorchooser, ttk
|
|
from itertools import zip_longest
|
|
from functools import partial
|
|
|
|
from _tkinter import Tcl_Obj, TclError
|
|
|
|
from .custom_widgets import ContextMenu, MultiOption, ToggledFrame, Tooltip
|
|
from .utils import FileHandler, get_config, get_images
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
# LOCALES
|
|
_LANG = gettext.translation("gui.tooltips", localedir="locales", fallback=True)
|
|
_ = _LANG.gettext
|
|
|
|
# We store Tooltips, ContextMenus and Commands globally when they are created
|
|
# Because we need to add them back to newly cloned widgets (they are not easily accessible from
|
|
# original config or are prone to getting destroyed when the original widget is destroyed)
|
|
_RECREATE_OBJECTS: dict[str, dict[str, T.Any]] = {"tooltips": {},
|
|
"commands": {},
|
|
"contextmenus": {}}
|
|
|
|
|
|
def _get_tooltip(widget, text=None, text_variable=None):
|
|
""" Store the tooltip layout and widget id in _TOOLTIPS and return a tooltip.
|
|
|
|
Auto adjust tooltip width based on amount of text.
|
|
|
|
"""
|
|
_RECREATE_OBJECTS["tooltips"][str(widget)] = {"text": text,
|
|
"text_variable": text_variable}
|
|
logger.debug("Adding to tooltips dict: (widget: %s. text: '%s')", widget, text)
|
|
|
|
wrap_length = 400
|
|
if text is not None:
|
|
while True:
|
|
if len(text) < wrap_length * 5:
|
|
break
|
|
if wrap_length > 800:
|
|
break
|
|
wrap_length = int(wrap_length * 1.10)
|
|
|
|
return Tooltip(widget, text=text, text_variable=text_variable, wrap_length=wrap_length)
|
|
|
|
|
|
def _get_contextmenu(widget):
|
|
""" Create a context menu, store its mapping and return """
|
|
rc_menu = ContextMenu(widget)
|
|
_RECREATE_OBJECTS["contextmenus"][str(widget)] = rc_menu
|
|
logger.debug("Adding to Context menu: (widget: %s. rc_menu: %s)",
|
|
widget, rc_menu)
|
|
return rc_menu
|
|
|
|
|
|
def _add_command(name, func):
|
|
""" For controls that execute commands, the command must be added to the _COMMAND list so that
|
|
it can be added back to the widget during cloning """
|
|
logger.debug("Adding to commands: %s - %s", name, func)
|
|
_RECREATE_OBJECTS["commands"][str(name)] = func
|
|
|
|
|
|
def set_slider_rounding(value, var, d_type, round_to, min_max):
|
|
""" Set the value of sliders underlying variable based on their datatype,
|
|
rounding value and min/max.
|
|
|
|
Parameters
|
|
----------
|
|
var: tkinter.Var
|
|
The variable to set the value for
|
|
d_type: [:class:`int`, :class:`float`]
|
|
The type of value that is stored in :attr:`var`
|
|
round_to: int or list
|
|
If :attr:`d_type` is :class:`float` then this is the decimal place rounding for
|
|
:attr:`var`. If :attr:`d_type` is :class:`int` then this is the number of steps between
|
|
each increment for :attr:`var`. If a list is provided, then this must be a list of
|
|
discreet values that are of the correct :attr:`d_type`.
|
|
min_max: tuple (`int`, `int`)
|
|
The (``min``, ``max``) values that this slider accepts
|
|
"""
|
|
if isinstance(round_to, list):
|
|
# Lock to nearest item
|
|
var.set(min(round_to, key=lambda x: abs(x-float(value))))
|
|
elif d_type == float:
|
|
var.set(round(float(value), round_to))
|
|
else:
|
|
steps = range(min_max[0], min_max[1] + round_to, round_to)
|
|
value = min(steps, key=lambda x: abs(x - int(float(value))))
|
|
var.set(value)
|
|
|
|
|
|
class ControlPanelOption():
|
|
"""
|
|
A class to hold a control panel option. A list of these is expected
|
|
to be passed to the ControlPanel object.
|
|
|
|
Parameters
|
|
----------
|
|
title: str
|
|
Title of the control. Will be used for label text and control naming
|
|
dtype: datatype object
|
|
Datatype of the control.
|
|
group: str, optional
|
|
The group that this control should sit with. If provided, all controls in the same
|
|
group will be placed together. Default: None
|
|
subgroup: str, optional
|
|
The subgroup that this option belongs to. If provided, will group options in the same
|
|
subgroups together for the same layout as option/check boxes. Default: ``None``
|
|
default: str, optional
|
|
Default value for the control. If None is provided, then action will be dictated by
|
|
whether "blank_nones" is set in ControlPanel
|
|
initial_value: str, optional
|
|
Initial value for the control. If None, default will be used
|
|
choices: list or tuple, object
|
|
Used for combo boxes and radio control option setting. Set to `"colorchooser"` for a color
|
|
selection dialog.
|
|
is_radio: bool, optional
|
|
Specifies to use a Radio control instead of combobox if choices are passed
|
|
is_multi_option:
|
|
Specifies to use a Multi Check Button option group for the specified control
|
|
rounding: int or float, optional
|
|
For slider controls. Sets the stepping
|
|
min_max: int or float, optional
|
|
For slider controls. Sets the min and max values
|
|
sysbrowser: dict, optional
|
|
Adds Filesystem browser buttons to ttk.Entry options.
|
|
Expects a dict: {sysbrowser: str, filetypes: str}
|
|
helptext: str, optional
|
|
Sets the tooltip text
|
|
track_modified: bool, optional
|
|
Set whether to set a callback trace indicating that the parameter has been modified.
|
|
Default: False
|
|
command: str, optional
|
|
Required if tracking modified. The command that this option belongs to. Default: None
|
|
"""
|
|
|
|
def __init__(self, title, dtype, # pylint:disable=too-many-arguments
|
|
group=None, subgroup=None, default=None, initial_value=None, choices=None,
|
|
is_radio=False, is_multi_option=False, rounding=None, min_max=None,
|
|
sysbrowser=None, helptext=None, track_modified=False, command=None):
|
|
logger.debug("Initializing %s: (title: '%s', dtype: %s, group: %s, subgroup: %s, "
|
|
"default: %s, initial_value: %s, choices: %s, is_radio: %s, "
|
|
"is_multi_option: %s, rounding: %s, min_max: %s, sysbrowser: %s, "
|
|
"helptext: '%s', track_modified: %s, command: '%s')", self.__class__.__name__,
|
|
title, dtype, group, subgroup, default, initial_value, choices, is_radio,
|
|
is_multi_option, rounding, min_max, sysbrowser, helptext, track_modified,
|
|
command)
|
|
|
|
self.dtype = dtype
|
|
self.sysbrowser = sysbrowser
|
|
self._command = command
|
|
self._options = {"title": title,
|
|
"subgroup": subgroup,
|
|
"group": group,
|
|
"default": default,
|
|
"initial_value": initial_value,
|
|
"choices": choices,
|
|
"is_radio": is_radio,
|
|
"is_multi_option": is_multi_option,
|
|
"rounding": rounding,
|
|
"min_max": min_max,
|
|
"helptext": helptext}
|
|
self.control = self.get_control()
|
|
self.tk_var = self.get_tk_var(initial_value, track_modified)
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
@property
|
|
def name(self):
|
|
""" Lowered title for naming """
|
|
return self._options["title"].lower()
|
|
|
|
@property
|
|
def title(self):
|
|
""" Title case title for naming with underscores removed """
|
|
return self._options["title"].replace("_", " ").title()
|
|
|
|
@property
|
|
def group(self):
|
|
""" Return group or _master if no group set """
|
|
group = self._options["group"]
|
|
group = "_master" if group is None else group
|
|
return group
|
|
|
|
@property
|
|
def subgroup(self):
|
|
""" str: The subgroup for the option, or ``None`` if none provided. """
|
|
return self._options["subgroup"]
|
|
|
|
@property
|
|
def default(self):
|
|
""" Return either selected value or default """
|
|
return self._options["default"]
|
|
|
|
@property
|
|
def value(self):
|
|
""" Return either initial value or default """
|
|
val = self._options["initial_value"]
|
|
val = self.default if val is None else val
|
|
return val
|
|
|
|
@property
|
|
def choices(self):
|
|
""" Return choices """
|
|
return self._options["choices"]
|
|
|
|
@property
|
|
def is_radio(self):
|
|
""" Return is_radio """
|
|
return self._options["is_radio"]
|
|
|
|
@property
|
|
def is_multi_option(self):
|
|
""" bool: ``True`` if the control should be contained in a multi check button group,
|
|
otherwise ``False``. """
|
|
return self._options["is_multi_option"]
|
|
|
|
@property
|
|
def rounding(self):
|
|
""" Return rounding """
|
|
return self._options["rounding"]
|
|
|
|
@property
|
|
def min_max(self):
|
|
""" Return min_max """
|
|
return self._options["min_max"]
|
|
|
|
@property
|
|
def helptext(self):
|
|
""" Format and return help text for tooltips """
|
|
helptext = self._options["helptext"]
|
|
if helptext is None:
|
|
return helptext
|
|
logger.debug("Format control help: '%s'", self.name)
|
|
if helptext.startswith("R|"):
|
|
helptext = helptext[2:].replace("\nL|", "\n - ").replace("\n", "\n\n")
|
|
else:
|
|
helptext = helptext.replace("\n\t", "\n - ").replace("%%", "%")
|
|
helptext = self.title + " - " + helptext
|
|
logger.debug("Formatted control help: (name: '%s', help: '%s'", self.name, helptext)
|
|
return helptext
|
|
|
|
def get(self):
|
|
""" Return the value from the tk_var
|
|
|
|
Notes
|
|
-----
|
|
tk variables don't like empty values if it's not a stringVar. This seems to be pretty
|
|
much the only reason that a get() call would fail, so replace any numerical variable
|
|
with it's numerical zero equivalent on a TCL Error. Only impacts variables linked
|
|
to Entry widgets.
|
|
"""
|
|
try:
|
|
val = self.tk_var.get()
|
|
except TclError:
|
|
if isinstance(self.tk_var, tk.IntVar):
|
|
val = 0
|
|
elif isinstance(self.tk_var, tk.DoubleVar):
|
|
val = 0.0
|
|
else:
|
|
raise
|
|
return val
|
|
|
|
def set(self, value):
|
|
""" Set the tk_var to a new value """
|
|
self.tk_var.set(value)
|
|
|
|
def set_initial_value(self, value):
|
|
""" Set the initial_value to the given value
|
|
|
|
Parameters
|
|
----------
|
|
value: varies
|
|
The value to set the initial value attribute to
|
|
"""
|
|
logger.debug("Setting inital value for %s to %s", self.name, value)
|
|
self._options["initial_value"] = value
|
|
|
|
def get_control(self):
|
|
""" Set the correct control type based on the datatype or for this option """
|
|
if self.choices and self.is_radio:
|
|
control = "radio"
|
|
elif self.choices and self.is_multi_option:
|
|
control = "multi"
|
|
elif self.choices and self.choices == "colorchooser":
|
|
control = "colorchooser"
|
|
elif self.choices:
|
|
control = ttk.Combobox
|
|
elif self.dtype == bool:
|
|
control = ttk.Checkbutton
|
|
elif self.dtype in (int, float):
|
|
control = "scale"
|
|
else:
|
|
control = tk.Entry
|
|
logger.debug("Setting control '%s' to %s", self.title, control)
|
|
return control
|
|
|
|
def get_tk_var(self, initial_value, track_modified):
|
|
""" Correct variable type for control """
|
|
if self.dtype == bool:
|
|
var = tk.BooleanVar()
|
|
elif self.dtype == int:
|
|
var = tk.IntVar()
|
|
elif self.dtype == float:
|
|
var = tk.DoubleVar()
|
|
else:
|
|
var = tk.StringVar()
|
|
if initial_value is not None:
|
|
var.set(initial_value)
|
|
logger.debug("Setting tk variable: (name: '%s', dtype: %s, tk_var: %s, initial_value: %s)",
|
|
self.name, self.dtype, var, initial_value)
|
|
if track_modified and self._command is not None:
|
|
logger.debug("Tracking variable modification: %s", self.name)
|
|
var.trace("w",
|
|
lambda name, index, mode, cmd=self._command: self._modified_callback(cmd))
|
|
|
|
if track_modified and self._command == "train" and self.title == "Model Dir":
|
|
var.trace("w", lambda name, index, mode, v=var: self._model_callback(v))
|
|
|
|
return var
|
|
|
|
@staticmethod
|
|
def _modified_callback(command):
|
|
""" Set the modified variable for this tab to TRUE
|
|
|
|
On initial setup the notebook won't yet exist, and we don't want to track the changes
|
|
for initial variables anyway, so make sure notebook exists prior to performing the callback
|
|
"""
|
|
config = get_config()
|
|
if config.command_notebook is None:
|
|
return
|
|
config.set_modified_true(command)
|
|
|
|
@staticmethod
|
|
def _model_callback(var):
|
|
""" Set a callback to load model stats for existing models when a model
|
|
folder is selected """
|
|
config = get_config()
|
|
if not config.user_config_dict["auto_load_model_stats"]:
|
|
logger.debug("Session updating disabled by user config")
|
|
return
|
|
if config.tk_vars.running_task.get():
|
|
logger.debug("Task running. Not updating session")
|
|
return
|
|
folder = var.get()
|
|
logger.debug("Setting analysis model folder callback: '%s'", folder)
|
|
get_config().tk_vars.analysis_folder.set(folder)
|
|
|
|
|
|
class ControlPanel(ttk.Frame): # pylint:disable=too-many-ancestors
|
|
"""
|
|
A Control Panel to hold control panel options.
|
|
This class handles all of the formatting, placing and TK_Variables
|
|
in a consistent manner.
|
|
|
|
It can also provide dynamic columns for resizing widgets
|
|
|
|
Parameters
|
|
----------
|
|
parent: tkinter object
|
|
Parent widget that should hold this control panel
|
|
options: list of ControlPanelOptions objects
|
|
The list of controls that are to be built into this control panel
|
|
label_width: int, optional
|
|
The width that labels for controls should be set to.
|
|
Defaults to 20
|
|
columns: int, optional
|
|
The initial number of columns to set the layout for. Default: 1
|
|
max_columns: int, optional
|
|
The maximum number of columns that this control panel should be able
|
|
to accommodate. Setting to 1 means that there will only be 1 column
|
|
regardless of how wide the control panel is. Higher numbers will
|
|
dynamically fill extra columns if space permits. Defaults to 4
|
|
option_columns: int, optional
|
|
For check-button and radio-button containers, how many options should
|
|
be displayed on each row. Defaults to 4
|
|
header_text: str, optional
|
|
If provided, will place an information box at the top of the control
|
|
panel with these contents.
|
|
style: str, optional
|
|
The name of the style to use for the control panel. Styles are configured when TkInter
|
|
initializes. The style name is the common prefix prior to the widget name. Default:
|
|
``None`` (use the OS style)
|
|
blank_nones: bool, optional
|
|
How the control panel should handle None values. If set to True then None values will be
|
|
converted to empty strings. Default: False
|
|
scrollbar: bool, optional
|
|
``True`` if a scrollbar should be added to the control panel, otherwise ``False``.
|
|
Default: ``True``
|
|
"""
|
|
|
|
def __init__(self, parent, options, # pylint:disable=too-many-arguments
|
|
label_width=20, columns=1, max_columns=4, option_columns=4, header_text=None,
|
|
style=None, blank_nones=True, scrollbar=True):
|
|
logger.debug("Initializing %s: (parent: '%s', options: %s, label_width: %s, columns: %s, "
|
|
"max_columns: %s, option_columns: %s, header_text: %s, style: %s, "
|
|
"blank_nones: %s, scrollbar: %s)",
|
|
self.__class__.__name__, parent, options, label_width, columns, max_columns,
|
|
option_columns, header_text, style, blank_nones, scrollbar)
|
|
self._style = "" if style is None else f"{style}."
|
|
super().__init__(parent, style=f"{self._style}.Group.TFrame")
|
|
|
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
|
|
self.options = options
|
|
self.controls = []
|
|
self.label_width = label_width
|
|
self.columns = columns
|
|
self.max_columns = max_columns
|
|
self.option_columns = option_columns
|
|
|
|
self.header_text = header_text
|
|
self._theme = get_config().user_theme["group_panel"]
|
|
if self._style.startswith("SPanel"):
|
|
self._theme = {**self._theme, **get_config().user_theme["group_settings"]}
|
|
|
|
self.group_frames = {}
|
|
self._sub_group_frames = {}
|
|
|
|
canvas_kwargs = {"bd": 0, "highlightthickness": 0, "bg": self._theme["panel_background"]}
|
|
|
|
self._canvas = tk.Canvas(self, **canvas_kwargs)
|
|
self._canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
self.mainframe, self.optsframe = self.get_opts_frame()
|
|
self._optscanvas = self._canvas.create_window((0, 0), window=self.mainframe, anchor=tk.NW)
|
|
self.build_panel(blank_nones, scrollbar)
|
|
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
@staticmethod
|
|
def _adjust_wraplength(event):
|
|
""" dynamically adjust the wrap length of a label on event """
|
|
label = event.widget
|
|
label.configure(wraplength=event.width - 1)
|
|
|
|
def get_opts_frame(self):
|
|
""" Return an auto-fill container for the options inside a main frame """
|
|
style = f"{self._style}Holder."
|
|
mainframe = ttk.Frame(self._canvas, style=f"{style}TFrame")
|
|
if self.header_text is not None:
|
|
self.add_info(mainframe)
|
|
optsframe = ttk.Frame(mainframe, name="opts_frame", style=f"{style}TFrame")
|
|
optsframe.pack(expand=True, fill=tk.BOTH)
|
|
holder = AutoFillContainer(optsframe, self.columns, self.max_columns, style=style)
|
|
logger.debug("Opts frames: '%s'", holder)
|
|
return mainframe, holder
|
|
|
|
def add_info(self, frame):
|
|
""" Plugin information """
|
|
info_frame = ttk.Frame(frame, style=f"{self._style}InfoHeader.TFrame")
|
|
info_frame.pack(fill=tk.X, side=tk.TOP, expand=True, padx=10, pady=(10, 0))
|
|
label_frame = ttk.Frame(info_frame, style=f"{self._style}InfoHeader.TFrame")
|
|
label_frame.pack(padx=5, pady=5, fill=tk.X, expand=True)
|
|
for idx, line in enumerate(self.header_text.splitlines()):
|
|
if not line:
|
|
continue
|
|
style = f"{self._style}InfoHeader" if idx == 0 else f"{self._style}InfoBody"
|
|
info = ttk.Label(label_frame, text=line, style=f"{style}.TLabel", anchor=tk.W)
|
|
info.bind("<Configure>", self._adjust_wraplength)
|
|
info.pack(fill=tk.X, padx=0, pady=0, expand=True, side=tk.TOP)
|
|
|
|
def build_panel(self, blank_nones, scrollbar):
|
|
""" Build the options frame for this command """
|
|
logger.debug("Add Config Frame")
|
|
if scrollbar:
|
|
self.add_scrollbar()
|
|
self._canvas.bind("<Configure>", self.resize_frame)
|
|
|
|
for option in self.options:
|
|
group_frame = self.get_group_frame(option.group)
|
|
sub_group_frame = self._get_subgroup_frame(group_frame["frame"], option.subgroup)
|
|
frame = group_frame["frame"] if sub_group_frame is None else sub_group_frame.subframe
|
|
|
|
ctl = ControlBuilder(frame,
|
|
option,
|
|
label_width=self.label_width,
|
|
checkbuttons_frame=group_frame["chkbtns"],
|
|
option_columns=self.option_columns,
|
|
style=self._style,
|
|
blank_nones=blank_nones)
|
|
if group_frame["chkbtns"].items > 0:
|
|
group_frame["chkbtns"].parent.pack(side=tk.BOTTOM, fill=tk.X, anchor=tk.NW)
|
|
|
|
self.controls.append(ctl)
|
|
for control in self.controls:
|
|
filebrowser = control.filebrowser
|
|
if filebrowser is not None:
|
|
filebrowser.set_context_action_option(self.options)
|
|
logger.debug("Added Config Frame")
|
|
|
|
def get_group_frame(self, group):
|
|
""" Return a group frame.
|
|
|
|
If a group frame has already been created for the given group, then it will be returned,
|
|
otherwise it will be created and returned.
|
|
|
|
Parameters
|
|
----------
|
|
group: str
|
|
The name of the group to obtain the group frame for
|
|
|
|
Returns
|
|
-------
|
|
:class:`ttk.Frame` or :class:`ToggledFrame`
|
|
If this is a 'master' group frame then returns a standard frame. If this is any
|
|
other group, then will return the ToggledFrame for that group
|
|
"""
|
|
group = group.lower()
|
|
|
|
if self.group_frames.get(group, None) is None:
|
|
logger.debug("Creating new group frame for: %s", group)
|
|
is_master = group == "_master"
|
|
opts_frame = self.optsframe.subframe
|
|
if is_master:
|
|
group_frame = ttk.Frame(opts_frame, style=f"{self._style}.Group.TFrame")
|
|
retval = group_frame
|
|
else:
|
|
group_frame = ToggledFrame(opts_frame, text=group.title(), theme=self._style)
|
|
retval = group_frame.sub_frame
|
|
|
|
group_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5, anchor=tk.NW)
|
|
|
|
self.group_frames[group] = {"frame": retval,
|
|
"chkbtns": self.checkbuttons_frame(retval)}
|
|
group_frame = self.group_frames[group]
|
|
return group_frame
|
|
|
|
def add_scrollbar(self):
|
|
""" Add a scrollbar to the options frame """
|
|
logger.debug("Add Config Scrollbar")
|
|
scrollbar = ttk.Scrollbar(self,
|
|
command=self._canvas.yview,
|
|
style=f"{self._style}Vertical.TScrollbar")
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self._canvas.config(yscrollcommand=scrollbar.set)
|
|
self.mainframe.bind("<Configure>", self.update_scrollbar)
|
|
logger.debug("Added Config Scrollbar")
|
|
|
|
def update_scrollbar(self, event): # pylint: disable=unused-argument
|
|
""" Update the options frame scrollbar """
|
|
self._canvas.configure(scrollregion=self._canvas.bbox("all"))
|
|
|
|
def resize_frame(self, event):
|
|
""" Resize the options frame to fit the canvas """
|
|
logger.debug("Resize Config Frame")
|
|
canvas_width = event.width
|
|
self._canvas.itemconfig(self._optscanvas, width=canvas_width)
|
|
self.optsframe.rearrange_columns(canvas_width)
|
|
logger.debug("Resized Config Frame")
|
|
|
|
def checkbuttons_frame(self, frame):
|
|
""" Build and format frame for holding the check buttons
|
|
if is_master then check buttons will be placed in a LabelFrame
|
|
otherwise in a standard frame """
|
|
logger.debug("Add Options CheckButtons Frame")
|
|
chk_frame = ttk.Frame(frame, name="chkbuttons", style=f"{self._style}Group.TFrame")
|
|
holder = AutoFillContainer(chk_frame,
|
|
self.option_columns,
|
|
self.option_columns,
|
|
style=f"{self._style}Group.")
|
|
logger.debug("Added Options CheckButtons Frame")
|
|
return holder
|
|
|
|
def _get_subgroup_frame(self, parent, subgroup):
|
|
if subgroup is None:
|
|
return subgroup
|
|
if subgroup not in self._sub_group_frames:
|
|
sub_frame = ttk.Frame(parent, style=f"{self._style}Group.TFrame")
|
|
self._sub_group_frames[subgroup] = AutoFillContainer(sub_frame,
|
|
self.option_columns,
|
|
self.option_columns,
|
|
style=f"{self._style}Group.")
|
|
sub_frame.pack(anchor=tk.W, expand=True, fill=tk.X)
|
|
logger.debug("Added Subgroup Frame: %s", subgroup)
|
|
return self._sub_group_frames[subgroup]
|
|
|
|
|
|
class AutoFillContainer():
|
|
""" A container object that auto-fills columns.
|
|
|
|
Parameters
|
|
----------
|
|
parent: :class:`ttk.Frame`
|
|
The parent widget that holds this container
|
|
initial_columns: int
|
|
The initial number of columns that this container should display
|
|
max_columns: int
|
|
The maximum number of column that this container is permitted to display
|
|
style: str, optional
|
|
The name of the style to use for the control panel. Styles are configured when TkInter
|
|
initializes. The style name is the common prefix prior to the widget name. Default:
|
|
empty string (use the OS style)
|
|
"""
|
|
def __init__(self, parent, initial_columns, max_columns, style=""):
|
|
logger.debug("Initializing: %s: (parent: %s, initial_columns: %s, max_columns: %s)",
|
|
self.__class__.__name__, parent, initial_columns, max_columns)
|
|
self.max_columns = max_columns
|
|
self.columns = initial_columns
|
|
self.parent = parent
|
|
self._style = style
|
|
# self.columns = min(columns, self.max_columns)
|
|
self.single_column_width = self.scale_column_width(288, 9)
|
|
self.max_width = self.max_columns * self.single_column_width
|
|
self._items = 0
|
|
self._idx = 0
|
|
self._widget_config = [] # Master list of all children in order
|
|
self.subframes = self.set_subframes()
|
|
logger.debug("Initialized: %s", self.__class__.__name__)
|
|
|
|
@staticmethod
|
|
def scale_column_width(original_size, original_fontsize):
|
|
""" Scale the column width based on selected font size """
|
|
font_size = get_config().user_config_dict["font_size"]
|
|
if font_size == original_fontsize:
|
|
return original_size
|
|
scale = 1 + (((font_size / original_fontsize) - 1) / 2)
|
|
retval = round(original_size * scale)
|
|
logger.debug("scaled column width: (old_width: %s, scale: %s, new_width:%s)",
|
|
original_size, scale, retval)
|
|
return retval
|
|
|
|
@property
|
|
def items(self):
|
|
""" Returns the number of items held in this container """
|
|
return self._items
|
|
|
|
@property
|
|
def subframe(self):
|
|
""" Returns the next sub-frame to be populated """
|
|
frame = self.subframes[self._idx]
|
|
next_idx = self._idx + 1 if self._idx + 1 < self.columns else 0
|
|
logger.debug("current_idx: %s, next_idx: %s", self._idx, next_idx)
|
|
self._idx = next_idx
|
|
self._items += 1
|
|
return frame
|
|
|
|
def set_subframes(self):
|
|
""" Set a sub-frame for each possible column """
|
|
subframes = []
|
|
for idx in range(self.max_columns):
|
|
name = f"af_subframe_{idx}"
|
|
subframe = ttk.Frame(self.parent, name=name, style=f"{self._style}TFrame")
|
|
if idx < self.columns:
|
|
# Only pack visible columns
|
|
subframe.pack(padx=5, pady=5, side=tk.LEFT, anchor=tk.N, expand=True, fill=tk.X)
|
|
subframes.append(subframe)
|
|
logger.debug("Added subframe: %s", name)
|
|
return subframes
|
|
|
|
def rearrange_columns(self, width):
|
|
""" On column number change redistribute widgets """
|
|
if not self.validate(width):
|
|
return
|
|
|
|
new_columns = min(self.max_columns, max(1, width // self.single_column_width))
|
|
logger.debug("Rearranging columns: (width: %s, old_columns: %s, new_columns: %s)",
|
|
width, self.columns, new_columns)
|
|
self.columns = new_columns
|
|
if not self._widget_config:
|
|
self.compile_widget_config()
|
|
self.destroy_children()
|
|
self.repack_columns()
|
|
# Reset counters
|
|
self._items = 0
|
|
self._idx = 0
|
|
self.pack_widget_clones(self._widget_config)
|
|
|
|
def validate(self, width):
|
|
""" Validate that passed in width should trigger column re-arranging """
|
|
if ((width < self.single_column_width and self.columns == 1) or
|
|
(width > self.max_width and self.columns == self.max_columns)):
|
|
logger.debug("width outside min/max thresholds: (min: %s, width: %s, max: %s)",
|
|
self.single_column_width, width, self.max_width)
|
|
return False
|
|
range_min = self.columns * self.single_column_width
|
|
range_max = (self.columns + 1) * self.single_column_width
|
|
if range_min < width < range_max:
|
|
logger.debug("width outside next step refresh threshold: (step down: %s, width: %s,"
|
|
"step up: %s)", range_min, width, range_max)
|
|
return False
|
|
return True
|
|
|
|
def compile_widget_config(self):
|
|
""" Compile all children recursively in correct order if not already compiled and add
|
|
to :attr:`_widget_config` """
|
|
zipped = zip_longest(*(subframe.winfo_children() for subframe in self.subframes))
|
|
children = [child for group in zipped for child in group if child is not None]
|
|
self._widget_config = [{"class": child.__class__,
|
|
"id": str(child),
|
|
"tooltip": _RECREATE_OBJECTS["tooltips"].get(str(child), None),
|
|
"rc_menu": _RECREATE_OBJECTS["contextmenus"].get(str(child), None),
|
|
"pack_info": self.pack_config_cleaner(child),
|
|
"name": child.winfo_name(),
|
|
"config": self.config_cleaner(child),
|
|
"children": self.get_all_children_config(child, []),
|
|
# Some children have custom kwargs, so keep dicts in sync
|
|
"custom_kwargs": self._custom_kwargs(child)}
|
|
for idx, child in enumerate(children)]
|
|
logger.debug("Compiled AutoFillContainer children: %s", self._widget_config)
|
|
|
|
@classmethod
|
|
def _custom_kwargs(cls, widget):
|
|
""" For custom widgets some custom arguments need to be passed from the old widget to the
|
|
newly created widget.
|
|
|
|
Parameters
|
|
----------
|
|
widget: tkinter widget
|
|
The widget to be checked for custom keyword arguments
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
The custom keyword arguments required for recreating the given widget
|
|
"""
|
|
retval = {}
|
|
if widget.__class__.__name__ == "MultiOption":
|
|
retval = {"value": widget._value, # pylint:disable=protected-access
|
|
"variable": widget._master_variable} # pylint:disable=protected-access
|
|
elif widget.__class__.__name__ == "ToggledFrame":
|
|
# Toggled Frames need to have their variable tracked
|
|
retval = {"text": widget._text, # pylint:disable=protected-access
|
|
"toggle_var": widget._toggle_var} # pylint:disable=protected-access
|
|
return retval
|
|
|
|
def get_all_children_config(self, widget, child_list):
|
|
""" Return all children, recursively, of given widget.
|
|
|
|
Parameters
|
|
----------
|
|
widget: tkinter widget
|
|
The widget to recursively obtain the configurations of each child
|
|
child_list: list
|
|
The list of child configurations already collected
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
The list of configurations for all recursive children of the given widget
|
|
"""
|
|
unpack = set()
|
|
for child in widget.winfo_children():
|
|
# Hidden Toggle Frame boxes need to be mapped
|
|
if child.winfo_ismapped() or "toggledframe_subframe" in str(child):
|
|
not_mapped = not child.winfo_ismapped()
|
|
# ToggleFrame is a custom widget that creates it's own children and handles
|
|
# bindings on the headers, to auto-hide the contents. To ensure that all child
|
|
# information (specifically pack information) can be collected, we need to pack
|
|
# any hidden sub-frames. These are then hidden again once collected.
|
|
if not_mapped and (child.winfo_name() == "toggledframe_subframe" or
|
|
child.winfo_name() == "chkbuttons"):
|
|
child.pack(fill=tk.X, expand=True)
|
|
child.update_idletasks() # Updates the packing info of children
|
|
unpack.add(child)
|
|
|
|
if child.winfo_name().startswith("toggledframe_header"):
|
|
# Headers should be entirely handled by parent widget
|
|
continue
|
|
|
|
child_list.append({
|
|
"class": child.__class__,
|
|
"id": str(child),
|
|
"tooltip": _RECREATE_OBJECTS["tooltips"].get(str(child), None),
|
|
"rc_menu": _RECREATE_OBJECTS["contextmenus"].get(str(child), None),
|
|
"pack_info": self.pack_config_cleaner(child),
|
|
"name": child.winfo_name(),
|
|
"config": self.config_cleaner(child),
|
|
"parent": child.winfo_parent(),
|
|
"custom_kwargs": self._custom_kwargs(child)})
|
|
self.get_all_children_config(child, child_list)
|
|
|
|
# Re-hide any toggle frames that were expanded
|
|
for hide in unpack:
|
|
hide.pack_forget()
|
|
hide.update_idletasks()
|
|
return child_list
|
|
|
|
@staticmethod
|
|
def config_cleaner(widget):
|
|
""" Some options don't like to be copied, so this returns a cleaned
|
|
configuration from a widget
|
|
We use config() instead of configure() because some items (ttk Scale) do
|
|
not populate configure()"""
|
|
new_config = {}
|
|
for key in widget.config():
|
|
if key == "class":
|
|
continue
|
|
val = widget.cget(key)
|
|
# Some keys default to "" but tkinter doesn't like to set config to this value
|
|
# so skip them to use default value.
|
|
if key in ("anchor", "justify", "compound") and val == "":
|
|
continue
|
|
# Following keys cannot be defined after widget is created:
|
|
if key in ("colormap", "container", "visual"):
|
|
continue
|
|
val = str(val) if isinstance(val, Tcl_Obj) else val
|
|
# Return correct command from master command dict
|
|
val = _RECREATE_OBJECTS["commands"][val] if key == "command" and val != "" else val
|
|
new_config[key] = val
|
|
return new_config
|
|
|
|
@staticmethod
|
|
def pack_config_cleaner(widget):
|
|
""" Some options don't like to be copied, so this returns a cleaned
|
|
configuration from a widget """
|
|
return {key: val for key, val in widget.pack_info().items() if key != "in"}
|
|
|
|
def destroy_children(self):
|
|
""" Destroy the currently existing widgets """
|
|
for subframe in self.subframes:
|
|
for child in subframe.winfo_children():
|
|
child.destroy()
|
|
|
|
def repack_columns(self):
|
|
""" Repack or unpack columns based on display columns """
|
|
for idx, subframe in enumerate(self.subframes):
|
|
logger.trace("Processing subframe: %s", subframe)
|
|
if idx < self.columns and not subframe.winfo_ismapped():
|
|
logger.trace("Packing subframe: %s", subframe)
|
|
subframe.pack(padx=5, pady=5, side=tk.LEFT, anchor=tk.N, expand=True, fill=tk.X)
|
|
elif idx >= self.columns and subframe.winfo_ismapped():
|
|
logger.trace("Forgetting subframe: %s", subframe)
|
|
subframe.pack_forget()
|
|
|
|
def pack_widget_clones(self, widget_dicts, old_children=None, new_children=None):
|
|
""" Recursively pass through the list of widgets creating clones and packing all
|
|
children.
|
|
|
|
Widgets cannot be given a new parent so we need to clone them and then pack the
|
|
new widgets.
|
|
|
|
Parameters
|
|
----------
|
|
widget_dicts: list
|
|
List of dictionaries, in appearance order, of widget information for cloning widgets
|
|
old_childen: list, optional
|
|
Used for recursion. Leave at ``None``
|
|
new_childen: list, optional
|
|
Used for recursion. Leave at ``None``
|
|
"""
|
|
for widget_dict in widget_dicts:
|
|
logger.debug("Cloning widget: %s", widget_dict)
|
|
old_children = [] if old_children is None else old_children
|
|
new_children = [] if new_children is None else new_children
|
|
if widget_dict.get("parent", None) is not None:
|
|
parent = new_children[old_children.index(widget_dict["parent"])]
|
|
logger.trace("old parent: '%s', new_parent: '%s'", widget_dict["parent"], parent)
|
|
else:
|
|
# Get the next sub-frame if this doesn't have a logged parent
|
|
parent = self.subframe
|
|
clone = widget_dict["class"](parent,
|
|
name=widget_dict["name"],
|
|
**widget_dict["custom_kwargs"])
|
|
if widget_dict["config"] is not None:
|
|
clone.configure(**widget_dict["config"])
|
|
if widget_dict["tooltip"] is not None:
|
|
Tooltip(clone, **widget_dict["tooltip"])
|
|
rc_menu = widget_dict["rc_menu"]
|
|
if rc_menu is not None:
|
|
# Re-initialize for new widget and bind
|
|
rc_menu.__init__(widget=clone)
|
|
rc_menu.cm_bind()
|
|
clone.pack(**widget_dict["pack_info"])
|
|
|
|
# Handle ToggledFrame sub-frames. If the parent is not set to expanded, then we need to
|
|
# hide the sub-frame
|
|
if clone.winfo_name() == "toggledframe_subframe":
|
|
toggle_frame = clone.nametowidget(clone.winfo_parent())
|
|
if not toggle_frame.is_expanded:
|
|
logger.debug("Hiding minimized toggle box: %s", clone)
|
|
clone.pack_forget()
|
|
|
|
old_children.append(widget_dict["id"])
|
|
new_children.append(clone)
|
|
if widget_dict.get("children", None) is not None:
|
|
self.pack_widget_clones(widget_dict["children"], old_children, new_children)
|
|
|
|
|
|
class ControlBuilder():
|
|
"""
|
|
Builds and returns a frame containing a tkinter control with label
|
|
This should only be called from the ControlPanel class
|
|
|
|
Parameters
|
|
----------
|
|
parent: tkinter object
|
|
Parent tkinter object
|
|
option: ControlPanelOption object
|
|
Holds all of the required option information
|
|
option_columns: int
|
|
Number of options to put on a single row for check-buttons/radio-buttons
|
|
label_width: int
|
|
Sets the width of the control label
|
|
checkbuttons_frame: tkinter.frame
|
|
If a check-button frame is passed in, then check-buttons will be placed in this frame
|
|
rather than the main options frame
|
|
style: str
|
|
The name of the style to use for the control panel. Styles are configured when TkInter
|
|
initializes. The style name is the common prefix prior to the widget name. Provide an empty
|
|
string to use the OS style
|
|
blank_nones: bool
|
|
Sets selected values to an empty string rather than None if this is true.
|
|
"""
|
|
def __init__(self, parent, option, option_columns, # pylint: disable=too-many-arguments
|
|
label_width, checkbuttons_frame, style, blank_nones):
|
|
logger.debug("Initializing %s: (parent: %s, option: %s, option_columns: %s, "
|
|
"label_width: %s, checkbuttons_frame: %s, style: %s, blank_nones: %s)",
|
|
self.__class__.__name__, parent, option, option_columns, label_width,
|
|
checkbuttons_frame, style, blank_nones)
|
|
|
|
self.option = option
|
|
self.option_columns = option_columns
|
|
self.helpset = False
|
|
self.label_width = label_width
|
|
self.filebrowser = None
|
|
# Default to Control Panel Style
|
|
self._style = style = style if style else "CPanel."
|
|
self._theme = get_config().user_theme["group_panel"]
|
|
if self._style.startswith("SPanel"):
|
|
self._theme = {**self._theme, **get_config().user_theme["group_settings"]}
|
|
|
|
self.frame = self.control_frame(parent)
|
|
self.chkbtns = checkbuttons_frame
|
|
|
|
self.set_tk_var(blank_nones)
|
|
self.build_control()
|
|
logger.debug("Initialized: %s", self.__class__.__name__)
|
|
|
|
# Frame, control type and variable
|
|
def control_frame(self, parent):
|
|
""" Frame to hold control and it's label """
|
|
logger.debug("Build control frame")
|
|
frame = ttk.Frame(parent,
|
|
name=f"fr_{self.option.name}",
|
|
style=f"{self._style}Group.TFrame")
|
|
frame.pack(fill=tk.X)
|
|
logger.debug("Built control frame")
|
|
return frame
|
|
|
|
def set_tk_var(self, blank_nones):
|
|
""" Correct variable type for control """
|
|
val = "" if self.option.value is None and blank_nones else self.option.value
|
|
self.option.tk_var.set(val)
|
|
logger.debug("Set tk variable: (option: '%s', variable: %s, value: '%s')",
|
|
self.option.name, self.option.tk_var, val)
|
|
|
|
# Build the full control
|
|
def build_control(self):
|
|
""" Build the correct control type for the option passed through """
|
|
logger.debug("Build config option control")
|
|
if self.option.control not in (ttk.Checkbutton, "radio", "multi", "colorchooser"):
|
|
self.build_control_label()
|
|
self.build_one_control()
|
|
logger.debug("Built option control")
|
|
|
|
def build_control_label(self):
|
|
""" Label for control """
|
|
logger.debug("Build control label: (option: '%s')", self.option.name)
|
|
lbl = ttk.Label(self.frame,
|
|
text=self.option.title,
|
|
width=self.label_width,
|
|
anchor=tk.W,
|
|
style=f"{self._style}Group.TLabel")
|
|
lbl.pack(padx=5, pady=5, side=tk.LEFT, anchor=tk.N)
|
|
if self.option.helptext is not None:
|
|
_get_tooltip(lbl, text=self.option.helptext)
|
|
logger.debug("Built control label: (widget: '%s', title: '%s'",
|
|
self.option.name, self.option.title)
|
|
|
|
def build_one_control(self):
|
|
""" Build and place the option controls """
|
|
logger.debug("Build control: '%s')", self.option.name)
|
|
if self.option.control == "scale":
|
|
ctl = self.slider_control()
|
|
elif self.option.control in ("radio", "multi"):
|
|
ctl = self._multi_option_control(self.option.control)
|
|
elif self.option.control == "colorchooser":
|
|
ctl = self._color_control()
|
|
elif self.option.control == ttk.Checkbutton:
|
|
ctl = self.control_to_checkframe()
|
|
else:
|
|
ctl = self.control_to_optionsframe()
|
|
if self.option.control != ttk.Checkbutton:
|
|
ctl.pack(padx=5, pady=5, fill=tk.X, expand=True)
|
|
if self.option.helptext is not None and not self.helpset:
|
|
tooltip_kwargs = {"text": self.option.helptext}
|
|
if self.option.sysbrowser is not None:
|
|
tooltip_kwargs["text_variable"] = self.option.tk_var
|
|
_get_tooltip(ctl, **tooltip_kwargs)
|
|
|
|
logger.debug("Built control: '%s'", self.option.name)
|
|
|
|
def _multi_option_control(self, option_type):
|
|
""" Create a group of buttons for single or multi-select
|
|
|
|
Parameters
|
|
----------
|
|
option_type: {"radio", "multi"}
|
|
The type of boxes that this control should hold. "radio" for single item select,
|
|
"multi" for multi item select.
|
|
|
|
"""
|
|
logger.debug("Adding %s group: %s", option_type, self.option.name)
|
|
help_intro, help_items = self._get_multi_help_items(self.option.helptext)
|
|
ctl = ttk.LabelFrame(self.frame,
|
|
text=self.option.title,
|
|
name=f"{option_type}_labelframe",
|
|
style=f"{self._style}Group.TLabelframe")
|
|
holder = AutoFillContainer(ctl,
|
|
self.option_columns,
|
|
self.option_columns,
|
|
style=f"{self._style}Group.")
|
|
for choice in self.option.choices:
|
|
if option_type == "radio":
|
|
ctl = ttk.Radiobutton
|
|
style = f"{self._style}Group.TRadiobutton"
|
|
else:
|
|
ctl = MultiOption
|
|
style = f"{self._style}Group.TCheckbutton"
|
|
|
|
ctl = ctl(holder.subframe,
|
|
text=choice.replace("_", " ").title(),
|
|
value=choice,
|
|
variable=self.option.tk_var,
|
|
style=style)
|
|
if choice.lower() in help_items:
|
|
self.helpset = True
|
|
helptext = help_items[choice.lower()]
|
|
helptext = f"{helptext}\n\n - {help_intro}"
|
|
_get_tooltip(ctl, text=helptext)
|
|
ctl.pack(anchor=tk.W, fill=tk.X)
|
|
logger.debug("Added %s option %s", option_type, choice)
|
|
return holder.parent
|
|
|
|
@staticmethod
|
|
def _get_multi_help_items(helptext):
|
|
""" Split the help text up, for formatted help text, into the individual options
|
|
for multi/radio buttons.
|
|
|
|
Parameters
|
|
----------
|
|
helptext: str
|
|
The raw help text for this cli. option
|
|
|
|
Returns
|
|
-------
|
|
tuple (`str`, `dict`)
|
|
The help text intro and a dictionary containing the help text split into separate
|
|
entries for each option choice
|
|
"""
|
|
logger.debug("raw help: %s", helptext)
|
|
all_help = helptext.splitlines()
|
|
intro = ""
|
|
if any(line.startswith(" - ") for line in all_help):
|
|
intro = all_help[0]
|
|
retval = (intro,
|
|
{re.sub(r"[^A-Za-z0-9\-\_]+", "",
|
|
line.split()[1].lower()): " ".join(line.replace("_", " ").split()[1:])
|
|
for line in all_help if line.startswith(" - ")})
|
|
logger.debug("help items: %s", retval)
|
|
return retval
|
|
|
|
def slider_control(self):
|
|
""" A slider control with corresponding Entry box """
|
|
logger.debug("Add slider control to Options Frame: (widget: '%s', dtype: %s, "
|
|
"rounding: %s, min_max: %s)", self.option.name, self.option.dtype,
|
|
self.option.rounding, self.option.min_max)
|
|
validate = self.slider_check_int if self.option.dtype == int else self.slider_check_float
|
|
vcmd = self.frame.register(validate)
|
|
tbox = tk.Entry(self.frame,
|
|
width=8,
|
|
textvariable=self.option.tk_var,
|
|
justify=tk.RIGHT,
|
|
font=get_config().default_font,
|
|
validate="all",
|
|
validatecommand=(vcmd, "%P"),
|
|
bg=self._theme["input_color"],
|
|
fg=self._theme["input_font"],
|
|
highlightbackground=self._theme["input_font"],
|
|
highlightthickness=1,
|
|
bd=0)
|
|
tbox.pack(padx=(0, 5), side=tk.RIGHT)
|
|
cmd = partial(set_slider_rounding,
|
|
var=self.option.tk_var,
|
|
d_type=self.option.dtype,
|
|
round_to=self.option.rounding,
|
|
min_max=self.option.min_max)
|
|
ctl = ttk.Scale(self.frame,
|
|
variable=self.option.tk_var,
|
|
command=cmd,
|
|
style=f"{self._style}Horizontal.TScale")
|
|
_add_command(ctl.cget("command"), cmd)
|
|
rc_menu = _get_contextmenu(tbox)
|
|
rc_menu.cm_bind()
|
|
ctl["from_"] = self.option.min_max[0]
|
|
ctl["to"] = self.option.min_max[1]
|
|
logger.debug("Added slider control to Options Frame: %s", self.option.name)
|
|
return ctl
|
|
|
|
@staticmethod
|
|
def slider_check_int(value):
|
|
""" Validate a slider's text entry box for integer values.
|
|
|
|
Parameters
|
|
----------
|
|
value: str
|
|
The slider text entry value to validate
|
|
"""
|
|
if value.isdigit() or value == "":
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def slider_check_float(value):
|
|
""" Validate a slider's text entry box for float values.
|
|
Parameters
|
|
----------
|
|
value: str
|
|
The slider text entry value to validate
|
|
"""
|
|
if value:
|
|
try:
|
|
float(value)
|
|
except ValueError:
|
|
return False
|
|
return True
|
|
|
|
def control_to_optionsframe(self):
|
|
""" Standard non-check buttons sit in the main options frame """
|
|
logger.debug("Add control to Options Frame: (widget: '%s', control: %s, choices: %s)",
|
|
self.option.name, self.option.control, self.option.choices)
|
|
if self.option.sysbrowser is not None:
|
|
self.filebrowser = FileBrowser(self.option.name,
|
|
self.option.tk_var,
|
|
self.frame,
|
|
self.option.sysbrowser,
|
|
self._style)
|
|
|
|
if self.option.control == tk.Entry:
|
|
ctl = self.option.control(self.frame,
|
|
textvariable=self.option.tk_var,
|
|
font=get_config().default_font,
|
|
bg=self._theme["input_color"],
|
|
fg=self._theme["input_font"],
|
|
highlightbackground=self._theme["input_font"],
|
|
highlightthickness=1,
|
|
bd=0)
|
|
else: # Combobox
|
|
ctl = self.option.control(self.frame,
|
|
textvariable=self.option.tk_var,
|
|
font=get_config().default_font,
|
|
state="readonly",
|
|
style=f"{self._style}TCombobox")
|
|
|
|
# Style for combo list boxes needs to be set directly on widget as no style parameter
|
|
cmd = f"[ttk::combobox::PopdownWindow {ctl}].f.l configure -"
|
|
ctl.tk.eval(f"{cmd}foreground {self._theme['input_font']}")
|
|
ctl.tk.eval(f"{cmd}background {self._theme['input_color']}")
|
|
ctl.tk.eval(f"{cmd}selectforeground {self._theme['control_active']}")
|
|
ctl.tk.eval(f"{cmd}selectbackground {self._theme['control_disabled']}")
|
|
|
|
rc_menu = _get_contextmenu(ctl)
|
|
rc_menu.cm_bind()
|
|
|
|
if self.option.choices:
|
|
logger.debug("Adding combo choices: %s", self.option.choices)
|
|
ctl["values"] = self.option.choices
|
|
ctl["state"] = "readonly"
|
|
logger.debug("Added control to Options Frame: %s", self.option.name)
|
|
return ctl
|
|
|
|
def _color_control(self):
|
|
""" Clickable label holding the currently selected color """
|
|
logger.debug("Add control to Options Frame: (widget: '%s', control: %s, choices: %s)",
|
|
self.option.name, self.option.control, self.option.choices)
|
|
frame = ttk.Frame(self.frame, style=f"{self._style}Group.TFrame")
|
|
lbl = ttk.Label(frame,
|
|
text=self.option.title,
|
|
width=self.label_width,
|
|
anchor=tk.W,
|
|
style=f"{self._style}Group.TLabel")
|
|
ctl = tk.Frame(frame,
|
|
bg=self.option.tk_var.get(),
|
|
bd=2,
|
|
cursor="hand2",
|
|
relief=tk.SUNKEN,
|
|
width=round(int(20 * get_config().scaling_factor)),
|
|
height=round(int(14 * get_config().scaling_factor)))
|
|
ctl.bind("<Button-1>", lambda *e, c=ctl, t=self.option.title: self._ask_color(c, t))
|
|
lbl.pack(side=tk.LEFT, anchor=tk.N)
|
|
ctl.pack(side=tk.RIGHT, anchor=tk.W)
|
|
frame.pack(padx=5, side=tk.LEFT, anchor=tk.W)
|
|
if self.option.helptext is not None:
|
|
_get_tooltip(frame, text=self.option.helptext)
|
|
# Callback to set the color chooser background on an update (e.g. reset)
|
|
self.option.tk_var.trace("w", lambda *e: ctl.config(bg=self.option.tk_var.get()))
|
|
logger.debug("Added control to Options Frame: %s", self.option.name)
|
|
return ctl
|
|
|
|
def _ask_color(self, frame, title):
|
|
""" Pop ask color dialog set to variable and change frame color """
|
|
color = self.option.tk_var.get()
|
|
chosen = colorchooser.askcolor(parent=frame, color=color, title=f"{title} Color")[1]
|
|
if chosen is None:
|
|
return
|
|
self.option.tk_var.set(chosen)
|
|
|
|
def control_to_checkframe(self):
|
|
""" Add check-buttons to the check-button frame """
|
|
logger.debug("Add control checkframe: '%s'", self.option.name)
|
|
chkframe = self.chkbtns.subframe
|
|
ctl = self.option.control(chkframe,
|
|
variable=self.option.tk_var,
|
|
text=self.option.title,
|
|
name=self.option.name,
|
|
style=f"{self._style}Group.TCheckbutton")
|
|
_get_tooltip(ctl, text=self.option.helptext)
|
|
ctl.pack(side=tk.TOP, anchor=tk.W, fill=tk.X)
|
|
logger.debug("Added control checkframe: '%s'", self.option.name)
|
|
return ctl
|
|
|
|
|
|
class FileBrowser():
|
|
""" Add FileBrowser buttons to control and handle routing """
|
|
def __init__(self, opt_name, tk_var, control_frame, sysbrowser_dict, style):
|
|
logger.debug("Initializing: %s: (tk_var: %s, control_frame: %s, sysbrowser_dict: %s, "
|
|
"style: %s)", self.__class__.__name__, tk_var, control_frame,
|
|
sysbrowser_dict, style)
|
|
self._opt_name = opt_name
|
|
self.tk_var = tk_var
|
|
self.frame = control_frame
|
|
self._style = style
|
|
self.browser = sysbrowser_dict["browser"]
|
|
self.filetypes = sysbrowser_dict["filetypes"]
|
|
self.action_option = self.format_action_option(sysbrowser_dict.get("action_option", None))
|
|
self.command = sysbrowser_dict.get("command", None)
|
|
self.destination = sysbrowser_dict.get("destination", None)
|
|
self.add_browser_buttons()
|
|
logger.debug("Initialized: %s", self.__class__.__name__)
|
|
|
|
@property
|
|
def helptext(self):
|
|
""" Dict containing tooltip text for buttons """
|
|
retval = {"folder": _("Select a folder..."),
|
|
"load": _("Select a file..."),
|
|
"load2": _("Select a file..."),
|
|
"picture": _("Select a folder of images..."),
|
|
"video": _("Select a video..."),
|
|
"model": _("Select a model folder..."),
|
|
"multi_load": _("Select one or more files..."),
|
|
"context": _("Select a file or folder..."),
|
|
"save_as": _("Select a save location...")}
|
|
return retval
|
|
|
|
@staticmethod
|
|
def format_action_option(action_option):
|
|
""" Format the action option to remove any dashes at the start """
|
|
if action_option is None:
|
|
return action_option
|
|
if action_option.startswith("--"):
|
|
return action_option[2:]
|
|
if action_option.startswith("-"):
|
|
return action_option[1:]
|
|
return action_option
|
|
|
|
def add_browser_buttons(self):
|
|
""" Add correct file browser button for control """
|
|
logger.debug("Adding browser buttons: (sysbrowser: %s", self.browser)
|
|
frame = ttk.Frame(self.frame, style=f"{self._style}Group.TFrame")
|
|
frame.pack(side=tk.RIGHT, padx=(0, 5))
|
|
|
|
for browser in self.browser:
|
|
if browser == "save":
|
|
lbl = "save_as"
|
|
elif browser == "load" and self.filetypes == "video":
|
|
lbl = self.filetypes
|
|
elif browser == "load":
|
|
lbl = "load2"
|
|
elif browser == "folder" and (self._opt_name.startswith(("frames", "faces"))
|
|
or "input" in self._opt_name):
|
|
lbl = "picture"
|
|
elif browser == "folder" and "model" in self._opt_name:
|
|
lbl = "model"
|
|
else:
|
|
lbl = browser
|
|
img = get_images().icons[lbl]
|
|
action = getattr(self, "ask_" + browser)
|
|
cmd = partial(action, filepath=self.tk_var, filetypes=self.filetypes)
|
|
fileopn = tk.Button(frame,
|
|
image=img,
|
|
command=cmd,
|
|
relief=tk.SOLID,
|
|
bd=1,
|
|
bg=get_config().user_theme["group_panel"]["button_background"],
|
|
cursor="hand2")
|
|
_add_command(fileopn.cget("command"), cmd)
|
|
fileopn.pack(padx=1, side=tk.RIGHT)
|
|
_get_tooltip(fileopn, text=self.helptext[lbl])
|
|
logger.debug("Added browser buttons: (action: %s, filetypes: %s",
|
|
action, self.filetypes)
|
|
|
|
def set_context_action_option(self, options):
|
|
""" Set the tk_var for the source action option
|
|
that dictates the context sensitive file browser. """
|
|
if self.browser != ["context"]:
|
|
return
|
|
actions = {opt.name: opt.tk_var for opt in options}
|
|
logger.debug("Settiong action option for opt %s", self.action_option)
|
|
self.action_option = actions[self.action_option]
|
|
|
|
@staticmethod
|
|
def ask_folder(filepath, filetypes=None):
|
|
""" Pop-up to get path to a directory
|
|
:param filepath: tkinter StringVar object
|
|
that will store the path to a directory.
|
|
:param filetypes: Unused argument to allow
|
|
filetypes to be given in ask_load(). """
|
|
dirname = FileHandler("dir", filetypes).return_file
|
|
if dirname:
|
|
logger.debug(dirname)
|
|
filepath.set(dirname)
|
|
|
|
@staticmethod
|
|
def ask_load(filepath, filetypes):
|
|
""" Pop-up to get path to a file """
|
|
filename = FileHandler("filename", filetypes).return_file
|
|
if filename:
|
|
logger.debug(filename)
|
|
filepath.set(filename)
|
|
|
|
@staticmethod
|
|
def ask_multi_load(filepath, filetypes):
|
|
""" Pop-up to get path to a file """
|
|
filenames = FileHandler("filename_multi", filetypes).return_file
|
|
if filenames:
|
|
final_names = " ".join(f"\"{fname}\"" for fname in filenames)
|
|
logger.debug(final_names)
|
|
filepath.set(final_names)
|
|
|
|
@staticmethod
|
|
def ask_save(filepath, filetypes=None):
|
|
""" Pop-up to get path to save a new file """
|
|
filename = FileHandler("save_filename", filetypes).return_file
|
|
if filename:
|
|
logger.debug(filename)
|
|
filepath.set(filename)
|
|
|
|
@staticmethod
|
|
def ask_nothing(filepath, filetypes=None): # pylint:disable=unused-argument
|
|
""" Method that does nothing, used for disabling open/save pop up """
|
|
return
|
|
|
|
def ask_context(self, filepath, filetypes):
|
|
""" Method to pop the correct dialog depending on context """
|
|
logger.debug("Getting context filebrowser")
|
|
selected_action = self.action_option.get()
|
|
selected_variable = self.destination
|
|
filename = FileHandler("context",
|
|
filetypes,
|
|
command=self.command,
|
|
action=selected_action,
|
|
variable=selected_variable).return_file
|
|
if filename:
|
|
logger.debug(filename)
|
|
filepath.set(filename)
|