1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-07 10:37:19 -04:00
faceswap/lib/gui/utils.py
torzdf cd00859c40
model_refactor (#571) (#572)
* model_refactor (#571)

* original model to new structure

* IAE model to new structure

* OriginalHiRes to new structure

* Fix trainer for different resolutions

* Initial config implementation

* Configparse library added

* improved training data loader

* dfaker model working

* Add logging to training functions

* Non blocking input for cli training

* Add error handling to threads. Add non-mp queues to queue_handler

* Improved Model Building and NNMeta

* refactor lib/models

* training refactor. DFL H128 model Implementation

* Dfaker - use hashes

* Move timelapse. Remove perceptual loss arg

* Update INSTALL.md. Add logger formatting. Update Dfaker training

* DFL h128 partially ported

* Add mask to dfaker (#573)

* Remove old models. Add mask to dfaker

* dfl mask. Make masks selectable in config (#575)

* DFL H128 Mask. Mask type selectable in config.

* remove gan_v2_2

* Creating Input Size config for models

Creating Input Size config for models

Will be used downstream in converters.

Also name change of image_shape to input_shape to clarify ( for future models with potentially different output_shapes)

* Add mask loss options to config

* MTCNN options to config.ini. Remove GAN config. Update USAGE.md

* Add sliders for numerical values in GUI

* Add config plugins menu to gui. Validate config

* Only backup model if loss has dropped. Get training working again

* bugfixes

* Standardise loss printing

* GUI idle cpu fixes. Graph loss fix.

* mutli-gpu logging bugfix

* Merge branch 'staging' into train_refactor

* backup state file

* Crash protection: Only backup if both total losses have dropped

* Port OriginalHiRes_RC4 to train_refactor (OriginalHiRes)

* Load and save model structure with weights

* Slight code update

* Improve config loader. Add subpixel opt to all models. Config to state

* Show samples... wrong input

* Remove AE topology. Add input/output shapes to State

* Port original_villain (birb/VillainGuy) model to faceswap

* Add plugin info to GUI config pages

* Load input shape from state. IAE Config options.

* Fix transform_kwargs.
Coverage to ratio.
Bugfix mask detection

* Suppress keras userwarnings.
Automate zoom.
Coverage_ratio to model def.

* Consolidation of converters & refactor (#574)

* Consolidation of converters & refactor

Initial Upload of alpha

Items
- consolidate convert_mased & convert_adjust into one converter
-add average color adjust to convert_masked
-allow mask transition blur size to be a fixed integer of pixels and a fraction of the facial mask size
-allow erosion/dilation size to be a fixed integer of pixels and a fraction of the facial mask size
-eliminate redundant type conversions to avoid multiple round-off errors
-refactor loops for vectorization/speed
-reorganize for clarity & style changes

TODO
- bug/issues with warping the new face onto a transparent old image...use a cleanup mask for now
- issues with mask border giving black ring at zero erosion .. investigate
- remove GAN ??
- test enlargment factors of umeyama standard face .. match to coverage factor
- make enlargment factor a model parameter
- remove convert_adjusted and referencing code when finished

* Update Convert_Masked.py

default blur size of 2 to match original...
description of enlargement tests
breakout matrxi scaling into def

* Enlargment scale as a cli parameter

* Update cli.py

* dynamic interpolation algorithm

Compute x & y scale factors from the affine matrix on the fly by QR decomp.
Choose interpolation alogrithm for the affine warp based on an upsample or downsample for each image

* input size
input size from config

* fix issues with <1.0 erosion

* Update convert.py

* Update Convert_Adjust.py

more work on the way to merginf

* Clean up help note on sharpen

* cleanup seamless

* Delete Convert_Adjust.py

* Update umeyama.py

* Update training_data.py

* swapping

* segmentation stub

* changes to convert.str

* Update masked.py

* Backwards compatibility fix for models
Get converter running

* Convert:
Move masks to class.
bugfix blur_size
some linting

* mask fix

* convert fixes

- missing facehull_rect re-added
- coverage to %
- corrected coverage logic
- cleanup of gui option ordering

* Update cli.py

* default for blur

* Update masked.py

* added preliminary low_mem version of OriginalHighRes model plugin

* Code cleanup, minor fixes

* Update masked.py

* Update masked.py

* Add dfl mask to convert

* histogram fix & seamless location

* update

* revert

* bugfix: Load actual configuration in gui

* Standardize nn_blocks

* Update cli.py

* Minor code amends

* Fix Original HiRes model

* Add masks to preview output for mask trainers
refactor trainer.__base.py

* Masked trainers converter support

* convert bugfix

* Bugfix: Converter for masked (dfl/dfaker) trainers

* Additional Losses (#592)

* initial upload

* Delete blur.py

* default initializer = He instead of Glorot (#588)

* Allow kernel_initializer to be overridable

* Add ICNR Initializer option for upscale on all models.

* Hopefully fixes RSoDs with original-highres model plugin

* remove debug line

* Original-HighRes model plugin Red Screen of Death fix, take #2

* Move global options to _base. Rename Villain model

* clipnorm and res block biases

* scale the end of res block

* res block

* dfaker pre-activation res

* OHRES pre-activation

* villain pre-activation

* tabs/space in nn_blocks

* fix for histogram with mask all set to zero

* fix to prevent two networks with same name

* GUI: Wider tooltips. Improve TQDM capture

* Fix regex bug

* Convert padding=48 to ratio of image size

* Add size option to alignments tool extract

* Pass through training image size to convert from model

* Convert: Pull training coverage from model

* convert: coverage, blur and erode to percent

* simplify matrix scaling

* ordering of sliders in train

* Add matrix scaling to utils. Use interpolation in lib.aligner transform

* masked.py Import get_matrix_scaling from utils

* fix circular import

* Update masked.py

* quick fix for matrix scaling

* testing thus for now

* tqdm regex capture bugfix

* Minor ammends

* blur size cleanup

* Remove coverage option from convert (Now cascades from model)

* Implement convert for all model types

* Add mask option and coverage option to all existing models

* bugfix for model loading on convert

* debug print removal

* Bugfix for masks in dfl_h128 and iae

* Update preview display. Add preview scaling to cli

* mask notes

* Delete training_data_v2.py

errant file

* training data variables

* Fix timelapse function

* Add new config items to state file for legacy purposes

* Slight GUI tweak

* Raise exception if problem with loaded model

* Add Tensorboard support (Logs stored in model directory)

* ICNR fix

* loss bugfix

* convert bugfix

* Move ini files to config folder. Make TensorBoard optional

* Fix training data for unbalanced inputs/outputs

* Fix config "none" test

* Keep helptext in .ini files when saving config from GUI

* Remove frame_dims from alignments

* Add no-flip and warp-to-landmarks cli options

* Revert OHR to RC4_fix version

* Fix lowmem mode on OHR model

* padding to variable

* Save models in parallel threads

* Speed-up of res_block stability

* Automated Reflection Padding

* Reflect Padding as a training option

Includes auto-calculation of proper padding shapes, input_shapes, output_shapes

Flag included in config now

* rest of reflect padding

* Move TB logging to cli. Session info to state file

* Add session iterations to state file

* Add recent files to menu. GUI code tidy up

* [GUI] Fix recent file list update issue

* Add correct loss names to TensorBoard logs

* Update live graph to use TensorBoard and remove animation

* Fix analysis tab. GUI optimizations

* Analysis Graph popup to Tensorboard Logs

* [GUI] Bug fix for graphing for models with hypens in name

* [GUI] Correctly split loss to tabs during training

* [GUI] Add loss type selection to analysis graph

* Fix store command name in recent files. Switch to correct tab on open

* [GUI] Disable training graph when 'no-logs' is selected

* Fix graphing race condition

* rename original_hires model to unbalanced
2019-02-09 18:35:12 +00:00

603 lines
24 KiB
Python

#!/usr/bin/env python3
""" Utility functions for the GUI """
import logging
import os
import platform
import sys
import tkinter as tk
from tkinter import filedialog, ttk
from PIL import Image, ImageTk
from lib.Serializer import JSONSerializer
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
_CONFIG = None
_IMAGES = None
def initialize_config(cli_opts, scaling_factor, pathcache, statusbar, session):
""" Initialize the config and add to global constant """
global _CONFIG # pylint: disable=global-statement
if _CONFIG is not None:
return
logger.debug("Initializing config: (cli_opts: %s, tk_vars: %s, pathcache: %s, statusbar: %s, "
"session: %s)", cli_opts, scaling_factor, pathcache, statusbar, session)
_CONFIG = Config(cli_opts, scaling_factor, pathcache, statusbar, session)
def get_config():
""" return the _CONFIG constant """
return _CONFIG
def initialize_images():
""" Initialize the config and add to global constant """
global _IMAGES # pylint: disable=global-statement
if _IMAGES is not None:
return
logger.debug("Initializing images")
_IMAGES = Images()
def get_images():
""" return the _CONFIG constant """
return _IMAGES
def set_slider_rounding(value, var, d_type, round_to, min_max):
""" Set the underlying variable to correct number based on slider rounding """
if 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 FileHandler():
""" Raise a filedialog box and capture input """
def __init__(self, handletype, filetype, command=None, action=None,
variable=None):
logger.debug("Initializing %s: (Handletype: '%s', filetype: '%s', command: '%s', action: "
"'%s', variable: %s)", self.__class__.__name__, handletype, filetype, command,
action, variable)
self.handletype = handletype
all_files = ("All files", "*.*")
self.filetypes = {"default": (all_files,),
"alignments": (("JSON", "*.json"),
("Pickle", "*.p"),
("YAML", "*.yaml"),
all_files),
"config": (("Faceswap config files", "*.fsw"), all_files),
"csv": (("Comma separated values", "*.csv"), all_files),
"image": (("Bitmap", "*.bmp"),
("JPG", "*.jpeg", "*.jpg"),
("PNG", "*.png"),
("TIFF", "*.tif", "*.tiff"),
all_files),
"state": (("State files", "*.json"), all_files),
"log": (("Log files", "*.log"), all_files),
"video": (("Audio Video Interleave", "*.avi"),
("Flash Video", "*.flv"),
("Matroska", "*.mkv"),
("MOV", "*.mov"),
("MP4", "*.mp4"),
("MPEG", "*.mpeg"),
("WebM", "*.webm"),
all_files)}
self.contexts = {
"effmpeg": {
"input": {"extract": "filename",
"gen-vid": "dir",
"get-fps": "filename",
"get-info": "filename",
"mux-audio": "filename",
"rescale": "filename",
"rotate": "filename",
"slice": "filename"},
"output": {"extract": "dir",
"gen-vid": "savefilename",
"get-fps": "nothing",
"get-info": "nothing",
"mux-audio": "savefilename",
"rescale": "savefilename",
"rotate": "savefilename",
"slice": "savefilename"}
}
}
self.defaults = self.set_defaults()
self.kwargs = self.set_kwargs(filetype, command, action, variable)
self.retfile = getattr(self, self.handletype.lower())()
logger.debug("Initialized %s", self.__class__.__name__)
def set_defaults(self):
""" Set the default filetype to be first in list of filetypes,
or set a custom filetype if the first is not correct """
defaults = {key: val[0][1].replace("*", "")
for key, val in self.filetypes.items()}
defaults["default"] = None
defaults["video"] = ".mp4"
defaults["image"] = ".png"
logger.debug(defaults)
return defaults
def set_kwargs(self, filetype, command, action, variable=None):
""" Generate the required kwargs for the requested browser """
logger.debug("Setting Kwargs: (filetype: '%s', command: '%s': action: '%s', "
"variable: '%s')", filetype, command, action, variable)
kwargs = dict()
if self.handletype.lower() == "context":
self.set_context_handletype(command, action, variable)
if self.handletype.lower() in (
"open", "save", "filename", "savefilename"):
kwargs["filetypes"] = self.filetypes[filetype]
if self.defaults.get(filetype, None):
kwargs['defaultextension'] = self.defaults[filetype]
if self.handletype.lower() == "save":
kwargs["mode"] = "w"
if self.handletype.lower() == "open":
kwargs["mode"] = "r"
logger.debug("Set Kwargs: %s", kwargs)
return kwargs
def set_context_handletype(self, command, action, variable):
""" Choose the correct file browser action based on context """
if self.contexts[command].get(variable, None) is not None:
handletype = self.contexts[command][variable][action]
else:
handletype = self.contexts[command][action]
logger.debug(handletype)
self.handletype = handletype
def open(self):
""" Open a file """
logger.debug("Popping Open browser")
return filedialog.askopenfile(**self.kwargs)
def save(self):
""" Save a file """
logger.debug("Popping Save browser")
return filedialog.asksaveasfile(**self.kwargs)
def dir(self):
""" Get a directory location """
logger.debug("Popping Dir browser")
return filedialog.askdirectory(**self.kwargs)
def savedir(self):
""" Get a save dir location """
logger.debug("Popping SaveDir browser")
return filedialog.askdirectory(**self.kwargs)
def filename(self):
""" Get an existing file location """
logger.debug("Popping Filename browser")
return filedialog.askopenfilename(**self.kwargs)
def savefilename(self):
""" Get a save file location """
logger.debug("Popping SaveFilename browser")
return filedialog.asksaveasfilename(**self.kwargs)
@staticmethod
def nothing(): # pylint: disable=useless-return
""" Method that does nothing, used for disabling open/save pop up """
logger.debug("Popping Nothing browser")
return
class Images():
""" Holds locations of images and actual images
Don't call directly. Call get_images()
"""
def __init__(self):
logger.debug("Initializing %s", self.__class__.__name__)
pathcache = get_config().pathcache
self.pathicons = os.path.join(pathcache, "icons")
self.pathpreview = os.path.join(pathcache, "preview")
self.pathoutput = None
self.previewoutput = None
self.previewtrain = dict()
self.errcount = 0
self.icons = dict()
self.icons["folder"] = ImageTk.PhotoImage(file=os.path.join(
self.pathicons, "open_folder.png"))
self.icons["load"] = ImageTk.PhotoImage(file=os.path.join(
self.pathicons, "open_file.png"))
self.icons["context"] = ImageTk.PhotoImage(file=os.path.join(
self.pathicons, "open_file.png"))
self.icons["save"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "save.png"))
self.icons["reset"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "reset.png"))
self.icons["clear"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "clear.png"))
self.icons["graph"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "graph.png"))
self.icons["zoom"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "zoom.png"))
self.icons["move"] = ImageTk.PhotoImage(file=os.path.join(self.pathicons, "move.png"))
logger.debug("Initialized %s: (icons: %s)", self.__class__.__name__, self.icons)
def delete_preview(self):
""" Delete the preview files """
logger.debug("Deleting previews")
for item in os.listdir(self.pathpreview):
if item.startswith(".gui_training_preview") and item.endswith(".jpg"):
fullitem = os.path.join(self.pathpreview, item)
logger.debug("Deleting: '%s'", fullitem)
os.remove(fullitem)
self.clear_image_cache()
def clear_image_cache(self):
""" Clear all cached images """
logger.debug("Clearing image cache")
self.pathoutput = None
self.previewoutput = None
self.previewtrain = dict()
@staticmethod
def get_images(imgpath):
""" Get the images stored within the given directory """
logger.trace("Getting images: '%s'", imgpath)
if not os.path.isdir(imgpath):
logger.debug("Folder does not exist")
return None
files = [os.path.join(imgpath, f)
for f in os.listdir(imgpath) if f.endswith((".png", ".jpg"))]
logger.trace("Image files: %s", files)
return files
def load_latest_preview(self):
""" Load the latest preview image for extract and convert """
logger.trace("Loading preview image")
imagefiles = self.get_images(self.pathoutput)
if not imagefiles or len(imagefiles) == 1:
logger.debug("No preview to display")
self.previewoutput = None
return
# Get penultimate file so we don't accidentally
# load a file that is being saved
show_file = sorted(imagefiles, key=os.path.getctime)[-2]
img = Image.open(show_file)
img.thumbnail((768, 432))
logger.trace("Displaying preview: '%s'", show_file)
self.previewoutput = (img, ImageTk.PhotoImage(img))
def load_training_preview(self):
""" Load the training preview images """
logger.trace("Loading Training preview images")
imagefiles = self.get_images(self.pathpreview)
modified = None
if not imagefiles:
logger.debug("No preview to display")
self.previewtrain = dict()
return
for img in imagefiles:
modified = os.path.getmtime(img) if modified is None else modified
name = os.path.basename(img)
name = os.path.splitext(name)[0]
name = name[name.rfind("_") + 1:].title()
try:
logger.trace("Displaying preview: '%s'", img)
size = self.get_current_size(name)
self.previewtrain[name] = [Image.open(img), None, modified]
self.resize_image(name, size)
self.errcount = 0
except ValueError:
# This is probably an error reading the file whilst it's
# being saved so ignore it for now and only pick up if
# there have been multiple consecutive fails
logger.warning("Unable to display preview: (image: '%s', attempt: %s)",
img, self.errcount)
if self.errcount < 10:
self.errcount += 1
else:
logger.error("Error reading the preview file for '%s'", img)
print("Error reading the preview file for {}".format(name))
self.previewtrain[name] = None
def get_current_size(self, name):
""" Return the size of the currently displayed image """
logger.trace("Getting size: '%s'", name)
if not self.previewtrain.get(name, None):
return None
img = self.previewtrain[name][1]
if not img:
return None
logger.trace("Got size: (name: '%s', width: '%s', height: '%s')",
name, img.width(), img.height())
return img.width(), img.height()
def resize_image(self, name, framesize):
""" Resize the training preview image
based on the passed in frame size """
logger.trace("Resizing image: (name: '%s', framesize: %s", name, framesize)
displayimg = self.previewtrain[name][0]
if framesize:
frameratio = float(framesize[0]) / float(framesize[1])
imgratio = float(displayimg.size[0]) / float(displayimg.size[1])
if frameratio <= imgratio:
scale = framesize[0] / float(displayimg.size[0])
size = (framesize[0], int(displayimg.size[1] * scale))
else:
scale = framesize[1] / float(displayimg.size[1])
size = (int(displayimg.size[0] * scale), framesize[1])
logger.trace("Scaling: (scale: %s, size: %s", scale, size)
# Hacky fix to force a reload if it happens to find corrupted
# data, probably due to reading the image whilst it is partially
# saved. If it continues to fail, then eventually raise.
for i in range(0, 1000):
try:
displayimg = displayimg.resize(size, Image.ANTIALIAS)
except OSError:
if i == 999:
raise
else:
continue
break
self.previewtrain[name][1] = ImageTk.PhotoImage(displayimg)
class ContextMenu(tk.Menu): # pylint: disable=too-many-ancestors
""" Pop up menu """
def __init__(self, widget):
logger.debug("Initializing %s: (widget_class: '%s')",
self.__class__.__name__, widget.winfo_class())
super().__init__(tearoff=0)
self.widget = widget
self.standard_actions()
logger.debug("Initialized %s", self.__class__.__name__)
def standard_actions(self):
""" Standard menu actions """
self.add_command(label="Cut", command=lambda: self.widget.event_generate("<<Cut>>"))
self.add_command(label="Copy", command=lambda: self.widget.event_generate("<<Copy>>"))
self.add_command(label="Paste", command=lambda: self.widget.event_generate("<<Paste>>"))
self.add_separator()
self.add_command(label="Select all", command=self.select_all)
def cm_bind(self):
""" Bind the menu to the widget's Right Click event """
button = "<Button-2>" if platform.system() == "Darwin" else "<Button-3>"
logger.debug("Binding '%s' to '%s'", button, self.widget.winfo_class())
x_offset = int(34 * get_config().scaling_factor)
self.widget.bind(button,
lambda event: self.tk_popup(event.x_root + x_offset, event.y_root, 0))
def select_all(self):
""" Select all for Text or Entry widgets """
logger.debug("Selecting all for '%s'", self.widget.winfo_class())
if self.widget.winfo_class() == "Text":
self.widget.focus_force()
self.widget.tag_add("sel", "1.0", "end")
else:
self.widget.focus_force()
self.widget.select_range(0, tk.END)
class ConsoleOut(ttk.Frame): # pylint: disable=too-many-ancestors
""" The Console out section of the GUI """
def __init__(self, parent, debug):
logger.debug("Initializing %s: (parent: %s, debug: %s)",
self.__class__.__name__, parent, debug)
ttk.Frame.__init__(self, parent)
self.pack(side=tk.TOP, anchor=tk.W, padx=10, pady=(2, 0),
fill=tk.BOTH, expand=True)
self.console = tk.Text(self)
rc_menu = ContextMenu(self.console)
rc_menu.cm_bind()
self.console_clear = get_config().tk_vars['consoleclear']
self.set_console_clear_var_trace()
self.debug = debug
self.build_console()
logger.debug("Initialized %s", self.__class__.__name__)
def set_console_clear_var_trace(self):
""" Set the trigger actions for the clear console var
when it has been triggered from elsewhere """
logger.debug("Set clear trace")
self.console_clear.trace("w", self.clear)
def build_console(self):
""" Build and place the console """
logger.debug("Build console")
self.console.config(width=100, height=6, bg="gray90", fg="black")
self.console.pack(side=tk.LEFT, anchor=tk.N, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(self, command=self.console.yview)
scrollbar.pack(side=tk.LEFT, fill="y")
self.console.configure(yscrollcommand=scrollbar.set)
self.redirect_console()
logger.debug("Built console")
def redirect_console(self):
""" Redirect stdout/stderr to console frame """
logger.debug("Redirect console")
if self.debug:
logger.info("Console debug activated. Outputting to main terminal")
else:
sys.stdout = SysOutRouter(console=self.console, out_type="stdout")
sys.stderr = SysOutRouter(console=self.console, out_type="stderr")
logger.debug("Redirected console")
def clear(self, *args): # pylint: disable=unused-argument
""" Clear the console output screen """
logger.debug("Clear console")
if not self.console_clear.get():
logger.debug("Console not set for clearing. Skipping")
return
self.console.delete(1.0, tk.END)
self.console_clear.set(False)
logger.debug("Cleared console")
class SysOutRouter():
""" Route stdout/stderr to the console window """
def __init__(self, console=None, out_type=None):
logger.debug("Initializing %s: (console: %s, out_type: '%s')",
self.__class__.__name__, console, out_type)
self.console = console
self.out_type = out_type
self.color = ("black" if out_type == "stdout" else "red")
logger.debug("Initialized %s", self.__class__.__name__)
def write(self, string):
""" Capture stdout/stderr """
self.console.insert(tk.END, string, self.out_type)
self.console.tag_config(self.out_type, foreground=self.color)
self.console.see(tk.END)
@staticmethod
def flush():
""" If flush is forced, send it to normal terminal """
sys.__stdout__.flush()
class Config():
""" Global configuration settings
Don't call directly. Call get_config()
"""
def __init__(self, cli_opts, scaling_factor, pathcache, statusbar, session):
logger.debug("Initializing %s: (cli_opts: %s, scaling_factor: %s, pathcache: %s, "
"statusbar: %s, session: %s)", self.__class__.__name__, cli_opts,
scaling_factor, pathcache, statusbar, session)
self.cli_opts = cli_opts
self.scaling_factor = scaling_factor
self.pathcache = pathcache
self.statusbar = statusbar
self.serializer = JSONSerializer
self.tk_vars = self.set_tk_vars()
self.command_notebook = None # set in command.py
self.session = session
logger.debug("Initialized %s", self.__class__.__name__)
@property
def command_tabs(self):
""" Return dict of command tab titles with their IDs """
return {self.command_notebook.tab(tab_id, "text").lower(): tab_id
for tab_id in range(0, self.command_notebook.index("end"))}
@staticmethod
def set_tk_vars():
""" TK Variables to be triggered by to indicate
what state various parts of the GUI should be in """
display = tk.StringVar()
display.set(None)
runningtask = tk.BooleanVar()
runningtask.set(False)
actioncommand = tk.StringVar()
actioncommand.set(None)
generatecommand = tk.StringVar()
generatecommand.set(None)
consoleclear = tk.BooleanVar()
consoleclear.set(False)
refreshgraph = tk.BooleanVar()
refreshgraph.set(False)
updatepreview = tk.BooleanVar()
updatepreview.set(False)
tk_vars = {"display": display,
"runningtask": runningtask,
"action": actioncommand,
"generate": generatecommand,
"consoleclear": consoleclear,
"refreshgraph": refreshgraph,
"updatepreview": updatepreview}
logger.debug(tk_vars)
return tk_vars
def load(self, command=None, filename=None):
""" Pop up load dialog for a saved config file """
logger.debug("Loading config: (command: '%s')", command)
if filename:
with open(filename, "r") as cfgfile:
cfg = self.serializer.unmarshal(cfgfile.read())
else:
cfgfile = FileHandler("open", "config").retfile
if not cfgfile:
return
cfg = self.serializer.unmarshal(cfgfile.read())
if not command and len(cfg.keys()) == 1:
command = list(cfg.keys())[0]
opts = self.get_command_options(cfg, command) if command else cfg
if not opts:
return
for cmd, opts in opts.items():
self.set_command_args(cmd, opts)
if command:
self.command_notebook.select(self.command_tabs[command])
self.add_to_recent(cfgfile.name, command)
logger.debug("Loaded config: (command: '%s', cfgfile: '%s')", command, cfgfile)
def get_command_options(self, cfg, command):
""" return the saved options for the requested
command, if not loading global options """
opts = cfg.get(command, None)
retval = {command: opts}
if not opts:
self.tk_vars["consoleclear"].set(True)
print("No {} section found in file".format(command))
logger.info("No %s section found in file", command)
retval = None
logger.debug(retval)
return retval
def set_command_args(self, command, options):
""" Pass the saved config items back to the CliOptions """
if not options:
return
for srcopt, srcval in options.items():
optvar = self.cli_opts.get_one_option_variable(command, srcopt)
if not optvar:
continue
optvar.set(srcval)
def save(self, command=None):
""" Save the current GUI state to a config file in json format """
logger.debug("Saving config: (command: '%s')", command)
cfgfile = FileHandler("save", "config").retfile
if not cfgfile:
return
cfg = self.cli_opts.get_option_values(command)
cfgfile.write(self.serializer.marshal(cfg))
cfgfile.close()
self.add_to_recent(cfgfile.name, command)
logger.debug("Saved config: (command: '%s', cfgfile: '%s')", command, cfgfile)
def add_to_recent(self, filename, command):
""" Add to recent files """
recent_filename = os.path.join(self.pathcache, ".recent.json")
logger.debug("Adding to recent files '%s': (%s, %s)", recent_filename, filename, command)
with open(recent_filename, "rb") as inp:
recent_files = self.serializer.unmarshal(inp.read().decode("utf-8"))
logger.debug("Initial recent files: %s", recent_files)
filenames = [recent[0] for recent in recent_files]
if filename in filenames:
idx = filenames.index(filename)
del recent_files[idx]
recent_files.insert(0, (filename, command))
recent_files = recent_files[:20]
logger.debug("Final recent files: %s", recent_files)
recent_json = self.serializer.marshal(recent_files)
with open(recent_filename, "wb") as out:
out.write(recent_json.encode("utf-8"))