Go up one levelGo to Previous Go to Next

Extending Skycat

There is an almost endless list of features that you could add to an application like skycat. Some of the features are of general use, while many others are specific to a certain project or telescope. This section describes some of the ways you can add new features or modify existing features without having to make any changes in the skycat source code. This is important, since it saves a lot of work merging source code after every new version comes out.

Plugins

Plugins are defined here as Tcl procedures that are called to extend skycat and add new features. Skycat supports two types of plugins: one at the widget level, which is called for each instance of a top level widget, after it has been constructed, and one at the application level, called once for the application, before any widgets are created. The Tcl plugin procedure can do something simple, such as add a new menu item with a new feature, or something quite complex, including replacing the main application class with a derived class and dynamically loading Tcl packages from shared library files.

Widget Level Plugins

Nearly all of the top level widgets used in skycat support plugins . This is a simple feature inherited from the TopLevelWidget base class. For any given widget class Foo that is a subclass of TopLevelWidget , a Tcl proc named Foo_plugin many be defined. The plugin proc is called for each instance of that class, after the class construction is complete (after calling the init method), with the name of the class instance as an argument.

The plugin source files are located as follows: If the environment variable FOO _PLUGIN is defined (replace FOO with the Itcl class or application name in upper case), it is assumed to be a colon separated list of plugin source file names, or the names of directories containing plugin files, or containing subdirectories with plugin files (see the example source tree below).

In the case of skycat, you could define the environment variable SKYCAT_PLUGIN , since skycat is the name of the application and also the name of the main class. You could also define other environment variables, such as SKYSEARCH_PLUGIN , for other toplevel Itcl classes, if you want to have a Tcl proc be called for each instance.

Example:

 
setenv SKYCAT_PLUGIN "$dir1/myplugin1.tcl:$dir2/myplugin2.tcl:$dir3"

In the above example, the two source files ( $dir1/myplugin1.tcl and $dir2/myplugin2.tcl ) will be loaded as plugins, as well as any file named $dir3/${classname}_plugin.tcl or $dir3/*/${classname}_plugin.tcl. Probably the simplest way to organize the plugins is to define a single top level plugin directory and create a subdirectory for each plugin.

You can put plugin procedures for more than one class in a single plugin file, or define environment variables and create files for each one separately. If the plugin procedure is defined, it will be called once for each instance of the class. The directory containing the plugin file is automatically appended the tcl auto_path variable, so that you can easily split the plugin into different source files in that directory, as long as it contains a tclIndex file.

The plugin proc can be used to add features to widgets, such as additional menus or buttons. Once you have the handle for the top level widget, it is usually easy to access other internal widgets, if necessary, to make any changes or additions you want. The Tcl language is also very flexible and will allow you to redefine procedures and methods at run time.

Example Widget Level Plugin

Below is an example plugin file for the SkyCat class. To use this plugin, you have to define the environment variable SKYCAT_PLUGIN first, for example:

 
setenv SKYCAT_PLUGIN your-pathname
/SkyCat_plugin.tcl

The source code for SkyCat_plugin.tcl is shown below:

 
proc SkyCat_plugin {this} { 
    set w [info namespace tail $this] 
    add_graphics_features $w 
}

This plugin is used to add some menu items to the Graphics menu for saving line graphics to a file in world or image coordinates so that they can be reloaded again later. The proc has to be called SkyCat_plugin , since the main Itcl class name is SkyCat . The argument $this is the name of an instance of the SkyCat class and is needed in order to be able to call methods. The variable w is set to the name of the top level window, which is the same string, but without the Itcl namespace information. You can usually use $w in place of $this , since the namespaces we are using ( skycat , cat , rtd , etc.) are imported by default. $w has the advantage that it can be used to refer to both the Tk widget and the Itcl class instance.

The code for the add_graphics_features proc is shown below:

 
proc add_graphics_features {w} { 
    set m [$w get_menu Graphics]
    $m add separator
    $w add_menuitem $m command "Save graphics..." \ 
        {Save line graphics to a file} \ 
        -command [code save_graphics $w]
 
    $w add_menuitem $m command "Load graphics..." \ 
        {Load line graphics from a file} \ 
        -command [code load_graphics $w] 
}

The methods used here to add menu items are described in the man page for TopLevelWidget in the tclutil package documentation. We first get the handle for the Graphics menu, and then add the two items to it, including a short help text and a Tcl command to be called to do the work.

We might have wanted to create a new menubutton, " Annotations ", rather than adding items to the existing one. The code to do that would look something like this:

 
proc add_graphics_features {w} { 
    set m [$w add_menubutton Annotations]

    $w add_menuitem $m command "Save annotations..." \ 
        {Save line graphics to a file} \ 
        -command [code save_annotations $w] 
 
    $w add_menuitem $m command "Load annotations..." \ 
        {Load line graphics from a file} \ 
        -command [code load_annotations $w] 
}

When the user selects the " Save graphics " (or " Save Annotations ") menu item, a tcl proc save_graphics is called and that is where the real work begins.

We could also have put this code in an Itcl class, however this plugin does not have its own window (it only adds the menu items), so it makes sense to use plain Tcl procs here. It is usually easiest to define one Itcl class per widget (frame or toplevel).

The save_graphics proc is a bit long, so we'll take it a step at a time as an example. The first thing we have to do is find out the name of the file in which to save the graphics:

 
set filename [filename_dialog] 
if {"$filename" == ""} { 
    return 
}

The Tcl proc filename_dialog is one of a collection of simple dialog procedures provided by the tclutil package. It pops up a file browser and returns the user's selection, or an empty string if no file was selected. So now we know the file. We can check if it exists already, and ask if we should overwrite it:

 
if {[file exists $filename]} { 
    if {! [confirm_dialog "File `$filename' exists. Overwrite it?"]} { 
        return 
    } 
}

confirm_dialog is another tclutil dialog that displays a message and gets a yes or no answer ( OK , Cancel ) from the user. Before we go any further, here is a short overview of the simple dialogs that are available in this environment:

Dialogs defined in the Tclutil Package

filename_dialog

Choose a file with a file browser.

confirm_dialog

Ask for confirmation before doing something.

error_dialog

Report an error message.

info_dialog

Display a message.

choice_dialog

Ask the user to make a choice form a number of items.

input_dialog

Ask the user to type something in.

When we save the graphics, we have to ask whether to save the coordinates as world coordinates or image pixel coordinates.

 
set choice [choice_dialog \ 
    "Please select the type of coordinates to save the graphics in:" \ 
        {{World Coordinates} {Image Coordinates} Cancel} \
        {World Coordinates} $w] 
if {$choice == "Cancel"} { 
    return 
elseif {$choice == "World Coordinates"} { 
    set units {deg J2000} 
} else { 
    set units image 
}

Here we set units to either " deg J2000 " for world coordinate degrees in J2000 or " image " for image pixel coordinates. This is the syntax that is supported by the rtd , along with other types, such as " canvas " and " screen ". We will use $units below to convert the coordinates of the graphic items from canvas to $units coordinates. But first, we need to open the output file and write the first line of output indicating the units of the coordinates.

 
if {[catch {set fd [open $filename w]} msg]} { 
    error_dialog $msg 
    return 
}
puts $fd "set units \"$units\""

Now we are almost ready to do some work. We still need access to the canvas widget holding the image and graphics, the rtdimage object that displays the image in the canvas, and the graphics editor object, draw , of class CanvasDraw , that is normally used to create graphic items and setup bindings for moving and resizing the objects.

 
set canvas [$w component image component canvas] 
set image [$w component image get_image] 
set draw [$w component image component draw] 
$draw deselect_objects

Here we used the Itk component method to access the canvas component of the SkyCat image window. Actually we are going two levels down here, that is why there are two " component " references in the first line. The above example could also be coded, perhaps a little more efficiently as follows:

 
set im [$w component image]
set canvas [$im component canvas] 
set image [$im get_image] 
set draw [$im component draw] 
...

The second line gets the handle of the internal rtdimage object, which we want to use for coordinate conversion. This is not a widget, but the Tk image type called rtdimage , which is implemented in C++. We could use the Itcl class object (of type SkyCatCtrl(n) ) returned by [$w component image] , since it implements the same methods as the internal rtdimage object (by forwarding them), however, it is more efficient to use the object directly when possible.

The third line in the example gets the handle of the CanvasDraw object used to manage the line graphics and uses it to make sure no objects are selected, since we don't want to save the selection handles along with the graphics.

Now we are ready to save the canvas graphics to the file. We can get the list of graphic objects and all of the information we need about them from the canvas widget and use the rtdimage object to convert the coordinates to the required units.

In the loop below, we write one line for each canvas item (except for image items, which we ignore here). Each line has the format of a Tcl list of the form { type coordinates configOptions }, where:

The convert_coords proc converts a list of coordinates from one units to another using an rtdimage object, which provides subcommands for this based on the image's FITS header, assuming the image supports world coordinates. The list of coordinates ( coords ) might contain only two values {x y} or multiple points {x0 y0 x1 y1 x2 y2 ... xn yn} and the return value is a list with the new units.

 
proc convert_coords {coords from_units to_units image} { 
    set result {} 
    set len [llength $coords] 
    for {set i 0} {$i < $len} {incr i 2} { 
        set ix [lindex $coords $i] 
        set iy [lindex $coords [expr $i+1]] 
        $image convert coords $ix $iy $from_units x y $to_units 
        lappend result $x $y 
    } 
    return $result 
}

The procedure for loading the graphics back is not shown here, but can be found in the source distribution in the skycat/demos directory.

Note that you can also define plugin procedures for other top level windows. For example, for the catalog window, the proc would be called SkySearch_plugin .

Application Level Plugins

Some tasks might require more than just adding a new menu item to the existing window. You might need to dynamically load shared libraries for new Tcl packages or even replace the main application widget ( SkyCat(n) ) with a new derived class widget, which modifies the default behavior.

An application plugin is defined in the same way as the widget plugins described above, by defining the environment variable SKYCAT_PLUGIN as a colon separated list of files or directories. However in this case, the plugin source file does not only (or necessarily) define the SkyCat_plugin procedure, but also includes Tcl commands to execute at the global level. This works because the plugin files for the main Itcl application class ( SkyCat ) are sourced before any widgets are created (other plugin files are loaded on demand as needed).

Although the plugin procedure is only called after a widget is created, the file for the main widget is sourced before the first instance is created, giving you a chance to execute code, where you can, among other things, redefine the definition of the rtdimage Tk image type to include new commands defined in a C++ subclass of Skycat (which is a subclass of RtdImage ).

Before creating the first instance of the main skycat window, the application plugin files are sourced . In the plugin code, you can set the global Tcl variable mainclass to the name of a new Itcl class derived from the SkyCat widget class. The code that creates the main window will then use that class in place of the SkyCat class. This is one way to gain full control of the application without modifying it and also allows you to add new command line options, since these are the same as the options for the main application class widget.

Example Application Level Plugin

Here is a very simple example of an application plugin, not for Skycat , but for Rtd . Just to demonstrate how it works, we could make Skycat be a plugin for Rtd , so that when we define the environment variable RTD_PLUGIN to point to this file (or a directory containing the file Rtd_plugin.tcl) and then start " rtd ", the window that actually comes up will be the skycat main window:

 
# assumes these environment variables are defined
 
lappend auto_path $env(CAT_LIBRARY) $env(SKYCAT_LIBRARY) 
 
# load the required packages
 
foreach pkg {Cat Skycat} { 
    if {[catch {package require $pkg} msg]} { 
        puts "error loading $pkg package: $msg" 
        return 
    } 
}
 
# use this class for the main window
 
set mainclass SkyCat

A similar plugin can also be defined for skycat. There is one small problem though with redefining the main application class as we did above, since this will not work for more than one plugin. We could achieve a similar result by simply loading the Cat and Skycat Tcl packages and then adding the Data-Servers menubutton to the menubar:

 
# assumes these environment variables are defined
 
lappend auto_path $env(CAT_LIBRARY) $env(SKYCAT_LIBRARY) 
 
# load the required packages
 
foreach pkg {Cat Skycat} { 
    if {[catch {package require $pkg} msg]} { 
        puts "error loading $pkg package: $msg" 
        return 
    } 
}
 
proc Rtd_plugin {this} { 
    set w [info namespace tail $this] 
    set image [$w component image] 
     
    # set X defaults 
    cat::setXdefaults 
    skycat::setXdefaults 
 
    # add the catalog menu 
    AstroCat::add_catalog_menu $this $image ::skycat::SkySearch 0 
}

We would have to add a little more code to get everything just right (for example, loading the skycat Xdefaults cause the new menu button to have a different color than the others), but this should give you an idea of how it works.

Subclassing

If you are planning on making changes or additions to skycat, you may want to define subclasses of the existing Itcl or C++ classes and redefine selected methods to do what you want. This is the normal way of doing things within skycat and related packages and can be used for new applications as well as for skycat plugins.

For example, skycat's main window is a subclass of the rtd main window. The catalog window is a subclass of the AstroCat class. By defining a derived class, you can easily add buttons or menu items to existing classes. By looking at the source code and/or documentation and redefining selected methods, you can also change or add to the existing behavior.

One important thing to note here is the role of the Itcl class constructor and the init method. You should not make any assumptions about the state of a widget or the options while in the constructor. The base classes TopLevelWidget and FrameWidget both call the init method, using an " after idle " handler, after all constructors in the class hierarchy have completed.

Another thing to watch out for, when redefining methods, is to keep them compatible with the original versions. It is best to call the parent class version of a method first, and then add your own code, unless you are sure that that is not what you want.


Go up one levelGo to Previous Go to Next

Please send questions or comments to abrighto@eso.org.
Copyright © 1998 ESO - European Southern Observatory