#!/usr/bin/env python3
#

# Write a configuration file for rgbx-1.0.py.

# Developed against Python 3.8.10

# rgbx-gui version 1.0
#
# Copyright 2022 Joseph Smith
#
# Permission to use, copy, modify, and/or distribute this software for any purpose with or without
# fee is hereby granted, provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.

# Known bugs and shortcomings:
# - Skip offset and Invert skip aren't grayed out when Field skip is set back to 0
#   - solution: trace_add()
# - TAB navigation is flawed
# MAIN SHORTCOMING: THE GUI IS TOO BIG AND CLUMSY.

# Changes:
# - dropped help windows because of hideously ugly Text widget

# -----

# Remote TODOs

# TODO: add warnings such as about unknown type < user typo; alt: testconfig() routine
# TODO: log(): print and write to rgbx-X.Y.log
# TODO: separate GUI definition file(s)


# Impossible TODOs ??

# TODO: gray out Stroke color with Stroke width reset to 0
# TODO: same for Skip offset and Invert skip in relation to Field skip
# TODO: more importantly: reactivate widgets on non-zero value after previous 0
#       - tried command=, validatecommand= and trace() - all failed

# --------------------------------------------------------------------------------------------------
# Q and A secion. 
#
# Q: why not group functions neatly into objects?
# A: because doing so would bring lots of overhead with little or no real gain.
#
# Q: why so many global variables?
# A: because having them snake through the code in the form of function parameters would result in
#    a maintenance nightmare (tried and failed).
# --------------------------------------------------------------------------------------------------


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()

# Config file for this script.
MYCONFIG = 'rgbx-gui-1.0.cf'

# Config file written for RGBX.
CONFIGFILE = 'rgbx-config.txt'

# SIS configuration directory will serve as a central location for storing presets.
# If SISCONFIGDIR is left empty, the directory will be created in the CWD.
PRESETSDIR = 'rgbx-presets'

# Simple Inkscape Scripting configuration directory.
# Where the config file and presets directory live. If left empty, they will be created in the CWD.
# This is also RGBX's idea of the CWD when run from the SIS extension.
SISCONFIGDIR = '~/.config/inkscape/extensions/SimpInkScr'

# Display a roughly 4:3 window by default
# TODO: make 3:4 ('vertical') the default
WINDOWSTYLE = 'standard'

# CONFIG

# Parse configuration file; sets globals directly
def loadconfig(cffile):

  # For maintainers to maintain: expected types CAN be speciified; without, everything returns as str.
  # Lists of (quoted) config var names.
  expectint = []
  expectfloat = []

  # List of config params read from the file.
  cflist = []

  # Read in file contents.
  with open(cffile) as fp:
    lines = fp.readlines()
    for line in lines:
      if '#' in line:
        parts = line.split('#')
        line = parts[0].rstrip()
      else:
        line = line.rstrip()
      if line == '':
        pass
      elif '=' in line:
        entry = line.split('=')
        cfname = entry[0].strip()
        cfval = entry[1].strip()
        cfval = cfval.replace("'", "")
        cfval = cfval.replace('"', "")
        configentry = [cfname, cfval]
        cflist.append(configentry)
  fp.close()

  # Set global variables.
  for i in range(len(cflist)):
    name = cflist[i][0]
    value = cflist[i][1]
    if (value.startswith("0x")):
      iv = int(value[2:], 16)
      globals()[name] = int(iv)
    elif name in expectint:
      iv = int('%s' % value, 0)
      globals()[name] = iv
    elif name in expectfloat:
      fv = float('%s' % value)
      globals()[name] = float(fv)
    else:
      globals()[name] = value

  return()

# Read own configuration.
loadconfig(MYCONFIG)

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

# SPECIFIC SECTION

# GLOBALS

# MAIN SETTINGS

# Show frame borders?
FRAMEBORDERS = 0

# 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                                                                       Ok  Cancel  |
#  -------------------------------------------------------------------------------------
#
# --------------------------------------------------------------------------------------------------
#
# Frames
#
#################################### STANDARD ######################################################
#
# Notes:
# - square brackets are used for tokens because of web issues: [a]
# - parentheses indicate an optional presence: [a](.[b])
#
# 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:
# [frame name]:	[frame definition]
# [frame definition]:	[parent]:[text]:[layout]
# [layout]:		[row][.(col](.[rowspan and/or colspan]))
# [colspan]:		h[num]	(h for horizontal)
# [rowspan]:		v[num]	(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

if (WINDOWSTYLE == 'standard'):

  # [width]x[height]+[xpos]+[ypos]. Passed as is to Tkinter [window].geometry().
  WINDOWSIZE = '734x528+40+40'

  # Max. frame width in widets; used for wrapping horizontal sequences of radio buttons around.
  # Rows must be reserved accordingly.
  # A value of 0 means no wrapping will take place.
  MAXFRAMEWIDTH = 0

  ROOTFRAMESLIST = [

# [frame name]              [parent]               [text]              [layout]
# Row 0
  'frame_root:              root:                  :                   0.0',
  'frame_sets:              frame_root:            Color sets:         0.0.h3',
  'frame_sets_widgets:      frame_sets:            :                   1.0',           # 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
# [symbolic name]: '[frame]:[type]:[index]:[weight]
# [type]: r (row), c (column)
# [index]: row or columm number (0..n-1)
# Example: 'frame_root0': 'frame_root:r:0:1'
# Becomes: root.grid_[row|column]configure(0, weight=1)
# There is little code gain in this dict, it just keeps things central
  ROOTWEIGHTDICT = {

# [symbolic name]        [frame]            [type]      [index]        [weight]
  '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
# [varname]: '[parent];[vector];[wtype];[text];[vtype];[initvalue][,[more values], ..[xpad]'
# [parent]          parent frame
# [vector]          h (horizontal), v (vertical): how to arrange widgets within a frame
# [text]            the text to display on the widget
# [wtype]           widget type: cb (checkbutton), of (openfile), en (entry), rb (radiobutton)
#                   - button (bt) and menubutton (mb) never appear in ROOTCONFIGDICT
#                   - these aren't associated with a config variable (-> go into ROOTPARADICT)
# [vtype]           variable type: i (int),  f (float), b (bool), s {string)
# [default]         initial (default) value
#                   - can be a comma-separated list (with radiobutton)
#                   - in that case, first value is the default
# [xpad]            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 = {

# [var/widget]            [parent]                 [vector]  [wtype]    [text]             [vtype]    [default]     [xpad]
  '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
# [widget type]        bt (button), mb (menubutton)
# Minor TODO: [xpad] as above, but with orientation
  ROOTPARADICT = {

# [widget]                [parent]                 [vector]  [wtype]    [text]             [vtype]    [default]     [xpad]
  '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;              ;          ;             ',
  'write':                'frame_ok;               h;        bt;        Write;             ;          ;             4.E',
  'exit':                 'frame_ok;               h;        bt;        Exit;              ;          ;             4.E',

  }

#################################### VERTICAL ######################################################

elif (WINDOWSTYLE == 'vertical'):

  WINDOWSIZE = '464x806'

  # Max. width of 4 widgets for horizontal frames.
  # used here with color set radiobuttons; requires two rows to be reserved for 7 buttons.
  MAXFRAMEWIDTH = 4

  ROOTFRAMESLIST = [

# [frame name]              [parent]               [text]              [layout]
# Row 0
  'frame_root:              root:                  :                   0.0',
  'frame_sets:              frame_root:            Color sets:         0.0.h2',
  'frame_sets_widgets:      frame_sets:            :                   1.0.v2',           # relative to row 0: frame_sets

# Row 1
  'frame_field:             frame_root:            Field settings:     2.0',
  'frame_field_widgets:     frame_field:           :                   1.0',
  'frame_page:              frame_root:            Page settings:      2.1',
  'frame_page_widgets:      frame_page:            :                   1.0',

# Row 2
  'frame_layout:            frame_root:            Layout settings:    3.0',
  'frame_layout_widgets:    frame_layout:          :                   1.0',
  'frame_color:             frame_root:            Color settings:     3.1',
  'frame_color_widgets:     frame_color:           :                   1.0',

# Row 3
  'frame_rotate:            frame_root:            Rotate settings:    4.0',
  'frame_rotate_widgets:    frame_rotate:          :                   1.0',
  'frame_start:             frame_root:            Start color:        4.1',
  'frame_start_widgets:     frame_start:           :                   1.0',

# Row 4
  'frame_presets:           frame_root:            Presets:            5.0.h2',
  'frame_presets_widgets:   frame_root:            :                   6.0.h2',
  'frame_misc:              frame_root:            Other:              7.0.h2',
  'frame_misc_widgets:      frame_root:            :                   8.0.h2',
  'frame_colorfile:         frame_misc_widgets:    :                   0.0',
  'frame_bonus:             frame_misc_widgets:    :                   0.1',

# Row 5
  'frame_bogus:             frame_root:            :                   9.0.h2',
  'frame_ok:                frame_root:            :                   10.0.h2',

  ]

  ROOTWEIGHTDICT = {

# [symbolic name]        [frame]            [type]      [index]        [weight]
#  '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',
  'frame_ok_col0':       'frame_ok:          c:         0:             0',
  'frane_ok_col1':       'frame_ok:          c:         1:             1',

  }

  ROOTCONFIGDICT = {

# [var/widget]            [parent]                 [vector]  [wtype]    [text]             [vtype]    [default]     [xpad]
  '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;        ;             ',

  }

  ROOTPARADICT = {

# [widget]                [parent]                 [vector]  [wtype]    [text]             [vtype]    [default]     [xpad]
  'bonus':                'frame_bonus;            h;        bt;        Bonus code;        ;          ;             0',
  'presets':              'frame_presets_widgets;  h;        mb;        Presets;           ;          ;             20',
  'savepresets':          'frame_presets_widgets;  h;        bt;        Save;              ;          ;             20',
  'deletepresets':        'frame_presets_widgets;  h;        bt;        Delete;            ;          ;             20',
  'defaultpresets':       'frame_presets_widgets;  h;        bt;        Defaults;          ;          ;             20',
  'write':                'frame_ok;               h;        bt;        Write;             ;          ;             4.W',
  'exit':                 'frame_ok;               h;        bt;        Exit;              ;          ;             20.E',

  }

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

# SHARED DICTIONARIES

# Object dictionaries.
# These are merely containers and can be safely used for both the main and subwindows, provided
# unique keys are used. Not using unique keys (such as 'ok' or 'root_ok', 'subwin1_ok' etc.) would
# result in a configuration pudding anyway.

# Set by main/subwindow code and by makewidgetframe(), config: setframeweights(), read by setupwidgets()
# [framename]: [Tkinter Frame object].
FRAMEDICT = {}

# Set by setupwidgets(), config: configwidgets()
# [varname]: [widget object]
TKOBJDICT = {}

# For radiobuttons
RBOBJDICT = {}

# Set by inittkvars(), read by manageconfigdict()
# [varname]: [IntVar|StringVar object]
TKVARDICT = {}

# Configuration settings

# Set by initdefaultsdict(), passed to settkvars() by restoredefaults(), onpresetselect()
# Simple '[var]: [value]' pairs retrieved from *CONFIGDICT config dictionaries.
# This dictionary exisis merely for convenience. It can be used whenever additional info,
# such as about widget type, is not needed. It relieves functions from having to know
# about config dictionary names.
DEFAULTSDICT = {}

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

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


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

# Widget dependencies

# Widgets to be disabled on startup
ROOTDISABLED = [
  'huerotation',
  'rotatecolor',
  'backgroundcolor',
# There are issues with these; see wconfigspecifics(), configwidgets()
#  'skipoffset',
#  'invertskip',
  ]


###################################################################################################
###################################################################################################
# IN THE CASE OF CONFLICTING STATES PRESENT, ALL RADIO BUTToNS WILL HAVE TO BE LISTED, EACH WITH
# ITS APPROPRIATE STATE
###################################################################################################
###################################################################################################
# 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
# [parent widget]: [widget]|-[widget],...]
# [widget]: enable; -[widget]: disable
ROOTENABLEDICT = {
# There are issues with these; see wconfigspecifics(), configwidgets()
#  'fieldskip': 'skipoffset,invertskip',
  'showbackground': 'backgroundcolor',
  'colorset:plainrgb': 'turnaround',
  'colorset:web': 'turnaround',
  'colorset:rotate': 'huerotation, rotatecolor , -colorstep, -rbgoutput',
  }


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

# BONUS SETTINGS

# Everything here is analogous to what happens with the main window.

BONUSWINDOWSIZE = '366x278+80+80'

# --------------------------------------------------------------------------------------------------
#
# Frames
#
# Frame list for bonus window
BONUSFRAMESLIST = [
# [frame name]                     [parent]                  [text]              [layout]
# 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 = {
# [symbolic name]     [frame]                     [type]     [index]             [weight]
  '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 = {
# [var/widget]            [parent]                           [vector]   [wtype]    [text]             [vtype]    [default]     [xpad]
  '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 = {
# [var/widget]            [parent]                           [vector]   [wtype]    [text]             [vtype]    [default]     [xpad]
#  'bwin_help':            'frame_bwin_ok;                    h;         bt;        Help;              b;         no;           ',
  'bwin_ok':              'frame_bwin_ok;                    h;         bt;        OK;                b;         no;           ',
  'bwin_cancel':          'frame_bwin_ok;                    h;         bt;        Cancel;            b;         no;           ',
  }

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

# Widgets disabled on startup
BONUSDISABLED = [

  'objectstrokecolor',

  ]

# Widget enable/disable dependencies
BONUSENABLEDICT = {

  'objectstrokewidth': 'objectstrokecolor',

  }


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

# COMBINED DICTIONARIES

# Having partial dictionaries allows functions to skip certain widgets in for loops without fuss.
# Some functions need more info than others. The general approach is to configure all windows at
# startup, whether they are ever opened or not.

# These are used for creating widgets, but not for handling RGBX configuration
ROOTWIDGETSDICT = {**ROOTCONFIGDICT, **ROOTPARADICT}
BONUSWIDGETSDICT = {**BONUSCONFIGDICT, **BONUSPARADICT}

# Some functions require all info concerning configuration variables from all windows.
# Examples: finctions handling presets and defaults.
VARDICT = {**ROOTCONFIGDICT, **BONUSCONFIGDICT}

# Everything
ALLDICT = {**ROOTWIDGETSDICT, **BONUSWIDGETSDICT}


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

# 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):

  wobj = TKOBJDICT[wname]

  if (wtype == 'en'):

    if (wname == 'fieldskip'):

      # In an ideal world a callback command would be triggered every time the value
      # of an entry field changes. Withut an extra button this doen't seem achievable.
      # Solution: leave Skip offset and Invert active all the time, even though with
      # a Field skip value of 0 this doesn't make sense.
      pass

  elif (wtype == 'bt'):

    if (wname == 'bonus'):

      wobj.configure(command=onbonusbutton)
      wobj.bind('[Return]', onbonusbutton)

    elif (wname == 'savepresets'):

      wobj.configure(command=savepresets)
      wobj.bind('[Return]', savepresets)

    elif (wname == 'defaultpresets'):

      wobj.configure(command=restoredefaults)
      wobj.bind('[Return]', restoredefaults)

    elif (wname == 'deletepresets'):

      wobj.configure(command=removepresetspaths)
      wobj.bind('[Return]', removepresetspaths)

    elif (wname == 'write'):

      wobj.configure(command=onwritebutton)
      wobj.bind('[Return]', onwritebutton)

    elif (wname == 'exit'):

      wobj.configure(command=onexitbutton)
      wobj.bind('[Return]', onexitbutton)

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

      wobj.configure(command=onbonusokbutton)
      wobj.bind('[Return]', onbonusokbutton)

    elif (wname == 'bwin_cancel'):

      wobj.configure(command=onbonuscancelbutton)
      wobj.bind('[Return]', onbonuscancelbutton)
    # *****************************************

  elif (wtype == 'fs'):

    if (wname == 'colorfile'):

      wobj.configure(command=selectcolorfile)
      wobj.bind('[Return]', selectcolorfile)

  elif (wtype == 'mb'):

    if (wname == 'presets'):

      # Tkinter menubutton has no command option it seems.
      setpresets(wname)

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

  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):

  global CONFIGDICT

  fpath = ''
  filetypes = (('Text files', '*.txt'), ('All files', '*.*'))

  # fd is a file dialog, not a file descriptor: 'from tkinter import filedialog as fd'
  fpath = fd.askopenfilename(title='Color file', initialdir=CWD, filetypes=filetypes)
  if fpath:
    showmessage('Color file', 'Selected color file:\n%s' % fpath)
    # TODO: can't this be simplified (TKVARDICT only)?
    CONFIGDICT['colorfile'] = fpath
    TKVARDICT['colorfile'].set(fpath)

  return()


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


# Handle OK: write config if changes were made
def onwritebutton(event=None):

  manageconfigdict()
  writeconfig(CONFIGFILE)

  return()


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


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

# GENERAL SECTION


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

# FUNCTION DEFINITIONS

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

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

# GENERAL/MISC

# Stuff that doesn't go elswhere


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

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

  return(rv)


# Set up the environment to run in. Currently only make sure a presets ditectory exists.
# TODO: Q: does the presets directory really have to exist is presets feature is never used?
# Needed by readpresets(), setpresets() -> getpresetspaths(),
# savepresets() -> writeconfig(presetsfile), removepresetspaths()
def initrunenv():

  presetsdirpath = getpresetsdirpath()

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

  return()


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 '[frame_name]:'
def getframedef(frname, framelist=ROOTFRAMESLIST):

  listentry = ''

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

  return(listentry)


def getframeparent(frname, framelist=ROOTFRAMESLIST):

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

  # [frame name]: [position](: [parent frame])
  pname = frlist[1]					# frame_root

  return(pname)


def getframelayout(frname, framelist=ROOTFRAMESLIST):

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

  # [frame name]: [position](: [parent frame])
  frargs = frlist[3]					# 6.1.h2
  frlayout = frargs.split('.')			# 6 1 h2

  return(frlayout)


# The following functions peel back the frame's layout string

def getframerow(frlayout):
  frrow = frlayout[0]
  return(frrow)

def getframecol(frlayout):
  frcol = '0'
  if hasindex(frlayout, 1):
    if (not 'h' in frlayout[1] and not 'v' in frlayout[1] and not 'w' in frlayout[1]):
      frcol = frlayout[1].strip()
  return(frcol)

def getframespanstring(frlayout):
  frspanstr = ', '				# (weird)
  if hasindex(frlayout, 2):
    # Column is never at index 2.
    frspanstr = frlayout[2].strip()
  elif hasindex(frlayout, 1):
    # Index 1 may hold either the column of the span string.
    if ('h' in frlayout[1] or 'v' in frlayout[1]):
      frspanstr = frlayout[1].strip()
  return(frspanstr)

## With every field added the number of tests grows..
#def getMAXFRAMEWIDTH(frlayout):
#  frwidth = 0
#  if hasindex(frlayout, 3):
#    # Column is never at index 2.
#    frspanstr = frlayout[3].strip()
#  # From here a for loop would be nice.
#  elif hasindex(frlayout, 2):
#    if ('w' in frlayout[2]):
#      frwidth = frlayout[2].strip()
#  elif hasindex(frlayout, 1):
#    # Index 1 may hold either the column of the span string.
#    if ('w' in frlayout[1]):
#      frwidth = frlayout[1].strip()
#  return(frwidth)


def buildframe(frname, framelist=ROOTFRAMESLIST):

#  global MAXFRAMEWIDTH

  # Last field of the frame definition is the layout field
  pname = getframeparent(frname, framelist=framelist)	# e.g. 6 1 h2
  frlayout = getframelayout(frname, framelist=framelist)	# e.g. 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
  frrow = getframerow(frlayout)
  # Column
  frcol = getframecol(frlayout)
  # Span string
  frspanstr = getframespanstring(frlayout)
#  # Max frame width in widgets (for radiobuttons)
#  frwidth = getMAXFRAMEWIDTH(frlayout)
#  MAXFRAMEWIDTH = frwidth

  # 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:]

  # 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):

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

  return(wlist[0].strip())


def getwvect(wname):

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

  return(wlist[1].strip())


def getwtype(wname):

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

  return(wlist[2].strip())


def getwtext(wname):

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

  return(wlist[3].strip())


def getvartype(var):

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

  return(vlist[4].strip())


def getvarval(var):

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

  return(vlist[5].strip())


def getwlayout(wname):

  wlist = splitstr(ALLDICT[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'):
      if (count > 0 and MAXFRAMEWIDTH > 0 and (count % (MAXFRAMEWIDTH - 1)) == 0):
        row += 1
        col = 0
      else:
        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)
    wvect = getwvect(w)
    wtype = getwtype(w)
    wtext = getwtext(w)
    wlayout = getwlayout(w)
    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'):

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

  elif (vtype == 'int'):

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

  return(tkvar)


# Initoalize Tkinter objects
def inittkvars():

  # inittkvar() return values are unused but available.

  for w in ALLDICT:

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

    # 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(widgetdict=ROOTWIDGETSDICT, enabledict=ROOTENABLEDICT, disabledlist=ROOTDISABLED):

  for w in widgetdict:

    wtype = getwtype(w)
    vval = getvarval(w)

    # 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:

        pass

        ############################################################################################
        ############################################################################################
        # This will do the trick on the first change, but not a second time.
        #obj = TKOBJDICT[w]
        #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, 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):
  tkvar = TKVARDICT[wname].get()
  return(tkvar)


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

# 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()


###################################################################################################
# CONFIG SPECIFICS ('fieldskip')
###################################################################################################
###################################################################################################
# DEFUNCT (Tkinter's entry field handling won't cooperate)
###################################################################################################
# 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()


###################################################################################################
# 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, enabledict=ROOTENABLEDICT):

  wstr = getvarval(wname)	# 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, 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, ..'
  # In that case the value must be specified for every radio button separately.
  invertlist = togglerbinvert(wname, wval)

  # 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


# 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 (???)

  TKVARDICT[wname].set(val)

  return()


# Initialize Tkinter variables (IntVar/StringVar). The values in these correspond with a
# widget's state.
def settkvars(configdict):

  for w in configdict:

    if w in VARDICT:

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

      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)

    else:

      # This should never happen
      logwarning('unable to initialize configuration variable %s' % w)

  return()


# Store initial (default) '[var]: [value]' pairs read from a config dict in the defaults dict
def initdefaultsdict():

  global DEFAULTSDICT

  for var in VARDICT:
    val = getvarval(var)
    DEFAULTSDICT[var] = 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():

  global CONFIGDICT

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

  for var in VARDICT:

    vtype = getvartype(var)
    wtype = getwtype(var)

    # String values are written python style, in single quotes.
    # Numeric values are written without quotes.
    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())

    # For entries: empty input: use the default
    if (wtype == 'en' and curval == ''):
      curval = default

    # 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:

      # Add value, quoted is necessary
      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 presets files.
def writeconfig(cpath):

  printtext = ''

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

  try:

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

  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


# Returns the full pathname of the rpesets ditectory
def getpresetsdirpath():

  if (SISCONFIGDIR != ''):
    # Expand a tilde if present
    scdir = os.path.expanduser(SISCONFIGDIR)
    pdpath = os.path.join(scdir, PRESETSDIR)
  else:
    # Presets dir will be created in CWD
    pdpath = PRESETSDIR

  return(pdpath)


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

  import glob

  presetsdirpath = getpresetsdirpath()

  if os.path.exists(presetsdirpath):

    pspaths = []					# list of files

#    for f in glob.glob(os.path.join(PRESETSDIR, '*')):
    for f in glob.glob(os.path.join(presetsdirpath, '*')):
      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)


# 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 '[var] = [value]' config file with a special name.
def savepresets():

  presetsdirpath = getpresetsdirpath()

  write = False

  # No need to ask, user can hit Cancel
  #if asksavepresets():
  if True:

    rv = manageconfigdict()

    if (rv):

      answer = askpresetname()

      if (answer != None):

        # CWD/rgbx-presets/[answer]
        presetsfile = os.path.join(presetsdirpath, answer)

        if os.path.exists(presetsdirpath):

          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)
    # Update Presets menu
    setpresets('presets')

  return()


# Populate the Presets menu dynamically. Strange that Tkinter doesn't have some builtin
# routine for accomplishing this. In addition, the procedure is quite ill-documented.
# Another sweeping approach here: there were issues deleting individual menu items,
# therefore the existing menu button is simply destroyed and rebuilt whenever changes
# are made to it.
# Note 1: the menu button created by setupwidgets() at startup is # immediately killed,
# which is ok: it happens largely transparent to the user and it keeps the code lean.
# Note 2: in the current configuration, wname is always 'presets'.
# TODO: generalize this routine.
def setpresets(wname):

  # Remove existing menu button and create a new one. Calling updatemenu() from here
  # keeps things central.
  mnu = updatemenu(wname)

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

  # TODO: re-examine the following procedure, borrowed from a forum. At least it works,
  # even if it is a bit opaque. Why not just populate a dictionary and skip the list?

  if presetspathlist:

    # Add key-value pairs to dictionary
    for i in range(len(presetspathlist)):

      key = wname+str(i)				# key0, key1, ..

      temp = {key: presetspathlist[i]}
      menudict.update(temp)

      # Q: Why does this not work ??
      #mkeys.append[key]

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

    # Add entries and values to the menu
    for i in range(len(presetspathlist)):

      mkey = mkeys[i]
      #mkey = list(menudict.keys())[i]		# ???

      fname = menudict.get(mkey)

      # Get the label name (part of the file's basename after last '.').
      lbl = getpresetsname(menudict[wname+str(i)])

      # Menu items aren't individual objects: add_radiobutton() returns None.
      mnu.menu.add_radiobutton(label=lbl, command=lambda x = fname: onpresetselect(x))
      mnu.grid(row=0, column=0, sticky='W', padx=4, pady=4)

  else:

    # Add a 'none' entry to the menu. This keeps a presets button with an empty list of items
    # from hijacking the window when pressed (!).
    mnu.menu.add_radiobutton(label='(none saved)')
    mnu.grid(row=0, column=0, sticky='W', padx=4, pady=4)

  return()


# Update a menu button's list of items by detroying the menu and creating a new one.
def updatemenu(wname):

  # Deleting menu items one by one ('menu.delete(label)') returns an error:
  # AttributeError: 'Menubutton' object has no attribute 'delete'
  # A typical case of running out of options with Tkinter. Documentation is abysmal.
  # Alt: just destroy the menu botton and create a new one

  oldmnu = TKOBJDICT[wname]
  oldmnu.grid_remove()

  tmpwdgdict = {}
  tmpwdgdict[wname] = ROOTWIDGETSDICT[wname]
  setupwidgets(widgetdict=tmpwdgdict)
  newmnu = TKOBJDICT[wname]

  return(newmnu)

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

  global PRESETSDICT

  PRESETSDICT.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('"', "")
          PRESETSDICT[var] = firstvalue(val)

  except OSError:

    titletext='Error loading presets'
    warntext = 'Reading presets from %s failed' % fpath
    showwarning(titletext, warntext)

  return()


# 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 PRESETSDICT

  # Reconfiguration is done in a sweeping way: defaults plus values read from the preset file
  settkvars(DEFAULTSDICT)		# back to defaults
  settkvars(PRESETSDICT)		# read settings from PRESETSDICT

  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))

  # Flush presets list
  setpresets('presets')

  return()


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

# BONUS CODE

# Keep the window object globally accessible
bonuswindow = None

# Handle OK: write config if changes were made
def onbonusokbutton():
  # Update CURRENT configuration
  manageconfigdict()
  bonuswindow.after(200, bonuswindow.destroy)

# On Cancel, simply exit
def onbonuscancelbutton(event=None):
  bonuswindow.after(200, bonuswindow.destroy)

# Set uo a subwindow. This is basically a copy of the main code for setting up the
# root window (same routines, different dictionaries passed), but with an extra
# reinitialization added in case the subwindow is  opened more than once.
def onbonusbutton(event=None):

  global bonuswindow

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

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

  # Widgets
  setupwidgets(widgetdict=BONUSWIDGETSDICT)
  # Bind tk vars to widgets (Tk var initializaion has been taken care of globally)
  configwidgets(widgetdict=BONUSWIDGETSDICT, enabledict=BONUSENABLEDICT, disabledlist=BONUSDISABLED)

  return()


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

# MAIN CODE

# Set up the environment to run in
initrunenv()

# Window
root = tk.Tk()
root.title('RGBX Configuration Editor')
root.geometry(WINDOWSIZE)
root.attributes('-topmost', True)
FRAMEDICT['root'] = root
#root.resizable(False, False)

# Defaults
initdefaultsdict()

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

# Widgets
# Create widget objects
setupwidgets()
# Initialize tk vars with default values
inittkvars()
# Configure widgets: bind tk vars to widgets
configwidgets()

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

root.protocol("WM_DELETE_WINDOW", byebye)

root.mainloop()