Actions

Bryan Oakley - 20-May-2004

An action is a pseudo-object that has properties that mostly resemble procs. In addition, actions have state and are generally associated with one or more widgets. The main advantage an action has over a proc is that it "knows" which widgets it is associated with and can enable or disable all of the widgets when the action itself becomes enabled or disabled.

Bryan Schofield posted to comp.lang.tcl a nice example of one implementation of actions [L1 ] (broken link 2015-05). The use of actions is also mentioned at Tk coding styles and philosophies.

See Also

ANNOUNCE: Actions for Tk , Bryan Schofield, comp.lang.tcl, 2003-11-18
Bryan Schofield's "Action" package , Jeff Godfrey, comp.lang.tcl, 2004-09-03
Includes further explanation of the system by the author.

Description

Bryan Schofield -- 24 May 2004

Here's the source for my implementation of an action package: ActionPackage and ActionPackageDemo

Following is a quick example of how actions can be used, using a simpler implementation than the one described by Bryan Schofield. This comes from the implementation I presently use (but is not yet publicly available). It's only 177 lines of code and is very straight-forward; I generally advise people to create their own implementation so they can appreciate how it works.

button .cut -text Cut -command [list action invoke cutAction]
.menubar.editMenu add command -label "Cut" -command [list action invoke cutAction]
.popup add command -label "Cut" -command [list action invoke cutAction]

action associate cutAction .cut [list .menubar.editMenu "Cut"] [list .popup "Cut"]

bind all <Control-x> [list action invoke cutAction]
...
action define cutAction {} {
    <code to cut the selected text>
}
proc toggleSelection {} {
    # called whenever the selection changes
    if {<there is a selection>} {
        action enable cutAction copyAction
    } else {
        action disable cutAction copyAction
}

Notice that the use of actions adds only one extra line of code in your application (the line that begins "action associate"), and is actually a net loss of several lines of code. Without actions you might have to code toggleSelection as:

proc toggleSelection {} {
    if {<there is a selection>} {
        .cut configure -state normal
        .menubar.editMenu entryconfigure "Cut" -state normal
        .popup entryconfigure "Cut" -state normal
    } else {
        ...
    }
}

Not only that, but without actions you also have to add code to make sure that if there is no selection that the procedure that does the cutting won't do anything if called via the accelerator.

As you can see, actions make it much easier to maintain a consistent state across all widgets that implement a particular functionality. You only have to make a single function call to disable all controls that invoke the particular action. In addition, should you add new controls (speech recognition, gestures, additional buttons or menus, etc) you do not have to hunt down all the places where you might need to enable the widgets; associate the new control with the action and the action takes care of the rest.

In addition to the sheer convenience of actions, they also provide a nice API into your application, making your scripts scriptable. The scripts can be used to implement user defined macros, automated tests, or even compound actions.

The above is just one sort of implementation. Many people might prefer a more object oriented approach. For example:

new Action cutAction {...}
cutAction button .cut -label "Cut" ...
cutAction menuitem .menubar.editMenu "Cut" ...
cutAction menuitem .popup "Cut" ...
cutAction accelerator <Control-x> .

Bryan Oakley 08-Jul-2005 Recently I've revisited my action code and have started using yet another way to associate actions to widgets. I was always bothered by having to use a separate 'action associate' command for each widget; I didn't like the loose coupling between creating the widget and associating it with an action. My current methodology looks like this:

action define cutAction {...}
action associate cutAction [button .cut ...]
action associate cutAction [menuitem .editMenu command "Cut" -command ...]

The above requires that I create a new "menuitem" command which is simply a wrapper around [.menu add command ...]. The wrapper is necessary because the standard way of adding items to a menu return an empty string. My wrapper returns a list that is the menu name and menu item identifier, which the action code handles as a special case. This allows me to create a widget and associate it to an action within a single statement, and will work for any widget or object creation command that returns a handle to the created object

No matter how actions are implemented, it's the concept that is important. Pick a style that you like and write an implementation. The first time I wrote an action library (and I'm on my 3rd or 4th iteration as I don't take code with me from employer to employer) I not only gave actions state, but I made the action definition responsible for the labels and images associated with the action. The theory was, if I wanted to change all "Cut" widgets to say "Slice" I could do "cutAction configure -label Slice" and they all would do just that.

Over time I realized that most menu items and buttons don't change their text once created so I abandoned that idea and eventually whittled my personal action library down to just a handful of lines of code.


NEM 22 May 2004: This is a really excellent idea. Perhaps you would consider adding your code to tklib? I can see a common application would be to keep toolbars and menus in sync. With that in mind, would it be feasible to go one step further and have the action command actually create the toolbutton and menu item? Maybe something like:

action .edit.cut -state normal -label "Cut" -underline 0 -accelerator Ctrl-C \
    -command ... -image CutImg
.edit.cut configure -state disabled

This would actually create two widgets and a "pseudo-widget" which just forwards on requests to the actual widgets. I can see this being perhaps a bit less flexible than the above approach, though. Also, the widget path would represent the grouping of the action (i.e. the above would create a menu item in the Edit menu, and a toolbutton grouped with other Edit actions), rather than the position in the widget hierachy, so perhaps a different separator to "." would be better. Definitely a cool idea though.

Bryan Schofield 24 May 2004 The problem with having actions create widgets is that you are actually limiting the action's usability. You are enforcing the action be tied to a menu and toolbar, when in fact it need not be applied to either. My implementation, ActionPackage is purposely made very generic where the action can be applied to any class of widget, even custom megawidgets, through the use of applicators. It's my thought if you wanted to create an action and at the same time create menu's, buttons, toolbar entries, or whatever, that you create a small helper proc:

proc myAction {actName subpath args} {
    eval action::create $actName $args
    button .buttonFrame.$subpath
    action::apply $actName .menubar.$subpath .toolbar .buttonFrame.$subpath
}

Using this approach you don't tie higher level functionality to a lower level package.

Bryan Oakley 25-May-2004: agreed. While actions can be tied to widgets, they don't have to be. A good implementation of actions, IMO, should allow actions to stand alone or be tied to almost any sort of object. With the code I use now I have the restriction that anything associated with an action must have a "configure" subcommand with a "-state" option, but even that restriction doesn't necessarily need to be there.

I also find it useful to use actions as an API for the GUI. Even without actions tied to widgets it's possible to use actions to write a script that manipulates data just as if the user were clicking on buttons.


PWQ 26 May 04, Also with regard to Tk coding styles and philosophies, it would seem that actions are in fact a counter measure to make up for limitations in Tk widgets.

While -textvariable is fine for simple labels and simple entry fields, a real UI requires much finer control. This means that for most code, changing a variable is not enough to effect the stylistic changes desired.

A proactive change would be to allow attaching variables to all widget properties, thus simplifing a grey out state change to:

set State4CutCopy disabled

Assuming that widgets could be configured as:

label .a.b.c.l1 -state @State4CutCopy
frame .a.b.c.f2 -state @State4CutCopy
label .a.b.c.f2.k1 -state @State4CutCopy

(In the above example, the @ notation is used to save creating -xxxvariable option for each current option)


Bryan Oakley 26-May-2004: I'll respectfully disagree with the notion that actions are a counter measure to make up for limitations in tk widgets. IMO, no amount of improvements to the Tk widgets can make up for what actions provide to the developer. Actions are a way of tying multiple widgets together so that a single function call can update the state of a lot of disparate widgets, as well as to provide an API for the application to enable macros, automated testing, end user scripting, etc. Even if I could use the "@" notation as a sort of -statevariable option, I'd still use actions. They are just too useful IMO.

PWQ 12 Jun 05, I have looked at the action code as posted to the wiki ActionPackage. It seems to me that the name is a misnomer. What this package appears to do IMHO is in fact act more of a template processor. You can specify an action as a list of options that can be applied to specified widgets. Each widget type needs to know how an option could be applied.

With regard to my previous comment, this package seems to have nothing in common with my statements about adding variables to any option. I will elaborate on this in another page in the future.

RLH A paper on Actions: [L2 ]


MB 19-July-2007: The concept of actions strongly reminds me of the client-observer pattern. (I'm sorry, but I don't know the "official" name...) There is a "client" object and one or more "observer" objects. The observer expresses its wish to "observe" the client by registering itself to the client. In case "something happens" to the client, it will tell each registered observer about the fact. Reading client as action, observer as widget and the "something happened"-event as enabling/disabling the action, we get the situation discussed on this page.