TIP 560: Megawidget Configure/Property Support

Login
Bounty program for improvements to Tcl and certain Tcl packages.
    Author:         Donal K. Fellows <donal.k.fellows@manchester.ac.uk>
    State:          Draft
    Type:           Project
    Vote:           Pending
    Created:        23-Jan-2020
    Post-History:
    Tcl-Version:    8.7
    Keywords:       Tk, TclOO, configuration, properties, options
    Tk-Branch:      tip-560

Abstract

This TIP is a companion for TIP #558 and builds upon the basic facilities described in it; it describes how to build a configuration system based on TclOO that can support making Tk megawidgets.

Rationale and Design Requirements

Tk megawidgets should be a natural fit for TclOO, as Tk widgets have long behaved like classes. However, configuration of Tk-like objects is quite complex and has long been one of the most awkward parts for authors of megawidgets, often leading to only partial implementations. It is also rather different to the simple system described in TIP #558. In particular, options can be configured from defaults, from the option database, or explicitly, and two methods, configure and cget, to handle scripted access; configure returns an option descriptor when reading, whereas cget just reads the value of the option. (Terminology note: Tcl has properties while Tk has options.)

It should be noted that this scheme is necessarily semantically incompatible with the configure of TIP #558; the results of configure are entirely different, with one returning the property value itself and the other returning an option descriptor (a short list); both mechanisms are present "in the wild" so attempting to unify this is extremely difficult with how things stand. Therefore it is a non-goal of this TIP to design a system that can allow an object to be accessed by both systems at once; that is never going to work right.

There are also options that can only be configured during widget creation (e.g., the -use option of toplevels and the -class option of quite a few widgets) though all options are always readable. Another complexity is that calls to configure are transactional; no changes are applied unless all changes are applied (though a useless redisplay might be triggered). What's more, any -class option needs to be handled early as it changes how the configuration database is read from. (Indeed, this is omitted from the standard mechanism, just as it is also handled specially in Tk frame widgets and so on.)

Finally, there should be a mechanism for supporting aliases of options.

In support of this, we will want to support typing of options as this is a common feature of Tk widgets. While there is a substantial set of standard types (such as strings, colors, images, and screen distances) it is an open set: we need a way of allowing user code to add custom types. A common custom type is the table-driven type, where values must be chosen from a given list of strings but can be abbreviated, so we should ensure that we provide special support for that.

Specification

Tk will supply a TclOO class, tk::Configurable, that classes may inherit from to gain a configure and a cget method, as well as a non-exported Initialise method (that may only be called once; subsequent calls will do nothing) intended to be used from constructors, and a non-exported PostConfigure method intended as a point for user- and subclass-interception. In addition, Tk will supply a metaclass, tk::configurable (notice the capitalisation difference), that will allow the creation of definitions suitable for configuration (that class may gain other behaviours in the future) with the option declaration (note that this isn't the built-in option command, but has the same name so that options are always called that). As with the oo::configurable metaclass, option names will be given without leading hyphens when they are specified. Classes created with tk::configurable will have tk::Configurable mixed in.

An example of use of this:

tk::configurable create myLabel {
    # Conventional setup of constructor/destructor
    variable window
    constructor {w args} {
        set window [label $w]
        my Initialise $w {*}$args
    }
    destructor {
        destroy $window
    }

    # Define some options for this class
    option label
    option borderwidth -type distance -default 1px \
        -name borderWidth -class BorderWidth
    option bd -alias borderwidth
}

As we can see from this, we want to support some configuration properties for an option. The full list (not fully shown above) is:

  • -name optName

    This gives the name used for looking up a default in the option database. It defaults to the main name of the option with string tolower applied to it.

  • -class clsName

    This gives the class name used for looking up a default in the option database (this is not a TclOO name). It defaults to the main name of the option with string totitle applied to it.

  • -default value

    The default value of the option, used when no other default is available. When this option is not present, the default will depend on the type of the option (see below), but is usually either the empty string or a zero value. The default will be validated against the type (see below); it is an error for the default to be not type-valid.

  • -type typeName

    The type of the value. The types will be subcommands of an ensemble (see Option Types below) that will provide defaults and validation. The default type will be string, which will do no validation and use a default that is empty.

    Defaults from the option database (read during Initialise) are subject to validation by the type, but if the default from the option database fails validation, it is ignored and does not trigger a failure of the megawidget to initialise. This is because the option database is not wholly under script control.

  • -initonly boolean

    Controls whether this is an initialisation only option. Initialisation only options may only be set by the Initialise method, and not by configure. (For example, real initialise-only options are the -use option of toplevels and the -container option of frames.) Options are not initialisation only by default; this is expected to be a rare use case.

  • -alias optionName

    Makes this option an alias for another option, which must exist at this point (for sanity's sake if nothing else). The other configuration properties to option (as described above) will be illegal if this is given. Note that alias options are not set by initialisation (unless explicitly provided), since their underlying option should be set instead.

The configure and cget methods will work in ways that should be immediately recognisable to Tk users. There will also be a non-exported PostConfigure method (taking no arguments) that will be called by configure after any call that could have changed the state (no determination will be done of whether the state actually changed); the default implementation of PostConfigure will be empty, but it will provide a convenient place to hook generation of events for state changes or validation of the whole configuration (errors will trigger the same rollback behavior as validation failures). It will be recommended (but out of the scope of this TIP to implement) that idle events are used to combine state update events.

Initialisation will be done by calling the non-exported Initialise method, which will take its first argument to be the widget path name (we do not assume that this is the same as the object name) and an even number of following arguments that will be the same as if for configure. The initialisation will write all elements of the array, using the information from the option configuration and retrieved from the option database (see option get), and is the only method that will write initialisation-only elements. Note that this method is intended to be called from a constructor, and will not call the PostConfigure method or perform state rollback on failure; the caller can do any system validation afterwards, and validation failures are expected to abort widget creation altogether rather than rolling anything back.

The Initialise method may only be called (successfully) once.

Interaction with Fundamental TclOO and Tk Mechanisms

As all options are readable in Tk, all will be listed in the readable properties of the class (see TIP #558 for the Tcl mechanism for this). Most options will also be listed in the writable properties of the class, but initialisation only options will not. (Note once again: megawidgets are not oo:configurable in the sense of TIP #558, but they do use the same basic TclOO mechanisms.)

The following methods will be created for each option (where name is the hyphen-removed version of the name):

  • <OptDescribe-name> — takes no arguments and returns a description of the parts of the option descriptor that do not change. In particular:

    For ordinary options, this is a list of three items; these are the option database name of the option, the option database class of the option, and the actual default value of the option.

    For aliases, this is a single-element list containing the name of the target option.

  • <OptValidate-name> — takes a single value, checks whether the value is of the type of the option, and returns the normalized option value. Typically forwarded to the validate method of the option's type object.

  • <OptRead-name> — how to actually read the option out of whatever storage it is implemented with. Takes no arguments and returns the current value of the option. Note that if a method exists with this name in the class, it is not overridden by the option declaration; this makes providing implementations in C on a case-by-case basis relatively straight forward.

    The default implementation forwards to the <StdOptRead> method (see below).

  • <OptWrite-name> — how to actually write the option to whatever storage it is implemented with. Takes a single argument, the value to write (after validation and normalization). Return value is ignored. Note that if a method exists with this name in the class, it is not overridden by the option declaration; this makes providing implementations in C on a case-by-case basis relatively straight forward.

    The default implementation forwards to the <StdOptWrite> method (see below).

The default storage mechanism for options will be the array in the object instance with the empty local name (so the option foo will be in array element variable (foo) in the instance namespace; this is a trick stolen from stooop). This can be overridden by defining appropriate non-exported methods, for which there are these implementations provided by default:

  • <StdOptRead> — This method reads an element of the array. It takes a single argument, the full name of the element, and returns the value inside. It is never called with the name of an alias. (The <OptRead-name> methods described above delegate to this.)

    If an implementation provides custom readers for each option so that it goes to the right slot in a C structure, it does not need to provide an override of this method.

  • <StdOptWrite> — This method writes an element of the array. It takes a two arguments, the full name of the element and the value to write. Its return value is ignored. It is never called with the name of an alias. (The <OptWrite-name> methods described above delegate to this.)

    If an implementation provides custom writers for each option so that it comes from the right slot in a C structure, it does not need to provide an override of this method.

  • <OptionsMakeCheckpoint> — This method saves the state so that it may be restored. It takes no arguments and returns the saved state that can be used with <OptionsRestoreCheckpoint> on the same class if required. The default implementation uses array get, but the method may be overridden; the only constraint is that whatever the checkpoint creator creates must be consumable by the matching checkpoint restorer. Any overriding implementation should include the superclass's checkpoint state in its own checkpoint state (calling via next).

    If any options are implemented with C backing store, an override for this method should be provided. Note that this means that it is probably unwise to override this method and not <OptionsRestoreCheckpoint>.

  • <OptionsRestoreCheckpoint> — This method restores a saved state created with <OptionsMakeCheckpoint>. It takes a single argument, the saved state. Its return value is ignored. The default implementation uses array set, but the method may be overridden. No attempt is made to validate the contents of a saved state. If overridden, the overriding implementation should also call the superclass's implementation (calling via next) to restore that portion of the checkpointed state.

    If any options are implemented with C backing store, an override for this method should be provided. Note that this means that it is probably unwise to override this method and not <OptionsMakeCheckpoint>.

Note that Initialise requires an existing widget name. A consequence of that is that any true initialisation-only options that need to be passed to that widget must be manually parsed before the widget is created (or the widget can be created, used for parsing, and then destroyed and rebuilt with the correct options; that's not too expensive if the temporary widget is never mapped).

Note also that the implementations of <StdOptRead>, <StdOptWrite>, <OptionsMakeCheckpoint>, <OptionsRestoreCheckpoint>, and PostConfigure are installed in a place in the class hierarchy where it is maximally easy for instances of tk::configurable to override. Their implementation class is ::tk::ConfigurableStandardImplementations.

Supporting Changes to the Tk Core

The option get is to gain an extra optional argument after all its current mandatory ones, default, which will be the value returned when the underlying call to Tk_GetOption() cannot find a value to return (the case where it returns NULL) and where Tk used to always return the empty string. Since option get previously did not take any optional arguments at all, this is a compatible change.

  • option get window name class ?default?

The value of this change is when we have any code where we already know what we want to use instead (such as with the option specified in this TIP) it is less ambiguous to get Tk to handle the switch over to our known default value rather than assuming that the empty string always means that there was no value specified in the option database.

Option Types

One key part of this specification is a system for typing of options, since it is extremely common for Tk widget options to be constrained to be of particular types. This will be done using an ensemble of type implementation commands, tk::OptionType, with the member elements of the ensemble being themselves ensemble-like (probably objects, but not necessarily), supporting at least two subcommands, validate and default.

The validate subcommand will take a single argument, the value to be validated, and will produce an error if the validation fails and return the value to be actually set otherwise (to allow a value to be converted to canonical form if desired). The default subcommand will take no arguments, and return the default value usually associated with the type. (Note that there is no need to make either of these commands aware of which class or instance they’re being used with; types are independent of how they are used and these defaults can be overridden when the option is created.)

For example, this will allow the validation of a proposed value, $foobar, for an option of type $gorp, to be done by calling:

tk::OptionType $gorp validate $foobar

The standard types will be:

  • string: any string. Defaults to the empty string. Validation always succeeds on this type and never changes the value.

  • boolean: value acceptable to string is boolean -strict (e.g., true or off). Defaults to false.

  • zboolean: empty string or boolean. Defaults to the empty string.

  • integer: any integer (i.e., acceptable to string is entier -strict). Defaults to 0.

  • zinteger: any integer (as above) or empty string. Defaults to the empty string.

  • float: any float (except NaN) acceptable to string is double -strict. Defaults to 0.0.

  • zfloat: any float (as above) or empty string. Defaults to the empty string.

  • distance: any screen distance (as parsed by winfo fpixels). Defaults to 0px.

  • image: name of any Tk image, or empty string. Defaults to the empty string.

  • color: any color (as parsed by winfo rgb). Defaults to black.

  • zcolor: any color, or empty string. Defaults to the empty string.

  • font: any font (as parsed by font actual). Defaults to TkDefaultFont.

  • relief: any relief (flat, groove, raised, ridge, solid, or sunken) or unambiguous prefix. Defaults to flat.

  • justify: any justification (left, right, or center) or unambiguous prefix. Defaults to left.

  • anchor: any anchor (n, ne, e, se, s, sw, w, nw, or center) or unambiguous prefix. Defaults to center.

  • window: any existing window path name, or empty string. Defaults to the empty string.

  • cursor: any of the forms acceptable as a cursor on the current platform, or the empty string. Defaults to the empty string.

  • list: any valid Tcl list. Defaults to the empty list (and empty string).

  • dict: any valid Tcl dictionary. Defaults to the empty dict (and empty string).

The official mechanism for adding a new type will be via the class tk::optiontype. Instances of that will automatically plug themselves inside using their names, and will be implemented using a callback provided to the constructor. This will result in a class definition (approximately) like this:

oo::class create tk::optiontype {
   constructor {default testCommand} {
      ... # trivial implementation that saves the params
   }

   method validate {value} {
      if {![{*}$testCommand $value]} {
         return -code error ... # error message generation
      }
      return $value
   }

   method default {} {
      return $default
   }

   self {
      # class-level definitions
   }
}

In practice, things are more complex because there are three basic ways to validate a value. In particular, there are types for which there are tests that return a boolean, types for which there are parsers that error on failure, and types that are driven by a table of permitted values. As such, tk::optiontype is actually an abstract class and there are concrete implementations of each of the validation options.

When a type is created with tk::optiontype createbool, a boolean test is expected to be provided as a command fragment that takes a single extra argument. An example of the use is this, which makes the boolean type described above:

tk::optiontype createbool boolean "false" {
    string is boolean -strict
}

When a type is created with tk::optiontype createthrow, the test instead is expected to throw an error on failure. Because this can be more complex, we assume that the value being tested is passed in the (local) variable $value. An example of the use is this, which makes the distance type described above:

tk::optiontype createthrow distance "0px" {
    winfo fpixels . $value
}

When a type is created with tk::optiontype createtable, the test is driven by tcl::prefix match and all the caller has to do is supply the table. An example of this one is:

tk::optiontype createtable justify "left" {
    center left right
}

It is up to the caller to ensure that each type's default values actually pass validation checks.

Note that all of the types above are created with unqualified names; the names are mangled internally by the above methods so that they plug in correctly into the tk::OptionType ensemble. This is implemented using the unexported Create method of tk::optiontype, for example like this:

method Create {realClass name args} {
    # Condition the class name first
    set name [namespace current]::[namespace tail $name]
    # Delegate to the concrete subclass's create method
    tailcall $realClass create $name {*}$args
}

forward createbool   my Create ::tk::BoolTestType
forward createthrow  my Create ::tk::ThrowTestType
forward createtable  my Create ::tk::TableType

Implementation

See the tip-560 branch.

Copyright

This document is placed in the public domain.

History