TIP 560: Megawidget Configure/Property Support

Login
Bounty program for improvements to Tcl and certain Tcl packages.
    Author:         Donal K. Fellows <[email protected]>
    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:

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

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:

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.

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:

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.