Version 3 of ActionPackage

Updated 2004-05-25 15:17:02

-- Bryan Schofield 25 May 2004 --

This page contains an implementation of a package to that provides the action concept to Tcl.

See Actions for an introduction to the action concept.

See ActionPackageDemo for source code to a demo program that uses this package.

 # action.tcl --
 #
 #       This file provides the complete package that introduces the concept of
 #       "actions" to Tk. All code is contained with the ::action or ::action
 #       decedent namespaces.
 #
 # Copyright (c) 2003 Bryan Schofield
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to
 # deal in the Software without restriction, including without limitation the
 # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 # sell copies of the Software, and to permit persons to whom the Software is
 # furnished to do so, subject to the following conditions:
 #
 # The above copyright notice and this permission notice shall be included in
 # all copies or substantial portions of the Software.
 #
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 # IN THE SOFTWARE.
 #
 #
 # TERMS AND DEFINITIONS
 #
 # action        A collection of options and values that can be set and queried
 #               at a single data repository but applies to any number of Tk
 #               widgets. These options, typically refered to as "configuration
 #               options", may not be applicable to all widgets. Under these
 #               circumstances, the said options are simply ignored for some
 #               widgets.
 #
 # applicator    A procedure that is capable of extracting useful configuration
 #               options from an action and appling the values of those options
 #               to a particular class of widgets.
 #
 # validator     A procedure that can ensure the validity of an action
 #               configuration option. Validator procedures are invoked during
 #               action configuration and can generate errors if option values
 #               are invalid.
 #
 #
 #
 # THE ACTION FRAMEWORK
 #
 # The action framework, or Framework, consists of mechanisms for specifying
 # action options, configuring or querying action options, and application of
 # action configuration options to Tk widgets. The Framework itself does not
 # provide details of what an action consists of or how the actions are applied
 # to common Tk widgets. However, this package provides implementations for
 # applying actions to Button and Menu class widgets.
 # See ::action::initializeDefaults
 #
 #
 #
 # COMMANDS
 # 
 # Details on commands can be read for specific commands at each command's proc
 # definition. A summary of the widely used commands is provided here.
 #
 #
 # Adding, removing, and querying action configuration options
 #
 #  ::action::addOption name defaultValue ?validationCmd?
 #  ::action::removeOption name
 #  ::action::getOptionList
 #
 #
 # Setting and querying widget class applicators
 #
 #  ::action::setApplicator class applicatorCmd
 #  ::action::getApplicator class
 #
 #
 # Creating, deleting, configuring, and querying actions
 #
 #  ::action::create act ?option value ...?
 #  ::action::delete act
 #  ::action::exists act
 #  ::action::cget act opt
 #  ::action::configure act ?option value ...?
 #
 #
 # Applying, removing, and querying relationships bewteen actions and widgets
 #  ::action::apply act args
 #  ::action::remove act args
 #  ::action::widgets act
 #
 #
 #
 # DEFAULT ACTION CONFIGURATION OPTIONS AND WIDGET CLASS APPLICATORS
 #
 # -text         The text or label associated with an action
 # -image        The image associated with an action
 # -command      The command to evaluated when an action is invoked
 # -state        The state of the action, which can be "normal" or "disabled"
 #
 # Button        Sets -text, -image, -command, -state options according to the
 #               action and -compound to "left"
 # Menu          Adds or modifies a menu entry matching the text string value
 #               of the -text action option. Sets the -image, -command, and
 #               -state  options according to the actions, the -label according
 #               to the -text action option, and -compound to "left".
 #
 # * ATTENTION *
 # The Menu applicator uses the "-text" value to identify which entry, if any,
 # in a menu corresponds to the action. This is done by trying to find a menu
 # entry index by pattern matching. See the menu man page or documentation for
 # more details on menu pattern matching for indices. If the menu has an entry
 # with the same -label value as the action's -text value, that menu entry will
 # be considered to be associated with the action. In short don't do this and
 # expect the Menu applicator to know that you want to *add* a new entry
 # instead of *modify* an existing entry:
 #
 #   ::action::create a -text "Hello" -command "doSomething"
 #   menu .m
 #   .m add command -label "Hello" -command "doSomethingElse"
 #   ::action::apply a .m
 #   # BAD! The action just overrode the -command menu option for
 #   # manually configured menu entry.
 #
 # Having said that, it safe to change the action -text option. The menu
 # applicator will know to change the existing entry instead of creating a new
 # one.
 #
 #   ::action::create a -text "Hello" -command "sayHello"
 #   menu .m
 #   ::action::apply a .m
 #   ...
 #   ::action::configure a -text "Goodbye" -command "sayGoodbye"
 #   # GOOD! The action just changes the label and command of the
 #   # existing menu entry for "a"
 #
 #
 #
 # TYPICAL USAGE
 #
 # package require action
 # namespace eval ::img {}
 # image create photo ::img::myImg -file myimage.gif
 # ::action::create myAction \ 
 #     -text "Do Something" \ 
 #     -image ::img::myImg \ 
 #     -command [list myCommand]
 # button .b1
 # button .b2
 # menu .menubar
 # menu .popup
 # ::action::apply myAction .b1 .b2 .menubar .popup
 #    ...
 # ::action::configure myAction -state "disabled"
 #    ...
 # ::action::configure myAction -state "normal" -text "Tun Sie Etwas"
 #
 #
 #
 #
 # ADVANCED USAGE
 #
 # # add a new option for Superframe class widgets
 # proc ::action::validator::superopt { value } {
 #    if { ... } {
 #       # $value is not ok!
 #       return -code error "invalid superopt value \"$value\", must be ..."
 #    }
 # }
 # ::action::addOption -superopt "Super Default" ::action::validator::superopt
 #
 # # add a new widget class, Superframe to accept actions
 # proc ::action::applicator::Superframe {widget act} {
 #    foreach optSet [::action::configure $act] {
 #       switch -- [lindex $optSet 0] {
 #           -text     {# do something to $widget}
 #           -image    {# do something to $widget}
 #           -command  {# do something to $widget}
 #           -state    {# do something to $widget}
 #          -superopt {# do something to $widget}
 #      }
 #   }
 # }
 #
 # ::action::create superAction \
 #     -text "Do Something" \ 
 #     -image ::img::myImg \ 
 #     -command [list myCommand] \ 
 #     -superopt "Be super!"
 #
 # Superframe .sf
 # ::action::apply superAction .sf
 #
 #

 package require Tcl 8.4
 package require Tk 8.4
 package provide action 1.0


 namespace eval ::action {
    # a namespace for containing procs that validate values for options
    namespace eval validator {}
    # a namespace for containing procs that apply actions to widgets
    namespace eval applicator {}

    # array of commands used to apply actions to classes of widgets
    # key is widget class
    variable applicator
    array set applicator {}


    # default option/value array
    # this contains option names as keys and default values
    variable option
    array set option {}
    variable validator
    array set validator {}


    # the action data array
    variable action
    array set action {}
 }




 # ::action::addOption --
 #
 #       Adds an option to the action framework. Action are able to configure
 #       this option immediately after the option was added. Existing actions
 #       will inherit default values.
 #
 # Arguments:
 #       name          The name of the option
 #       defaultValue  The option default value
 #       validationCmd A tcl command to be evaluated when this option is 
 #                     configured. This command will be passed the value of the
 #                     option and should generate an error if the value is 
 #                     invalid.
 #
 # Results:
 #       Error if the option name has white spaces or upper case letters
 #       Nothing if successful
 #
 proc ::action::addOption {name defaultValue {validationCmd ""}} {
    # if the name has capital letters or white space, reject it
    if {![regexp {^-?([a-z]|[0-9])+$} $name]} {
       return -code error "invalid option name \"$name\", names must be all lower case and can not have white spaces"
    }
    # make sure the first character is a "-"
    if {[string index $name 0] != "-"} {
       set name "-$name"
    }
    # set the default value of this option, if one already exists, then we will
    # just override it
    variable option
    variable validator
    set option($name) $defaultValue
    set validator($name) $validationCmd
    return
 }





 # ::action::removeOption --
 #
 #       Removes an option from the action framework.
 #
 # Arguments:
 #       name    The name of the option
 #
 # Results:
 #       Error if the option of the name
 #       Nothing if successful
 #
 proc ::action::removeOption {name} {
    variable option
    variable action
    variable validator
    # make sure the first character is a "-"
    if {[string index $name 0] != "-"} {
       set name "-$name"
    }
    if {![info exists option($name)]} {
       return -code error "action option \"$name\" does not exist"
    }
    # remove any references that existing action may have
    foreach act [array names action] {
       unset -nocomplain action($act,$name)
    }
    # remove the default option/value
    unset option($name) validator($name)
    return
 }




 # ::action::getOptionList --
 #
 #       Get a list of options, default values, and validator commands in the
 #       action framework. The list format is:
 #          {{option defaultValue validatorCmd}
 #           {option defaultValue validatorCmd} ..}
 #
 # Arguments:
 #       none
 #
 # Results:
 #       List of options, default values and validator commands
 #
 proc ::action::getOptionList {} {
    variable option
    variable validator
    set optSet {}
    foreach opt [lsort [array names option]] {
       lappend optSet [list $opt $option($opt) $validator($opt)]
    }
    return $optSet
 }




 # ::action::initializeDefaults --
 #
 #       This command sets up a set of default options and widget class
 #       handlers in the action frame work
 #
 # Arguments:
 #       none
 # 
 # Results:
 #      none
 #
 proc ::action::initializeDefaults {} {
    ::action::addOption -text ""
    ::action::addOption -image "" ::action::validator::image
    ::action::addOption -command "" 
    ::action::addOption -state "normal" ::action::validator::state
    ::action::setApplicator Button ::action::applicator::Button
    ::action::setApplicator Menu ::action::applicator::Menu
    return
 }



 # ::action::create --
 #
 #       Creates a new action with a given name and configures it according to
 #       any specified options.
 #
 # Arguments:
 #       act     The action name. This must be a unique name
 #       args    Options configuration arguments
 #
 # Returns:
 #       Error if an action by the specified name already exists
 #       Error if any of the configuration options are invalid
 #       The action name if successful
 #
 proc ::action::create {act args} {
    if {[::action::exists $act]} {
       return -code error "action \"$act\" already exists"
    }

    variable action
    # the list of widgets associated with the action
    set action($act,widgets) {}
    set action($act,previousConfig) {} ; # this will get set in the "configure" below
    # if we catch an error configuring the options, make sure we clean up
    # anything that we might have created
    if {[catch {eval ::action::configure $act $args} err]} {
       catch {::action::delete $act}
       return -code error $err
    }
    return $act
 }





 # ::action::delete --
 #
 #       Deletes an action from the action framework
 #
 # Arguments:
 #       act     The action name
 # 
 # Results:
 #       none
 #
 proc ::action::delete {act} {
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    variable action
    # the list of widgets associated with the action
    unset -nocomplain action($act,widgets) action($act,previousConfig)
    array unset action "$act,*"
    return 
 }




 # ::action::exists --
 #
 #       Determines if an action of the specified name exists
 #
 # Arguments:
 #       act     The action name
 #
 # Results:
 #      Returns 1 if action exists
 #      Returns 0 if action does not exist
 #
 proc ::action::exists {act} {
    variable action
    return [info exists action($act,widgets)]
 }





 # ::action::cget --
 #
 #       Get the value for an option of an action
 #
 # Arguments:
 #       act     The action name
 #       opt     The configuration option name
 #
 # Results:
 #       Error if the option name is invalid
 #       Value if the option for the action action has been configured via 
 #               "configure" method
 #       Default option value if the option for the action has not been 
 #               configured via the "configure" method
 #
 proc ::action::cget {act opt} {
    variable option
    variable action
    # make sure the action exists
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    # make sure the option is valid
    if {![info exists option($opt)]} {
       return -code error "invalid option \"$opt\", must be [array names option]"
    }
    if {[info exists action($act,$opt)]} {
       # return the action configured value
       return $action($act,$opt)
    } else {
       # return the default value
       return $option($opt)
    }
 }



 # ::action::configure --
 #
 #       Configures options for an action or returns a list describing the
 #       current configuration for the the action. The list is presented in 
 #       the following format:
 #        {{-option value defaultValue} {-option value defaultValue} ...}
 #
 # Arguments:
 #       act     The action name
 #       args    (optional) list of options and values
 #
 # Results:
 #       Error if any of the configuration options are invalid or have invalid
 #               values
 #       Nothing if options were successfully applied
 #       Current configuration list if no configuration options were specified
 #
 proc ::action::configure {act args} {
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    # if arguments were specified, then we should apply them to the action
    # if not, then we should generate a current option configuration list
    if {$args != ""} {
       return [eval ::action::applyConfigure $act $args]
    } else {
       return [::action::generateCurrentConfigurationList $act]
    }
 }



 # ::action::applyConfigure --
 #
 #       Applies configuration option to an action
 #
 # Arguments:
 #       act     The action name
 #       args    List of configuration options
 #
 # Results:
 #       Error if any of the configuration options are invalid or have invalid
 #               values
 #       Nothing if options were successfully applied
 #
 proc ::action::applyConfigure {act args} {
    variable option
    variable validator
    variable action
    set action($act,previousConfig) [::action::configure $act]
    foreach {opt value} $args {
       # make sure the option is valid
       if {![info exists option($opt)]} {
          return -code error "invalid option \"$opt\", must be [join [array names option] {, }]"
       }
       # make sure the option value is valid
       if {($validator($opt) != "")
           && [catch {eval $validator($opt) \$value} err]} {
          return -code error "option \"$opt\" has invalid value of \"$value\", $err"
       }
       # save the option value
       set action($act,$opt) $value
    }
    eval ::action::apply $act [::action::widgets $act]
    return
 }

 # ::action::generateCurrentConfigurationList --
 #
 #       Generates a list of the current configuration for the action in the tk
 #       configure style:
 #           {{-option value defaultValue} {-option value defaultValue} ...}
 #
 # Arguments:
 #       act     The action name
 # 
 # Results:
 #       configuration list
 #
 proc ::action::generateCurrentConfigurationList { act } {
    variable option
    variable action
    set config {}
    foreach opt [array names option] {
       lappend config [list $opt [::action::cget $act $opt] $option($opt)]
    }
    return $config
 }



 # ::action::setApplicator --
 #
 #       Registers an applicator for a particular class. The applicator command
 #       will be evaluated with widget name and a list of configuration options
 #       as returned by "configure". The applicator can be removed by
 #       re-registering the class with an empty string
 #
 # Arguments:
 #       class           The widget class
 #       applicatorCmd   The command to evaluate to apply an action to a class
 #                       of widgets.
 #
 # Results:
 #       none
 #
 proc ::action::setApplicator {class applicatorCmd} {
    variable applicator
    if {$applicatorCmd == ""} {
       unset -nocomplain applicator($class)
    } else {
       set applicator($class) $applicatorCmd
    }
    return
 }


 # ::action::getApplicator --
 #
 #       Gets the applicator command for a particular class. If no applicator
 #       command has been registers, an empty string is returned
 #
 # Arguments:
 #       class   The widget class
 #
 # Results:
 #       A tcl command if applicator has been registered
 #       An empty string if no applicator has been registered
 #      
 proc ::action::getApplicator {class} {
    variable applicator
    if {[info exists applicator($class)]} {
       return $applicator($class)
    } else {
       return ""
    }
 }


 # ::action::apply --
 #
 #       Applies an action to a set of widgets. This is accomplished by
 #       delegating actual application to registered applicators depending on
 #       the class of the widget(s)
 #
 # Arguments:
 #       act     The action name
 #       args    List of widgets to apply the action to
 #
 # Returns:
 #       Error if action does not exist
 #       Error if widget does not exist
 #       Error if widget class has no registered applicator
 #       Nothing if successful
 #
 proc ::action::apply {act args} {
    variable action
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    foreach widget $args {
       if {$widget == ""} { continue }
       if {![winfo exists $widget]} {
          return -code error "can not apply action \"$act\" to \"$widget\", widget does not exist"
       }
       set class [winfo class $widget]
       set applicator [::action::getApplicator $class]
       if {$applicator == ""} {
          return -code error "can not apply action \"$act\" to \"$widget\", no applicator for class \"$class\" has been registered"
       }
       eval $applicator $widget $act
       if {[lsearch $action($act,widgets) $widget] == -1} {
          lappend action($act,widgets) $widget
       }
    }
    return
 }


 # ::action::remove --
 #
 #       Removes any association between an action and a set of widgets. The
 #       widgets are *not* modified as a result of doing this. It merely breaks
 #       the connection of the action and widgets for future modification to
 #       the action
 #
 # Arguments:
 #       act     The action name
 #       args    List of widgets to apply the action to
 #
 # Returns:
 #       Error if action does not exist
 #       Error if widget does not exist
 #       Error if widget class has no registered applicator
 #       Nothing if successful
 #
 proc ::action::remove {act args} {

    variable action
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    foreach widget $args {
       if {$widget == ""} { continue }
       set i [lsearch $action($act,widgets) $widget]
       if {$i != -1} {
          set action($act,widgets) [lreplace $action($act,widgets) $i $i]
       }
    }
 }


 # ::action::widgets --
 #
 #       Returns a list widgets that the action has been applied to and still
 #       exists.
 #
 # Arguments:
 #       act     The action name
 #
 # Results:
 #       A list of widgets
 #
 proc ::action::widgets {act} {
    if {![::action::exists $act]} {
       return -code error "action \"$act\" does not exist"
    }
    variable action
    # build a list of widgets and make sure that any destroyed widgets have
    # been removed from the list
    set widgets {}
    foreach w $action($act,widgets) {
       if {[winfo exists $w]} {
          lappend widgets $w
       }
    }
    # save the new, edited list
    set action($act,widgets) $widgets
    return $widgets
 }


 #======================================================================
 # validators
 #======================================================================
 proc ::action::validator::image { imgName } {
    if {[lsearch [::image names] $imgName] == -1} {
       return -code error "image \"$imgName\" does not exist"
    }
 }

 proc ::action::validator::state { state } {
    if {($state != "normal") && ($state != "disabled")} {
       return -code error "invalid state \"$state\", must be normal, disabled"
    }
 }


 #======================================================================
 # applicators
 #======================================================================
 proc ::action::applicator::Button {b act} {
    # it's good if we just casually look to see what options the action has
    # available to us, rather than just expecting it to have some specific
    # options. You never know, someone might have removed an option. At any
    # rate, we can look at the configuration and when we see something we like,
    # apply it to the button
    foreach optSet [::action::configure $act] {
       switch -- [lindex $optSet 0] {
          -text    {$b configure -text [lindex $optSet 1]}
          -image   {$b configure -image [lindex $optSet 1] -compound left}
          -command {$b configure -command [lindex $optSet 1]}
          -state   {$b configure -state [lindex $optSet 1]}
       }
    }
 }

 proc ::action::applicator::Menu {m act} {
    # try to find the menu item in the menu that corresponds to this action.
    # we have to use the previous configuration information to find the action
    # by label because the by the time we will have been called, the action
    # might have a new -text value, which would make it impossible to find the
    # original menu entry.
    set label ""
    foreach optSet $::action::action($act,previousConfig) {
       switch -- [lindex $optSet 0] {
          -text {set label [lindex $optSet 1]}
       }
    }
    # now find the old label in the menu
    if {[catch {$m index $label} index]} {
       # hmm, no menu item exists, let's create one
       $m add command
       set index end
    }
    # now casually look to see what we can configure in the menu entry
    foreach optSet [::action::configure $act] {
       switch -- [lindex $optSet 0] {
          -text    {$m entryconfigure $index -label [lindex $optSet 1]}
          -image   {$m entryconfigure $index -image [lindex $optSet 1] -compound left}
          -command {$m entryconfigure $index -command [lindex $optSet 1]}
          -state   {$m entryconfigure $index -state [lindex $optSet 1]}
       }
    }

 }


 #======================================================================
 # Initialize defaults
 #======================================================================
 ::action::initializeDefaults

Category Actions | Category GUI