#!/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 # TODO: alternative window geometry ('column' vs 'standard') plus option in own config file # ----- # 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 ###################################################### # # (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: # <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'): 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>;<widget type>;<text>>;<vartype>;<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 = '520x820+20+20' # 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_misc: frame_root: Misc: 5.0.h2', 'frame_misc_widgets: frame_misc: : 0.1', 'frame_colorfile: frame_misc_widgets: : 0.0', 'frame_bonus: frame_misc_widgets: : 0.1', 'frame_presets_header: frame_root: Presets: 6.0', 'frame_presets_widgets0: frame_root: : 7.0', 'frame_presets_widgets1: frame_root: : 8.0', # Row 5 'frame_ok: frame_root: : 9.0', ] 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', } 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_widgets0; h; mb; Select presets; ; ; 20', 'savepresets': 'frame_presets_widgets0; h; bt; Save presets; ; ; 20', 'defaultpresets': 'frame_presets_widgets1; h; bt; Restore defaults; ; ; 20', 'deletepresets': 'frame_presets_widgets1; h; bt; Delete presets; ; ; 20', 'write': 'frame_ok; h; bt; Write; ; ; 4.E', 'exit': 'frame_ok; h; bt; Exit; ; ; 4.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' #BONUSWINDOWSIZE = '366x238-40-40' # -------------------------------------------------------------------------------------------------- # # 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()