#!/usr/bin/env python3
#

# Known bugs:
# - Skip offset and Invert skip aren't grayed out when Field skip is set back to 0
#   - solution: trace_add()
# - TAB navigation is flawed

# TODO: gray out Stroke color with Stroke width reset to 0
# TODO: more importantly: reactivate widgets on non-zero value after previous 0

# TODO: add a few warnings such as about unknown type < user typo

# TODO: tart up the help text (bold headers for example)

# TODO: bonus window help button: show BONUSTEXT

# TODO: (complicated) make changes in presets (addition, deletion) show immediately


###################################################################################################
# TODO: update this:
# Behavior on exit:
#
# Ok
#  Save settings as presets?
#  Yes			save (if necessary) and exit
#   (no changes)	exit
#   (changes)
# -------------------------------------------------------------------------------------------------
# Alt: Save prests button
# -------------------------------------------------------------------------------------------------
#   Presets		enter a name
#    OK		write presets and exit
#    Cancel		exit
# -------------------------------------------------------------------------------------------------
#     Message: Saving settings as cf.timestamp
#       OK             save and exit
#       Cancel         exit
# -------------------------------------------------------------------------------------------------
#  No			exit
#  Cancel		stay in prog
#
# Cancel
#  Exit RGBScript configuration?
#  OK			exit
#  Cancel		stay in prog
###################################################################################################


import tkinter as tk
from tkinter import messagebox
from tkinter import filedialog as fd

import os, sys

# Paths are in principle relative to the CWD.
CWD = os.getcwd()

# RGBScript will read this config if import of this GUI module succeeds.
# If it doesn't, rgbscript.cf is read instead.
CONFIGFILE = 'rgbscriptgui.cf'

# Location of presets files.
PRESETSDIR = 'rgbscript-presets'


# ==================================================================================================
# ==================================================================================================

# SPECIFIC SECTION

# GLOBALS

# MAIN SETTINGS

WINDOWSIZE = '734x528+40+40'

# Show frame borders?
FRAMEBORDERS = 0

# Object dictionaries.
# These are merely containers and can be safely used for both the main and subwindows.

# Set by main/subwindow code and by makewidgetframe(), config: setframeweights(), read by setupwidgets()
# : .
FRAMEDICT = {}

# Set by inittkvars(), read by manageconfigdict()
# : 
TKVARDICT = {}

# Set by setupwidgets(), config: configwidgets()
# : 
TKOBJDICT = {}

# For radiobuttons
RBOBJDICT = {}

# Configuration settings

# Set by initdefaultsdict(), passed to settkvars() by restoredefaults(), onpresetselect()
# Simple ': ' pairs retrieved from *CONFIGDICT
DEFAULTSDICT = {}

# Settings read from from the preset file. Set by readpresets()
RECONFIGDICT = {}

# Configuration dictionary for all settings: root plus subwindows.
#  =  pairs from this dict are written to the config file and presets files.
CONFIGDICT = {}

#
HELPTEXT = """RGBScript Configuration Help

Setting:                   Input:                Description:

Color set                                        defines the set of colors generated

Field settings
Fields                     integer               number of fields to be printed; 0 means 'auto'
Field height               integer               field height; 0 means 'auto'
Color step                 integer               step by which color values are in/decremented
Turnaround                 integer               limits the number of color steps

Page settings
Margin                     integer               width of the page margins
Show background                                  adds a background layer below the color layer
Background color           hex color code        default is 0xffffff
Fix striping                                     suppresses stripes in the output

Layout settings
Separation                 integer               field separation in pixels; default is 0
Field skip                 integer               number of fields to skip; default is 0
Skip offset                integer               with field skip: position to start showing fields from
Invert skip                                      inverts the skipping: only the 'skipped' fields are shown

Color settings
Opacity                    float                 value between 0 and 1; default is 1 (no transparency)
Invert colors                                    inverts the colors; does not affect the background color
RBG output                                       turns R-G-B output into R-B-G output
Shuffle colors                                   reshuffles the color list for every run
Print values                                     prints a list of geberated color values

Rotate settings 
huerotation                integer               color rotation in degrees; default is 30
rotatecolor                hex color code        color to rotate from

Start color                                      color to start from; default is red

Misc
Color file                                       loads the color set from a color file
Bonus code                                       bonus code settings (new window)
"""

# --------------------------------------------------------------------------------------------------

BONUSTEXT = """Bonus code settings

Setting:                   Input:                 Description:

showmesquares                                     print squares
showmerectangles                                  print rectangles
showmecircles                                     print circles
showmetriangles                                   print triangles
objectsize                 integer                object size; max size in combination with minobjectsize
minobjectsize              integer                min object size; 0 renders a fixed
objectstrokewidth          integer                stroke width in pixels
objectstrokecolor          color name             stroke color: HTML color name, no hex code (!)
objectopacity              integer                object opacity (1 means transparency at all)
spillover                                         allow objects to partly exceed page borders?
"""


# Window layout
#
#  -------------------------------------------------------------------------------------
# | 0    Color set                                                                      |
#  -------------------------------------------------------------------------------------
# | 1.0  Fields              | 2.1  Page               | 2.2  Layout                    |
# |                          |                         |                                |
#  -------------------------------------------------------------------------------------
# | 2.0  Colors              | 3.1  Rotate             | 3.2  Start color               |
# |                          |                         |                                |
# |                           -----------------------------------------------------------
# | [3]                      | 4.1  Misc                                                |
#  -------------------------------------------------------------------------------------
# | 4   Presets                                                                         |
#  -------------------------------------------------------------------------------------
# | 5   Help                                                                Ok  Cancel  |
#  -------------------------------------------------------------------------------------
#
# --------------------------------------------------------------------------------------------------
#
# Frames
#
# (row numbers differ from those in the code because of inserted separator frames)
#
# NOTE: THE FOLLOWING IS A LIST AND NOT A DICTIONARY.
# The use of colons can be confusing.
# Frame definitions for grid(). The entire window layout is defined here. 
# Syntax:
# :	
# :	::
# :		[.[.]]
# :		h	(h for horizontal)
# :		v	(v for vertical)
# Column is optional; default is 0, which means that 2 is the same as 2.0
# (the latter is probably clearer)
# Rowspan and columnspan are optional; default rowspan/columnspan is 1
# Optional rowspan and/or colspan may appear in an arbitrary order
# Example: 'frame_misc: frame_root: 6.1.h3',
# Will be written as: row=6, column=1, columnspan=3
ROOTFRAMESLIST = [

#  							
# Row 0
  'frame_root:  		root:			:			0.0',
  'frame_sets:			frame_root:		Color sets:		0.0.h3',
  'frame_sets_widgets:		frame_sets:		:			1.0.h3',		# relative to row 0: frame_sets

# Row 1
  'frame_field:		frame_root:		Field settings:	1.0',
  'frame_field_widgets:	frame_field:		:			1.0',
  'frame_page:	        	frame_root:		Page settings:		1.1',
  'frame_page_widgets:		frame_page:		:			1.0',
  'frame_layout:		frame_root:		Layout settings:	1.2',
  'frame_layout_widgets:	frame_layout:		:			1.0',

# Rows 2/3
  'frame_color:		frame_root:		Color settings:	2.0.v2',
  'frame_color_widgets:	frame_color:		:			1.0',
  'frame_rotate:		frame_root:		Rotate settings:	2.1',
  'frame_rotate_widgets:	frame_rotate:		:			1.0',
  'frame_start:     		frame_root:		Start color:		2.2',
  'frame_start_widgets:	frame_start:		:			1.0',
  'frame_misc:	        	frame_root:		Misc:			3.1.h2',
  'frame_misc_widgets:		frame_misc:		:			0.1',
  'frame_colorfile:		frame_misc_widgets:	:			0.0',
  'frame_bonus:		frame_misc_widgets:	:			0.1',

# Row 4
  'frame_presets:		frame_root:		Presets:		4.0.h3',
  'frame_presets_widgets:	frame_presets:		:			0.1.h3',

# Row 5
  'frame_help:	        	frame_root:		:			5.0',
  'frame_unused:	      	frame_root:		:			5.1',
  'frame_ok:	        	frame_root:		:			5.2',

  ]

# Frame weights, a rather inexact property of grids
# : ':::
# : r (row), c (column)
# : row or columm number (0..n-1)
# Example: 'frame_root0': 'frame_root:r:0:1'
# Becomes: root.grid_configure(0, weight=1)
# There is little code gain in this dict, it just keeps things central
ROOTWEIGHTDICT = {

# 						
  'root_row0':			'frame_root:		r:	0:		1',
  'root_row1':			'frame_root:		r:	1:		1',
  'root_row2':			'frame_root:		r:	2:		1',
  'root_row3':			'frame_root:		r:	3:		1',
  'root_row4':			'frame_root:		r:	4:		1',
  'root_row5':			'frame_root:		r:	5:		1',
  'root_row4_col0':		'frame_root:		c:	0:		1',
  'root_row5_col0':		'frame_ok:		c:	0:		0',
  'root_row5_col1':		'frame_ok:		c:	1:		1',
  'root_row5_col2':	      	'frame_ok:		c:	0:		1',

  }

# --------------------------------------------------------------------------------------------------
#
# Widgets
#
# A distinction is made between 'config' amd 'para' widgets. The latter are widgets like OK buttons,
# which play no role in the configuration being generated. Keeping the twi types separated isn't
# strictly necessary, but it may serve to avoid ambiguities. Here, in particular, it makes no sense
# to assign a default value to an OK or Cancel button for example.
#
# Names of widgets and their properties; these correspond directly with config var names
# : ';;;>;;[,, ..[;			parent frame
# 			h (horizontal), v (vertical): how to arrange widgets within a frame
# 			the text to display on the widget
# 			widget type: cb (checkbutton), of (openfile), en (entry), rb (radiobutton)
#				- button (bt) and menubutton (mb) never appear in ROOTCONFIGDICT
# 			variable type: i (int),  f (float), b (bool), s {string)
# 			initial (default) value
#				- can be a comma-separated list (with radiobutton)
#				- in that case, first value is the default
# 			horizontal padding of the widget; default is 10
# All values except for bools are strings as far as this part of the code is concerned
# - only turnaround is in fact set dynamically: 0xff/colorstep
# - with the default colorstep of 0x33, this equals 5, which is good enough for this purpose
ROOTCONFIGDICT = {

# 													
  'colorset':	        	'frame_sets_widgets;		h;		rb;		PlainRGB,\
												Hybrid,\
												LightRGB,\
												DarkMYC,\
												DarkRGB,\
												Web,\
												Rotate;		s;		plainrgb,\
																	hybrid,\
																	lightrgb,\
																	darkmyc,\
																	darkrgb,\
																	web,\
																	rotate',

  'fieldnum':			'frame_field_widgets;		v;		en;		Fields:;		i;		0',
  'fieldheight':		'frame_field_widgets;		v;		en;		Field height:;		i;		0',
  'colorstep':	        	'frame_field_widgets;		v;		en;		Color step:;		i;		0x33',
  'turnaround':		'frame_field_widgets;		v;		en;		Turnaround;		i;		5',

  'pagemargin':		'frame_page_widgets;		v;		en;		Margin:;		i;		0',
  'showbackground':		'frame_page_widgets;		v;		cb;		Background;		b;		no;		4',
  'backgroundcolor':		'frame_page_widgets;		v;		en;		Background color;	i;		0xffffff',
  'fixstriping':		'frame_page_widgets;		v;		cb;		Fix striping;		b;		no;		4',

  'fieldsep':	        	'frame_layout_widgets;		v;		en;		Field separation;	i;		0',
  'fieldskip':			'frame_layout_widgets;		v;		en;		Field skip;		i;		0',
  'skipoffset':		'frame_layout_widgets;		v;		en;		Skip offset;		i;		0',
  'invertskip':		'frame_layout_widgets;		v;		cb;		Invert skip;		b;		no;		4',

  'fieldopacity':		'frame_color_widgets;		v;		en;		Opacity;		f;		1',
  'invertcolors':		'frame_color_widgets;		v;		cb;		Invert colors;		b;		no',
  'rbgoutput':			'frame_color_widgets;		v;		cb;		RBG output;		b;		no',
  'shufflecolors':		'frame_color_widgets;		v;		cb;		Shuffle colors;	b;		no',
  'printvalues':		'frame_color_widgets;		v;		cb;		Print values;		b;		no',

  'huerotation':		'frame_rotate_widgets;		v;		en;		Rotation;		i;		30',
  'rotatecolor':		'frame_rotate_widgets;		v;		en;		Color:;		s;',

  'startcolor':	        'frame_start_widgets;		v;		rb;		Red,\
												Green,\
												Blue;			s;		red,\
																	green,\
																	blue',

  'colorfile':			'frame_colorfile;		h;		fs;		Color file;		s;',

  }

# Same format: widgets which do not correspond with config variables
# Usually buttons and menubuttons
# 		bt (button), mb (menubutton)
# Minor TODO:  as above, but with orientation
ROOTPARADICT = {

# 													
  'bonus':			'frame_bonus;			h;		bt;		Bonus code;		;		;		0',
  'presets':			'frame_presets_widgets;	h;		mb;		Select presets;	;		;		20',
  'savepresets':		'frame_presets_widgets;	h;		bt;		Save presets;		;		;		20',
  'defaultpresets':		'frame_presets_widgets;	h;		bt;		Restore defaults;	;		;		20',
  'deletepresets':		'frame_presets_widgets;	h;		bt;		Delete presets;	;		;		20',
  'help':			'frame_help;			h;		bt;		Help;			;		;		',
  'ok':			'frame_ok;			h;		bt;		OK;			;		;		4.E',
  'cancel':			'frame_ok;			h;		bt;		Cancel;		;		;		4.E',

  }

# Concatenate both dictionaries above into a new one
# This one is used for creating widgets, but not for handling RGBScript configuration
ROOTWIDGETSDICT = dict(ROOTCONFIGDICT)
ROOTWIDGETSDICT.update(ROOTPARADICT)

# --------------------------------------------------------------------------------------------------

# Widget dependencies

# Widgets to be disabled on startup
ROOTDISABLED = [
  'huerotation',
  'rotatecolor',
  'backgroundcolor',
  'skipoffset',
  'invertskip',
  ]


###################################################################################################
###################################################################################################
# IN THE CASE OF CONFLICTING STATES PRESENT, ALL RADIO BUTTUNS MUST BE LISTED
###################################################################################################
###################################################################################################
# Widgets to be toggled, depending on the state of some other widget
# Parent widget: the widget on which other widgets
# Dependent widgets: comma-separated list of names
# : |-,...]
# : enable; -								
# Row 0
  'frame_bonuswindow: 			bonuswindow:		:			0.0',
  'frame_bwin_main: 			frame_bonuswindow:	:			0.0',
  'frame_bwin_shapes:			frame_bwin_main:	Shapes:		0.0',
  'frame_bwin_shapes_widgets:		frame_bwin_shapes:	:			1.0',
  'frame_bwin_properties:		frame_bwin_main:	Properties:		0.1',
  'frame_bwin_properties_widgets:	frame_bwin_properties:	:			1.0',
  'frame_bwin_ok:	        	frame_bonuswindow:	:			2.0',
  ]

# Weight dict for bonus window frames
BONUSWEIGHTDICT = {
# 							
  'bwin_row0':			'frame_bonuswindow:	r:		0:		1',
  'bwin_row1':			'frame_bonuswindow:	r:		1:		1',
  'bwin_row2':			'frame_bonuswindow:	r:		2:		1',
  }

# --------------------------------------------------------------------------------------------------
#
# Widgets
#
# Widget dicts for bonus window
BONUSCONFIGDICT = {
# 														
#  'blah':			'frame_bwin_widgets;			h;		bt;		Blah;			;		;',
  'showmesquares':		'frame_bwin_shapes_widgets;		v;		cb;		Squares;		b;		no;',
  'showmerectangles':		'frame_bwin_shapes_widgets;		v;		cb;		Rectangles;		b;		no;',
  'showmecircles':		'frame_bwin_shapes_widgets;		v;		cb;		Circles;		b;		no;',
  'showmetriangles':		'frame_bwin_shapes_widgets;		v;		cb;		Triangles;		b;		no;',

  'objectsize':		'frame_bwin_properties_widgets;	v;		en;		Size (max size);	i;		10;',
  'minobjectsize':		'frame_bwin_properties_widgets;	v;		en;		Min size;		i;		0;',
  'objectstrokewidth':		'frame_bwin_properties_widgets;	v;		en;		Stroke width;		i;		0;',
  'objectstrokecolor':		'frame_bwin_properties_widgets;	v;		en;		Stroke color;		s;		white;',
  'objectopacity':		'frame_bwin_properties_widgets;	v;		en;		Opacity;		f;		1;',
  'spillover':			'frame_bwin_properties_widgets;	v;		cb;		Spillover;		b;		no;',
  }

BONUSPARADICT = {
# 														
  'bwin_ok':			'frame_bwin_ok;			h;		bt;		OK;			b;		no;',
  }

BONUSWIDGETSDICT = dict(BONUSCONFIGDICT)
BONUSWIDGETSDICT.update(BONUSPARADICT)

# --------------------------------------------------------------------------------------------------
#
# Widget dependencies
#

# Widgets disabled on startup
BONUSDISABLED = [

  'objectstrokecolor',

  ]

# Widget enable/disable dependencies
BONUSENABLEDICT = {

  'objectstrokewidth': 'objectstrokecolor',

  }


# ==================================================================================================
# ==================================================================================================
# ==================================================================================================

# SPECIFIC FUNCTIONS

# These apply to the configuration and operation of a specific root window

# This function binds specific actions to widgets
def wconfigspecifics(wname, wtype, var=None, enabledict=ROOTENABLEDICT, disabledlist=ROOTDISABLED):

  global TKOBJDICT			# apparently not needed

  wobj = TKOBJDICT[wname]

  if (wtype == 'en'):

    if (wname == 'fieldskip'):

      # Duplicate (configwidgets())
      #wobj.configure(command=lambda: togglefswidgets(wname, enabledict=enabledict))

      ##############################################################################################
      ##############################################################################################
      # TODO: won't do the trick
      wobj.bind('', lambda event: togglefswidgets(wname, enabledict=enabledict))
      ##############################################################################################
      ##############################################################################################

  elif (wtype == 'bt'):

    if (wname == 'bonus'):

      wobj.configure(command=onbonusbutton)
      wobj.bind('', onbonusbutton)

    elif (wname == 'savepresets'):

      wobj.configure(command=savepresets)
      wobj.bind('', savepresets)

    elif (wname == 'defaultpresets'):

      wobj.configure(command=restoredefaults)
      wobj.bind('', restoredefaults)

    elif (wname == 'deletepresets'):

      wobj.configure(command=removepresetspaths)
      wobj.bind('', removepresetspaths)

    elif (wname == 'help'):

      wobj.configure(command=onhelpbutton)
      wobj.bind('', onhelpbutton)

    elif (wname == 'ok'):

      wobj.configure(command=onokbutton)
      wobj.bind('', onokbutton)

    elif (wname == 'cancel'):

      wobj.configure(command=oncancelbutton)
      wobj.bind('', oncancelbutton)

    # **** Odd duckling: subwindow ************
    elif (wname == 'bwin_ok'):

      wobj.configure(command=onbonusokbutton)
      wobj.bind('', onbonusokbutton)
    # *****************************************

  elif (wtype == 'fs'):

    if (wname == 'colorfile'):

      wobj.configure(command=selectcolorfile)
      wobj.bind('', selectcolorfile)

  elif (wtype == 'mb'):

    if (wname == 'presets'):

      setpresets(wname, wobj)
      wobj.configure(takefocus=True)			# it still won't show any signs of focus, but space bar / arrow keys work

  if wname in disabledlist:
    wobj.configure(state=tk.DISABLED)

  return()


# Tkinter Open File Dialog https://www.pythontutorial.net/tkinter/tkinter-open-file-dialog/
# File selector for color file (e.g. colors.txt)
def selectcolorfile(event=None):
  filetypes = (('Text files', '*.txt'), ('All files', '*.*'))
  rgbfile = fd.askopenfilename(title='Color file', initialdir=CWD, filetypes=filetypes)
  # MAYBE
  #tk.messagebox.showinfo(title='Color file', message=filename)
  return(rgbfile)


def onhelpbutton(event=None):
  textwindow(1200, 1200, 'RGBScript Config Help', HELPTEXT)
  return()


def byebye():
  if messagebox.askokcancel("Exit", "Exit RGBScript configuration?"):
    root.after(200, root.destroy)


# Handle OK: write config if changes were made
def onokbutton(event=None):
  manageconfigdict()
  writeconfig(CONFIGFILE)
  root.after(200, root.destroy)


# Handle Cancel: close window
def oncancelbutton(event=None):
  byebye()


# ==================================================================================================
# ==================================================================================================

# GENERAL SECTION


# ==================================================================================================

# FUNCTION DEFINITIONS

# --------------------------------------------------------------------------------------------------

# GENERAL/MISC

# Stuff that doesn't go elswhere


####################################################################################################
####################################################################################################
# TODO: bold headers
def textwindow(xsize, ysize, ttl, txt):
  nw = tk.Toplevel(root)
  nw.title(ttl)
  nw.geometry("%sx%s" % (xsize, ysize))
  #tk.Label(nw, text = '
' + txt + '
', wraplength=xsize, justify="left").pack()
  textbox = tk.Text(nw)
  textbox.configure(width=xsize, height=ysize, padx=100, pady=100, borderwidth=4, relief='groove', background='white')
  textbox.insert('1.0', txt)
  #text.insert(END, "Bye Bye.....")		# append
  textbox.pack()
  tk.Button(nw, text="OK", command=nw.destroy).pack()
####################################################################################################
####################################################################################################


def logfatalerror(txt):

  print('Fatal error: %s' % txt)
  root.after(200, root.destroy)


def logerror(txt):

  print('Error: %s' % txt)

  return()


def logwarning(txt):

  print('Warning: %s' % txt)

  return()


def log(txt):

  print('%s' % txt)

  return()


def yesno(val):

  if (val == 0): return('no')

  return('yes')


def onezero(val):

  if (val == 'no'): return(0)

  return(1)


def firstvalue(val):

  vlist = splitstr(val, ',')
  first = vlist[0].strip()

  return(first)


def hasindex(lst, index):

  try:
    lst[index]
    return(True)
  except IndexError:
    return(False)


def splitstr(string, sep):

  lst = string.split(sep)

  return(lst)


# Strip all items of a list
def striplistitems(lst):

  stripped = []

  for x in lst:
    y = x.strip()
    stripped.append(y)

  return(stripped)


def askconfirm(ttext, qtext):

  rv = messagebox.askyesno(title=ttext, message=qtext)

  return(rv)


def showmessage(ttext, mtext):

  tk.messagebox.showinfo(title=ttext, message=mtext)

  return()


def showwarning(ttext, wtext):

  tk.messagebox.showwarning(title=ttext, message=wtext)

  return()


def showerror(ttext, etext):

  tk.messagebox.showwarning(title=ttext, message=etext)

  return()


# --------------------------------------------------------------------------------------------------

# FRAMES

# Set up frames within a window.
# Not all args in these functions are necessarily used, but they can be useful for logging and debugging


# Make a widget frame. Very similar in structure to the print*() functions in TKINTER WIDGET OBJECTS.
# 'parent' is the parent frame name, 'pobj' is the parent frame object.
def makewidgetframe(frame, parent, pobj, row, col, rspan, cspan):

  global FRAMEDICT			# not necessary (??)

  if (FRAMEBORDERS == 1):
    FRAMEDICT[frame] = tk.Frame(pobj, bd=0, relief="sunken", highlightbackground="blue", highlightthickness=2)
  else:
    FRAMEDICT[frame] = tk.Frame(pobj, bd=0, relief="sunken")

  FRAMEDICT[frame].grid(row=row, column=col, rowspan=rspan, columnspan=cspan, sticky="nsew", padx=2, pady=2)

  return()


# A frentry is an entry from the frames list
# The frame name frname is the first item in list
def getframename(frentry):

  frlist = splitstr(frentry, ':')
  frlist = striplistitems(frlist)
  frname = frlist[0]

  return(frname)


# Get the first matching entry from  the ROOTFRAMESLIST _list_
# Looks for ':'
def getframedef(frname, framelist=ROOTFRAMESLIST):

  listentry = ''

  for listentry in framelist:
    #frdfn = frdfn.replace(' ', '').replace('	', '')
    if listentry.startswith('%s:' % frname):
      break

  return(listentry)


def buildframe(frname, parent='', framelist=ROOTFRAMESLIST):

							# 				
  frdef = getframedef(frname, framelist)		# 'frame_blah:  frame_root:	Blah:	6.1.h2'
  frlist = splitstr(frdef, ':')
  frlist = striplistitems(frlist)			# ['frame_blah','frame_root','Blah','6.1.h2']

  # : [: ]
  pname = frlist[1]					# frame_root
  frargs = frlist[3]					# 6.1.h2
  frlayout = frargs.split('.')			# 6 1 h2

  # Non-root parent should have already been created by this same routine
  # Expects a root or subwindow object to be present in FRAMEDICT, placed there by main code
  pobj = FRAMEDICT[pname]

  # Row, column, span string
  frcol = '0'
  frspanstr = ', '					#
  if hasindex(frlayout, 2):
    frspanstr = frlayout[2].strip()			# h2
  if hasindex(frlayout, 1):
    if ('h' in frlayout[1] or 'v' in frlayout[1]):
      frspanstr = frlayout[1].strip()			# h2
    else:
      frcol = frlayout[1].strip()			# 1
  frrow = frlayout[0]					# 6

  # Row span, column span
  frrspan = 1
  frcspan = 1
  while ('h' in frspanstr or 'v' in frspanstr):
    if (frspanstr.startswith('h')):
      frcspan = frspanstr[1].strip()
      frspanstr = frspanstr[2:]
    if (frspanstr.startswith('v')):
      frrspan = frspanstr[1].strip()
      frspanstr = frspanstr[2:]

#  if frname.startswith('frame_sep'):
#    makeseparator(frname, parent, frrow, frcol, frrspan, frcspan)
#  else:

  # Create frame and add frame name as key to FRAMEDICT with object as value.
  makewidgetframe(frname, pname, pobj, frrow, frcol, frrspan, frcspan)

  return()


def setframetitle(frname, framelist=ROOTFRAMESLIST):

  frdef = getframedef(frname, framelist=framelist)
  frlist = splitstr(frdef, ':')
  frlist = striplistitems(frlist)
  frparent = frlist[1]
  frtitle = frlist[2]
  frfont = 'Arial 12 bold'
  frxpad = 10

  if frtitle:		# not all frames have a title
    printlabel(frname, frparent, FRAMEDICT[frname], 0, 0, frtitle, frfont, frxpad)

  return()


# Weights, for what they're worth..
def setframeweights(weightdict=ROOTWEIGHTDICT):

  for key in weightdict:

    val = weightdict[key]
    wtlist = splitstr(val, ':')
    wtlist = striplistitems(wtlist)

    # Parent frame name
    frname = wtlist[0]
    # Frame 'type': row or column
    frtype = wtlist[1]
    # Row or column number
    frgrid = wtlist[2]
    # Weight
    frwt = wtlist[3]

    if (frtype == 'r'):
      FRAMEDICT[frname].grid_rowconfigure(frgrid, weight=frwt)
    elif (frtype == 'c'):
      FRAMEDICT[frname].grid_columnconfigure(frgrid, weight=frwt)

  return()


# --------------------------------------------------------------------------------------------------

# WIDGETS

# Place widgets inside the frames created and initialize them with values.
# widget and variable names are identical. wname: widget context; var: variable context


def getwparent(wname, sdict=ROOTWIDGETSDICT):

  wlist = splitstr(sdict[wname], ';')

  return(wlist[0].strip())


def getwvect(wname, sdict=ROOTWIDGETSDICT):

  wlist = splitstr(sdict[wname], ';')

  return(wlist[1].strip())


def getwtype(wname, sdict=ROOTWIDGETSDICT):

  wlist = splitstr(sdict[wname], ';')

  return(wlist[2].strip())


def getwtext(wname, sdict=ROOTWIDGETSDICT):

  wlist = splitstr(sdict[wname], ';')

  return(wlist[3].strip())


def getvartype(var, sdict=ROOTWIDGETSDICT):

  vlist = splitstr(sdict[var], ';')

  return(vlist[4].strip())


def getvarval(var, sdict=ROOTWIDGETSDICT):

  vlist = splitstr(sdict[var], ';')

  return(vlist[5].strip())


def getwlayout(wname, sdict=ROOTWIDGETSDICT):

  wlist = splitstr(sdict[wname], ';')

  if hasindex(wlist, 6):
    return(wlist[6].strip())

  return()


# print*(): display a widget. 'frobj' is a frame object.
# Not all args are necessarily used, but they can be useful for logging and debugging

def printlabel(name, parent, frobj, row, col, text, font='TkTextFont', xpad=4, orientation='W'):

  lb = tk.Label(frobj, text=text, font=font)
  lb.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=4)

  return(lb)


def printcheckbutton(name, parent, frobj, row, col, text, xpad=4, orientation='W'):

  cb = tk.Checkbutton(frobj, text=' %s' % text)
  cb.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=4)

  return(cb)


def printentry(name, parent, frobj, row, col, text, font='TkTextFont', xpad=4, orientation='W'):

  lb = tk.Label(frobj, text=text, font=font)
  lb.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=4)
  en = tk.Entry(frobj, width=8)
  en.grid(row=row, column=col+1, sticky=orientation, padx=xpad, pady=4)

  return(en)


def printbutton(name, parent, frobj, row, col, text, xpad=4, orientation='W'):

  bt = tk.Button(frobj, text=text)
  bt.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=4)

  return(bt)


def printmenubutton(name, parent, frobj, row, col, text, xpad=4, orientation='W'):

  mb = tk.Menubutton(frobj, text=text, relief='raised', width=14)
  mb.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=10)
  mb.menu = tk.Menu(mb, tearoff=0)
  mb["menu"] = mb.menu

  return(mb)


def printradiobutton(name, parent, frobj, row, col, text, vect, xpad=4, orientation='W'):

  textlist = splitstr(text, ',')
  textlist = striplistitems(textlist)
  rblist = []
  count = 0

  for txt in textlist:
    rb = tk.Radiobutton(frobj)
    rb.configure(text=txt)
    rb.grid(row=row, column=col, sticky=orientation, padx=xpad, pady=4)
    if (vect == 'v'):
      row += 1
    elif (vect == 'h'):
      col += 1
    rblist.append(rb)
    count += 1

  return(rblist)


def setupwidgets(widgetdict=ROOTWIDGETSDICT):

  global TKOBJDICT, RBOBJDICT

  wcount = 0
  wfrobj = ''

  for w in widgetdict:

    # Get config var attributes from  the widgets dict
    wparent = getwparent(w, sdict=widgetdict)
    wvect = getwvect(w, sdict=widgetdict)
    wtype = getwtype(w, sdict=widgetdict)
    wtext = getwtext(w, sdict=widgetdict)
    wlayout = getwlayout(w, sdict=widgetdict)
    wfont = 'TkTextFont'

    oldwfrobj = wfrobj
    wfrobj = FRAMEDICT[wparent]
    if (oldwfrobj != wfrobj):
      wcount = 0

    if (wlayout):
      wllist = splitstr(wlayout, '.')
      wllist = striplistitems(wllist)
      wxpad = wllist[0]
      if hasindex(wllist, 1):
        worientation = wllist[1]
    else:
      wxpad = 10
      worientation = 'W'

    # Rows and columns are relative to the frame
    if (wvect == 'v'):
      row, col = wcount, 0
    elif (wvect == 'h'):
      row, col = 0, wcount

    if (wtype == 'lb'):

      obj = printlabel(w, wparent, wfrobj, row, col, wtext, wfont, xpad=wxpad)
      TKOBJDICT[w] = obj
      obj = printlabel(w, wparent, wfrobj, row, col, wtext, wfont, xpad=wxpad)
      TKOBJDICT[w] = obj

    elif (wtype == 'en'):

      obj = printentry(w, wparent, wfrobj, row, col, wtext, wfont, xpad=wxpad)
      TKOBJDICT[w] = obj

    elif (wtype == 'cb'):

      obj = printcheckbutton(w, wparent, wfrobj, row, col, wtext, xpad=wxpad)
      TKOBJDICT[w] = obj

    elif (wtype == 'bt'):

      obj = printbutton(w, wparent, wfrobj, row, col, wtext, orientation=worientation)
      TKOBJDICT[w] = obj

    elif (wtype == 'fs'):

      obj = printbutton(w, wparent, wfrobj, row, col, wtext, xpad=10)
      TKOBJDICT[w] = obj

    elif (wtype == 'mb'):

      obj = printmenubutton(w, wparent, wfrobj, row, col, wtext, xpad=20)
      TKOBJDICT[w] = obj

    elif (wtype == 'rb'):

      objlist = printradiobutton(w, wparent, wfrobj, row, col, wtext, wvect, xpad=wxpad)
      TKOBJDICT[w] = objlist

      for i in range(len(objlist)):

        subw = w + str(i)
        RBOBJDICT[subw] = objlist[i]

    wcount += 1

  return()


# Create Tkinter Tntvar/Stringvar and store it in TKVARDICT.
def inittkvar(wname, val, vtype='str'):

  global TKVARDICT		# Not really needed apparently

  if (vtype == 'str'):

    objv = tk.StringVar(None, val)
    TKVARDICT[wname] = objv

  elif (vtype == 'int'):

    objv = tk.IntVar(None, val)
    TKVARDICT[wname] = objv

  return(objv)


# Initoalize Tkinter objects.
def inittkvars(widgetdict=ROOTWIDGETSDICT):

  for w in widgetdict:

    # Get config var attributes from the widgets dict
    wtype = getwtype(w, sdict=widgetdict)
    vtype = getvartype(w, sdict=widgetdict)
    vval = getvarval(w, sdict=widgetdict)

    # Checkboxes write a boolean value to an IntVar (0|1)
    if (vtype == 'b'):

      vval = onezero(vval)
      intv = inittkvar(w, vval, 'int')

    else:

      # Radiobutton: first value is the default
      if (wtype == 'rb'):
        vval = firstvalue(vval)

      strv = inittkvar(w, vval)

  return()


# Configure a Tkinter object.
def configobj(wname, vtype='str', disabledlist=ROOTDISABLED):

  obj = TKOBJDICT[wname]

  if (vtype == 'str'):

    obj.configure(textvariable=TKVARDICT[wname])

  elif (vtype == 'int'):

    obj.configure(variable=TKVARDICT[wname])

  if wname in disabledlist:

    obj.configure(state=tk.DISABLED)

  return()


# Configure widgets.
def configwidgets(cfaction='config', widgetdict=ROOTWIDGETSDICT, enabledict=ROOTENABLEDICT, disabledlist=ROOTDISABLED):

  for w in widgetdict:

    wtype = getwtype(w, sdict=widgetdict)
    vval = getvarval(w, sdict=widgetdict)

    # Process according to object type
    if (wtype == 'lb'):				# Label

      configobj(w, disabledlist=disabledlist)

    elif (wtype == 'en'):				# Entry

      configobj(w, disabledlist=disabledlist)

      if w in enabledict:

        obj = TKOBJDICT[w]
        ############################################################################################
        ############################################################################################
        # TODO: will do the trick with TAB, but not a second time
        obj.configure(validate='focusout', validatecommand=lambda x = w: togglefswidgets(x, enabledict=enabledict))
        ############################################################################################
        ############################################################################################

    elif (wtype == 'cb'):				# Checkbutton

      configobj(w, 'int', disabledlist=disabledlist)

      obj = TKOBJDICT[w]
      obj.configure(command=lambda x = w: togglecbwidgets(x, enabledict=enabledict))

    elif (wtype == 'rb'):				# Radiobutton

      objlist = TKOBJDICT[w]

      # Radio button objects are indexed, e.g. RBOBJDICT[colorset0], i.e. one entry per button.
      # The Tkinter variable is named after the widget, e.g. TKVARDICT[colorset], i.e. a single
      # Tkinter var bound to all buttons.
      for i in range(len(objlist)):
        vlist = splitstr(vval, ',')			# for radiobuttons value is a comma-separated list
        vlist = striplistitems(vlist)
        thisval = vlist[i]
        subw = w + str(i)				# entry in RBOBJDICT, e.g. colorset0, colorset1, ..
        enablename = w + ':' + thisval		# entry in *ENABLEDICT, e.g. colorset:plainrgb, colorset:hybrid, ..
        obj = RBOBJDICT[subw]
        obj.configure(variable=TKVARDICT[w], value=thisval, command=lambda x = enablename: togglerbwidgets(x, widgetdict=widgetdict, enabledict=enabledict))

    else:

      # Specifics (commands etc.) moved out of this function
      # Button ('bt'), menubutton, ('mb'), fileselect ('fs') require non-standard treatment

      wconfigspecifics(w, wtype, enabledict=enabledict, disabledlist=disabledlist)

  return()


def gettkval(wname):
  objv = TKVARDICT[wname].get()
  return(objv)


# --------------------------------------------------------------------------------------------------

# WIDGET DEPENDENCIES

# What follows is a feast of double and triple negations: if widget A has value X, widgets B
# and C  must either be enabled or disabled. If widget A does not have value X, the behavior
# is consequently inverted. It gets a bit complex with radio buttons.


# Enable a widget.
def enablewidget(wname):
  obj = TKOBJDICT[wname]
  obj.configure(state=tk.NORMAL)
  return()


# Disable a widget.
def disablewidget(wname):
  obj = TKOBJDICT[wname]
  obj.configure(state=tk.DISABLED)
  return()


# Decide whether to enable or disable a widget.
def togglewidgets(wdglist, action='enable'):

  for w in wdglist:

    invert = 0
    if w.startswith('-'):
      w = w[1:]
      invert = 1

    if (action == 'enable'):
      if (invert):
        disablewidget(w)
      else:
        enablewidget(w)
    elif (action == 'disable'):
      if (invert):
        enablewidget(w)
      else:
        disablewidget(w)

  return()


# Enable/disable widgets dependent on fieldskip [entry] value (applies to StringVars).
###################################################################################################
###################################################################################################
# TODO: VALUE IS READ ONLY ONCE ???????????????????????????????????????????????????????????????????

# https://www.codegrepper.com/code-examples/python/how+to+reset+entry+in+tkinter
# “how to reset entry in tkinter” Code Answer
# https://newbedev.com/how-to-reset-entry-in-tkinter-code-example
# how to reset entry in tkinter code example
#
# widget.delete(0, END)
#
# https://coderslegacy.com/python/tkinter-clear-entry/
# Tkinter – Clear Entry box
#
# myentry.delete(0, 'end')		# requires a button
#

###################################################################################################
###################################################################################################
# TODO: respond on 
###################################################################################################
###################################################################################################
# Enable/disable field skip wodgets
def togglefswidgets(wname, enabledict=ROOTENABLEDICT):

  entry = enabledict[wname]
  wdglist = splitstr(entry, ',')
  wdglist = striplistitems(wdglist)

  newvalue = TKVARDICT[wname].get()

  # Alternative to validating input: disable for non-numeric input
  # -------------------- ROUTINE-SPECIFIC ------------------------------
  if (newvalue.isnumeric()):
    if (int(newvalue) > 0):
  # --------------------------------------------------------------------
      togglewidgets(wdglist)
    else:
      togglewidgets(wdglist, 'disable')

  TKVARDICT[wname].set(newvalue)

  return()


###################################################################################################
###################################################################################################
# TODO: CONTAINS CONFIG SPECIFICS ('showbackground')
###################################################################################################
###################################################################################################
# Enable/disable widgets dependent on some checkbutton (applies to IntVars).
def togglecbwidgets(wname, enabledict=ROOTENABLEDICT):

  if wname in enabledict:

    entry = enabledict[wname]
    wdglist = splitstr(entry, ',')
    wdglist = striplistitems(wdglist)

    if (wname == 'showbackground'):

      newvalue = TKVARDICT[wname].get()
      # -------------------- ROUTINE-SPECIFIC --------------------------
      if (newvalue):				# button is checked
      # ----------------------------------------------------------------
        togglewidgets(wdglist)
      else:
        togglewidgets(wdglist, 'disable')

  return()


# Return the combined specs of all inactive buttons.
def togglerbinvert(wname, current, widgetdict=ROOTWIDGETSDICT, enabledict=ROOTENABLEDICT):

  wstr = getvarval(wname, sdict=widgetdict)	# comma-separated list of button names
						# e.g. plainrgb,hybrid,lightrgb,darkmyc,darkrgb,web,rotate
  vlist = splitstr(wstr, ',')
  vlist = striplistitems(vlist)
  vlist.remove(current)			# delete current value from list, e.g. rotate
						# e.g. plainrgb,hybrid,lightrgb,darkmyc,darkrgb,web

  invertlist = []

  # Read the specs for all values except the CURRENT
  for v in vlist:

    dictname = wname + ':' + v		# e.g. colorset:plainrgb

    if dictname in enabledict:		# see if the value has a spec

      entry = enabledict[dictname]
      wdglist = splitstr(entry, ',')
      wdglist = striplistitems(wdglist)

      # This could be done in a single list assignment (??)
      for w in wdglist:

        inv = w

        if not inv in invertlist:		# no duplicates

          invertlist.append(inv)

  return(invertlist)


# Enable/disable widgets dependent on some radio list (applies to StringVar).
# This routine is oblivious to the PREVIOUS selection.
# This is a fairly simple procedure, but with a rather complex implementation.
# Essentially: 'untoggle' everything listed for a particular radio set, then
# toggle what is needed for the active button.
def togglerbwidgets(wstr, widgetdict=ROOTWIDGETSDICT, enabledict=ROOTENABLEDICT):

  # Data for the current (newly pressed, active) button
  wlist = splitstr(wstr, ':')			# e.g. colorset:rotate
  wname = wlist[0].strip()			# colorset
  wval = wlist[1].strip()			# rotate

  wdglist = []

  # see if anything is specified for the active button, e.g. 'colorset:rotate'
  if wstr in enabledict:

    # get the list of toggles for the active button
    activelist = enabledict[wstr]		# E.g. 'huerotation, rotatecolor , -colorstep, -rbgoutput'
    wdglist = splitstr(activelist, ',')
    wdglist = striplistitems(wdglist)		# E.g. ['huerotation', 'rotatecolor', '-colorstep', '-rbgoutput']

  # List of ALL items listed for this widget in the enable dict.
  # These are to be inverted for ALL BUT THE ACTIVE WIDGET.
  # E.g. ['turnaround', 'huerotation', 'rotatecolor', '-colorstep', '-rbgoutput']
  ##################################################################################################
  # THERE MAY BE CONFLICTING ITEMS IN THIS LIST, e.g. 'turnaround, -turnaroud, huerotation, ..'
  ##################################################################################################
  invertlist = togglerbinvert(wname, wval, widgetdict=widgetdict)

  # Negate all these states in a sweeping way. This basically turns everything off.
  # E.g. EFFECTIVELY: ['-turnaround', '-huerotation', '-rotatecolor', 'colorstep', 'rbgoutput']
  togglewidgets(invertlist, 'disable')

  # Turn the specified states on again for the active button
  # E.g. for rotate: ['huerotation, rotatecolor , -colorstep, -rbgoutput']
  # Note that 'turnaround' is left disabled (!)
  togglewidgets(wdglist)

  return()


# --------------------------------------------------------------------------------------------------

# CONFIGURATION SETTINGS

# These functions form the main engine of the script; everything else is basically initialization
# - if a setting matches the default, no need to write it to the config file
# - in addition: if a subwindow is opened for a second time, initialize it with the previous values


# Store initial (default) ': ' pairs read from a config dict in the defaults dict
# DEFAULTSDICT exisis merely for convenience.
def initdefaultsdict(configdict=ROOTCONFIGDICT):

  global DEFAULTSDICT

  for var in configdict:
    val = getvarval(var, sdict=configdict)
    DEFAULTSDICT[var] = val

  return()


# Reinitialze a subwindow if opened more than once. Note that configdict=ROOTCONFIGDICT is a mere
# placeholder, since the root window is never shown twice.
def reinitsubwindow(configdict=ROOTCONFIGDICT):

  for var in configdict:
    if var in CONFIGDICT:
      val = CONFIGDICT[var]
      # The unavoidable yes|no -> 1|0 conversion
      vtype = getvartype(var, sdict=configdict) 
      if (vtype == 'b'): val = onezero(val)
      TKVARDICT[var].set(val)

  return()


# Record non-default config settings (changed by user). Only these settings will be written to the
# new config file. Other settings (defaults) can be considered 'commented out'.
def manageconfigdict(configdict=ROOTCONFIGDICT):

  global CONFIGDICT

  # Needed in order to let caller know changes were made
  rv = 0

  for var in configdict:

    # String values are written python style, in single quotes.
    # Numeric values are written without quotes.
    vtype = getvartype(var, sdict=configdict)
    if (vtype == 'i' or vtype == 'f'):
      quote = ''
    else:
      quote = "'"

    # COMPARE default value from DEFAULTSDICT with current value from TKVARDICT
    # - if the values differ, store them in CONFIGDICT.
    # - if a value has been reset to the default, remove the entry from CONFIGDICT

    default = DEFAULTSDICT[var]		# Same thing: vval = getvarval(var, sdict=configdict)
    # Radio box 'types' have multiple possible values; first is default
    if (',' in default): default = firstvalue(default)

    # TKVARDICT contains Tkinter OBJECTS holding the CURRENT values.
    # Need to convert value to str for comparison.
    curval = str(TKVARDICT[var].get())

    # Convert bools: 0|1 -> no|yes
    if (vtype == 'b'): curval = yesno(int(curval))

    # Add only the vars whose values differ from the defaults to config dict.
    if (curval == default):
      # Delete item if value matches the default.
      if var in CONFIGDICT:
        CONFIGDICT.pop(var)
    else:
      CONFIGDICT[var] = quote + curval + quote
      rv = 1

  return(rv)


# Build the output text and write it to the config file.
# This routine is also used for preset files.
def writeconfig(cpath):

  printtext = ''
  for var in CONFIGDICT:
    printtext = printtext + '
' + var + ' = ' + CONFIGDICT[var]

  try:

    with open(cpath, mode='w') as f:
      f.writelines(printtext + '
')

  except OSError:

    titletext='Error saving configuration'
    warntext = 'Saving configuration to %s failed' % pfile
    showwarning(titletext, warntext)

  return()


# --------------------------------------------------------------------------------------------------

# PRESETS

# Load/save/delete presets.
# To avoid confusion:
# - a presets file is a preset file's basename here, which matches the name of the preset
# - a presets path is the file's absulute pathname


# Set a value to current in TKVARDICT. Basically the same as TKVARDICT[var].set(val) in caller.
# Therefore some extra overhead without much code gain, but consistent and clear.
def settkvar(wname, val):

  global TKVARDICT			# Not needed (???)

  objv = TKVARDICT[wname].set(val)

  return()


# Initialize Tkinter variables (IntVar/StringVar).
# These correspond with a widget's state.
def settkvars(configdict, widgetdict=ROOTWIDGETSDICT):

  for w in configdict:

    if w in widgetdict:

      # Get config var attributes from  the widgets dict
      wtype = getwtype(w, sdict=widgetdict)
      vtype = getvartype(w, sdict=widgetdict)

      vval = configdict[w]

      # Checkboxes write a boolean value to an IntVar (0|1)
      if (vtype == 'b'): vval = onezero(vval)
      if (wtype == 'rb'): vval = firstvalue(vval)

      settkvar(w, vval)

  return()


# Returns a list of preset files (rgbscript.cf.) for display the with Presets menu button.
def getpresetspaths():

  import glob

  if os.path.exists(PRESETSDIR):

    pspaths = []					# list of files

    # Must match filename like: rgbscript.cf.2022-06-06-13:44:23.920467
    #for f in glob.glob('rgbscript.cf.[1-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]*'):
    for f in glob.glob(os.path.join(PRESETSDIR, '*')):
      pspaths.append(f)

  else:

    titletext = 'Error loading presets'
    warntext = 'No presets directory found'
    showwarning(titletext, warntext)

  return(pspaths)


# Retrieve the name part of a presets filename
def getpresetsname(pfile):
  base = os.path.basename(pfile)
  parts = splitstr(base, '.')
  # Only interested in final part
  for p in parts:
    pname = p
  return(pname)


## Returns a timestamp for the presets file to be written.
#def getpresetsfilename():
#
#  from datetime import datetime
#
#  dt = datetime.now()				# e.g. 2022-06-06 13:44:23.920467
#  dt = str(dt).replace(' ', '-')		# e.g. 2022-06-06-13:44:23.920467 (also: what insanely complicated code)
#  pfile = 'rgbscript.cf' + '.' + dt	# rgbscript.cf.
#
#  return(pfile)

# Create the presets ditectory if it doesn't exist.
def makepresetsdir(pdir):

  if not os.path.exists(pdir):
    try:
      os.mkdir(pdir)
    except OSError as e:
      if e.errno != errno.EEXIST:

        titletext='Error creating directory'
        errtext = 'Creating directory %s failed' % pdir
        showerror(titletext, warntext)
        logfatalerror(errtext)

  else:

    if not os.path.isdir(pdir):

      titletext='Error creating directory'
      errtext = 'Non-directory %s exists' % pdir
      showerror(titletext, warntext)
      logfatalerror(errtext)

  return()


# Save current configuration in the form of a presets file stored in the presets directory.
# A presets file is just a ' = ' config file with a special name.
def savepresets():

  write = False

  # No need to ask, user can hit Cancel (maybe in some future version).
  #if asksavepresets():
  if True:

    rv = manageconfigdict()

    if (rv):

      answer = askpresetname()

      if (answer != None):

        # CWD/rgbscript-presets/
        presetsfile = os.path.join(PRESETSDIR, answer)

        if os.path.exists(PRESETSDIR):

          if os.path.exists(presetsfile):

            write = askconfirm('Presets exist', 'Overwrite %s?' % presetsfile)

          else:

            write = True

        else:

          titletext = 'Error saving presets'
          warntext = 'No presets directory present'
          showwarning(titletext, warntext)

    else:

        titletext = 'Error saving presets'
        messagetext = 'Not saving defaults'
        showmessage(titletext, messagetext)

  if (write):
    writeconfig(presetsfile)

  return()


# Populate the Presets menu dynamically. Strange that Tkinter doesn't have some builtin routine for this,
# nor that most tutorials are very explicit about it. Figuring this out was a mjor pain.
def setpresets(wname, mnu):

  presetspathlist = getpresetspaths()
  menudict = {}
  mkeys = []

  # Add key-value pairs to dictionary
  for i in range(len(presetspathlist)):
    key = 'key'+str(i)				# key0, key1, ..
    ########################################
    # ??????????????????????????????????????
    # Why does this not work ??
    #    mkeys.append[key]
    # ??????????????????????????????????????
    ########################################
    temp = {key: presetspathlist[i]}
    menudict.update(temp)

  mkeys = list(menudict.keys())		# ['key0', 'key1', ..]

  # Add entries and values to the menu
  for i in range(len(presetspathlist)):
    # mkey = list(menudict.keys())[i]
    mkey = mkeys[i]
    fname = menudict.get(mkey)
    # add_radiobutton() will display the previously selected menu option
    # alt.: add_command ('anpnymous')
    # getpresetsname() returns only the name (part of the file's basename after last '.')
    lbl = getpresetsname(menudict['key'+str(i)])
    mnu.menu.add_radiobutton(label=lbl, command=lambda x = fname: onpresetselect(x))
    mnu.grid(row=0, column=0, sticky='W', padx=4, pady=4)

  # Add a 'defaults' entry to the menu
  mnu.menu.add_radiobutton(label='defaults', command=restoredefaults)
  mnu.grid(row=0, column=0, sticky='W', padx=4, pady=4)

  return()


# Parse a preset file.
def readpresets(fpath):

  global RECONFIGDICT

  RECONFIGDICT.clear()

  # Read STRINGS from file
  try:
    with open(fpath) as fp:
      lines = fp.readlines()
      for line in lines:
        line = line.rstrip()
        if '=' in line:
          entry = line.split('=')
          var = entry[0].strip()
          val = entry[1].strip()
          val = val.replace("'", "")
          val = val.replace('"', "")
          RECONFIGDICT[var] = firstvalue(val)
  except OSError:
    titletext='Error loading presets'
    warntext = 'Reading presets from %s failed' % presetsfile
    showwarning(titletext, warntext)

  return()


# Ask whether to save current config as presets.
def asksavepresets():

  rv = messagebox.askyesno("Save settings", "Save settings as presets?")

  return(rv)


# Ask user to enter a name for current config to be saved as presets.
def askpresetname():

  from tkinter.simpledialog import askstring

  name = askstring("Save presets", "Name for presets:")

  return(name)

 
# Read the selected presets file.
def onpresetselect(presetspath):

  readpresets(presetspath)		# load settings from the preset file into RECONFIGDICT
###################################################################################################
###################################################################################################
# RESTORE WINDOW CONFIG
# Could be used for re-graying widgets ??
# TODO: def restoreconfig():
  # Reconfiguration is done in a sweeping way: defaults plus values read from the preset file
  settkvars(DEFAULTSDICT)		# back to defaults
  settkvars(RECONFIGDICT)		# read settings from RECONFIGDICT
###################################################################################################
###################################################################################################

  return()


# Ask whether to restore default settings.
def askrestoredefaults():

  rv = messagebox.askyesnocancel("Restore defaults", "Discard current settings?")

  return(rv)


# Restore default settings. What some programs would call a reset.
def restoredefaults():

  if askrestoredefaults():
    settkvars(DEFAULTSDICT)

  return()


# Ask whether to delete presets files.
def askdeletepresets():

  rv = messagebox.askyesnocancel("Delete presets", "Delete presets?")

  return(rv)

# Delete presets files from the presets directory.
def removepresetspaths():

  if askdeletepresets():

    flist = getpresetspaths()

    if flist:

      for f in flist:

        if os.path.isfile(f):

          log('Deleting %s' % os.path.join(CWD, f))

          try:
            os.remove(f)
          except OSError as e:
            logerror("%s - %s." % (e.filename, e.strerror))

  return()


# ************************************************************************************************************************************************************************************************************
# ************************************************************************************************************************************************************************************************************
# ************************************************************************************************************************************************************************************************************

# BONUS CODE

# Handle OK: write config if changes were made
def onbonusokbutton(event=None):
  manageconfigdict(configdict=BONUSCONFIGDICT)
  bonuswindow.after(200, bonuswindow.destroy)

#def wbonusconfig(wname, wtype, var=None):

#  global TKOBJDICT			# apparently not needed

#  wobj = TKOBJDICT[wname]
##  print(wobj)

#  if (wtype == 'bt'):

#    if (wname == 'bwin_ok'):

#      wobj.configure(command=onbonusokbutton)
#      wobj.bind('', onbonusokbutton)

bonuswindow = None
BWINSHOWN = 0

def onbonusbutton(event=None):

  global BWINSHOWN, bonuswindow

  # Window
  bonuswindow = tk.Toplevel(root)
  bonuswindow.title('Bonus code settings')
  bonuswindow.geometry(BONUSWINDOWSIZE)
  FRAMEDICT['bonuswindow'] = bonuswindow
  bonuswindow.resizable(False, False)

  # Defaults
  initdefaultsdict(configdict=BONUSCONFIGDICT)

  # Frames
  for frdfn in BONUSFRAMESLIST:
    frname = getframename(frdfn)
    buildframe(frname, framelist=BONUSFRAMESLIST)
    setframetitle(frname, framelist=BONUSFRAMESLIST)
  setframeweights(weightdict=BONUSWEIGHTDICT)

  # Widgets
  setupwidgets(widgetdict=BONUSWIDGETSDICT)
  inittkvars(widgetdict=BONUSWIDGETSDICT)		# initialize tk vars (default settings)
  configwidgets(widgetdict=BONUSWIDGETSDICT, enabledict=BONUSENABLEDICT, disabledlist=BONUSDISABLED)		# bind tk vars to widgets
  if (BWINSHOWN == 1):
    reinitsubwindow(configdict=BONUSCONFIGDICT)
  BWINSHOWN = 1

  return()


# ************************************************************************************************************************************************************************************************************
# ************************************************************************************************************************************************************************************************************
# ************************************************************************************************************************************************************************************************************


# ==================================================================================================
# ==================================================================================================

# MAIN CODE

if not os.path.exists(PRESETSDIR):
  makepresetsdir(PRESETSDIR)

# Window
root = tk.Tk()
root.title('RGBScript Configuration Editor')
root.geometry(WINDOWSIZE)
FRAMEDICT['root'] = root
#root.resizable(False, False)

# Defaults
###################################################################################################
###################################################################################################
#RECONFIGDICT.clear()			# Just to make sure
###################################################################################################
###################################################################################################
initdefaultsdict()

# Frames
for frdfn in ROOTFRAMESLIST:
  frname = getframename(frdfn)
  buildframe(frname)
  setframetitle(frname)
setframeweights()

# Widgets
setupwidgets()
inittkvars()				# initialize tk vars (default settings)
configwidgets()			# bind tk vars to widgets

# With this, the root window is set up and configured. From here configuration widgets
# and buttons take over.

###################################################################################################
###################################################################################################
#root.protocol("WM_DELETE_WINDOW", byebye)
###################################################################################################
###################################################################################################

root.mainloop()