ActionPackage

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