Implementation of the fileselector class

fileselector.c bastiaan, martijn, jrvosse.

We need to include some headers first:

  #include <string.h>             // some standard C and C++ include files
  #include <stdlib.h>
  #include <iostream.h>

  #include <hush/event.h>         // to handle hush events
  #include <hush/string.h>        // hush string class
  #include "fileselector.h"      // The fileselector class header
  
Options to configure the widgets:
  static const char* frameopts= "-relief raised -bd 1";
  static const char* entryopts= "-width 40 -relief sunken -bg white";

  
Options to pack the widgets:
  static const char* label_pack_opts = "-side left -padx 10 -pady 10";
  static const char* entry_pack_opts = "-side right -padx 10 -pady 10 "
                                       "-fill x -expand true";

  

Constructors & destructors

The first constructor creates a dummy fileselector object which can be used for handling events from the script interface
  fileselector::fileselector() {
      _filename[0] = _pathname[0] = _mask[0] = 0;
  }

  
The 'real' constructor follows below. The use of _register() is explained in the tutorial on garbage collection.
  fileselector::fileselector(const char *p, const char* options, int istop) : frame(p,0) {
      _filename[0] = _pathname[0] = _mask[0] = 0;

      widget* top;
      if (istop) top = new toplevel(p,options);
      else top = new frame(p,options);
      _register(top);

      build_gui(top, options);  // set up GUI
      bindings();                // set up bindings
      mask("*");                 // set default mask 
      dirpath(".");              // set default directory
  }

  fileselector::fileselector(const widget *parent, const char *p, const char* options, int istop) : frame(parent, p, 0) {
      _filename[0] = _pathname[0] = _mask[0] = 0;

      widget* top=0;      // top will be deleted automatically by parent
      if (istop) top = new toplevel(parent,p,options);
      else top = new frame(parent,p,options);

      top->rename();              // rename top widget (see tutorial)
      tk->bind(name(), this);     // bind this to tcl command name
      build_gui(top, options);  // set up GUI
      bindings();                // set up bindings
      mask("*");                 // set default mask 
      dirpath(".");              // set default directory
  }


  
The destructor. Now we have garbage collection for widgets, it is no longer needed ...
  fileselector::~fileselector() { }

  

Common member functions

Here come some "ordinary" member functions you expect every fileselector to have...

Retrieving the selected filename

This function is used to retrieve the result. It grabs the mouse pointer in order to prevent any activity on other windows. Then it waits untill the fileselector window is destroyed (!) which will be the case after pressing Ok or Cancel...
  const char* fileselector::get() {
      grab(); // grab mouse pointer
      wait(); // wait until the user has made his choise
      return _filename[0] ? _filename : 0;   // contains filename or null 
  }

  

The filename mask

The following two members can be used to get and set the mask. A mask is quit common concept in fileselector. The default mask is "*".
  const char* fileselector::mask() const { return _mask; } 

  void fileselector::mask(const char *m) { // set mask
      strcpy(_mask,m);            // save mask
      _maskentry->del();          // delete entry
      _maskentry->insert(_mask);  // insert new mask 
      rescan();
  }

  

The directory path

A get and set pair for the directory path as well. Side effect: changes applications current working directory
  const char* fileselector::dirpath() const { return _pathname; }

  int fileselector::dirpath(const char *p) {
      sprintf(buf, "cd %s", p);
      if (tk->eval(buf) == OK) {                  // chdir
          strcpy(_pathname, tk->evaluate("pwd")); // save path
          _pathentry->del();                      // delete entry
          _pathentry->insert(_pathname);  // insert new path
          rescan();                       // rescan directory
          return OK; 
      } else {
          sprintf(buf, "Could not chdir to %s", p);
          tk->error(buf);
          return ERROR;
      }
  }

  

Rescanning the directory

Side effect: Using global Tcl variables fileselector::fill and i

This function does a rescan, which is needed to update the widgets after the directory path or mask has changed.

Most of the work is done by evaluating raw Tcl commands. This is a bit tricky since they use (global) Tcl variables ... The glob -nocomplain .* * command returns a list of file and directory names of the current working directory. The foreach command is used to loop over this list. Only the directories are put the listbox, so they will appear before all other files.

Another glob command is used to retrieve a list of all filenames matching the pattern in _mask. This list is sorted by the lsort command and again, we use a foreach command to insert the filenames in the listbox.

  void fileselector::rescan() {
      _listbox  ->del();      // delete listbox
      
      // put all directories in listbox 
      sprintf(buf,"foreach i [glob -nocomplain .* * ] "
              "{ if [file isdirectory i] "
              "{%s insert end i }}",
              _listbox->path() );
      tk->eval(buf); 
      
      // put files in fileselector::fill variable after mask with glob
      sprintf(buf,"set fileselector::fill [glob -nocomplain %s]", _mask); 
      tk->eval(buf);
      
      // put files from variable in listbox
      sprintf(buf,"foreach i [lsort {fileselector::fill}] "
              "{ if [file isfile i] {%s insert end i }}", 
              _listbox->path() );
      tk->eval(buf); 
  }

  

The graphical user interface of the fileselector

This function is pretty long. It build the complete GUI. Maybe I should have split it up into several smaller functions... Note: GUIs like this need to be prototyped interactively!

We will put everything in a widget called "top". This can be a toplevel or a frame widget. Note: we need access to some of the widgets (the listbox, buttons and entries) in other member functions as well. These widget are defines as instance variables in the header file. The other widgets are defined as local variables in this function.

  void fileselector::build_gui(widget* top, const char* opts) {
      // Need some frames to pack things right. All need the same options.
      // Three frames for the three entries and their labels 
      // (file, path and mask), one frame for the listbox and the 
      // associated scrollbar, and finally one to frame the buttons:
      frame* _fileframe   = new frame(top, "fileframe",  frameopts);
      frame* _maskframe   = new frame(top, "maskframe",  frameopts);
      frame* _pathframe   = new frame(top, "pathframe",  frameopts);
      frame* _boxframe    = new frame(top, "boxframe",   frameopts);
      frame* _buttonframe = new frame(top, "buttonframe",frameopts);
      
      // Now we can create the file, path and mask entry boxes.
      _fileentry = new entry(_fileframe, "fileentry", entryopts);
      _pathentry = new entry(_pathframe, "pathentry", entryopts);
      _maskentry = new entry(_maskframe, "maskentry", entryopts);
      
      // Create labels to tell user what all these entries mean:
      label* _filelabel = new label(_fileframe, "filelabel", "-width 5");
      label* _masklabel = new label(_maskframe, "masklabel", "-width 5");
      label* _pathlabel = new label(_pathframe, "pathlabel", "-width 5");

      _filelabel->text("File:");
      _masklabel->text("Mask:");
      _pathlabel->text("Path:");
      
      // Create a listbox to show the files of the current dir.
      // We need a scrollbar for large directories.
      _listbox   = new listbox(_boxframe, "box", "-width 40 -height 10");
      scrollbar* _scrollbar = new scrollbar(_boxframe, "scroll");
      _scrollbar->yview(_listbox); 
      _listbox->yscroll(_scrollbar);
      
      // An Ok and Cancel button are pretty basic as well:
      _okbutton = new button(_buttonframe, "ok");
      _okbutton->text("Ok"); 
      
      _cancelbutton = new button(_buttonframe, "cancel");
      _cancelbutton->text("Cancel"); 
      
      // Pack all widgets. 
      // The listbox, scrollbar and the associated frame are a
      // bit special: they should fill and expand if the
      // fileselector window is resized by the user.
      // The entries only expand in the x dimension.
      // The buttonframe is packed before the boxframe to avoid
      // that the buttons disappear when the window gets to small.
      // See packer tutorial for more info...
      _fileframe->pack(); 
      _maskframe->pack();
      _pathframe->pack(); 
      _buttonframe->pack("-side bottom -fill x"); 
      _boxframe ->pack("-fill both -expand true");
      
      _filelabel->pack(label_pack_opts);
      _masklabel->pack(label_pack_opts);
      _pathlabel->pack(label_pack_opts);
      
      _fileentry->pack(entry_pack_opts);
      _maskentry->pack(entry_pack_opts);
      _pathentry->pack(entry_pack_opts);
      
      _scrollbar->pack("-side right -fill y -expand false");
      _listbox  ->pack("-side left -fill both -expand true"); 
      
      _cancelbutton->pack("-side right -padx 10 -pady 10");
      _okbutton    ->pack("-side left  -padx 10 -pady 10");
      
      options(opts);  // process options
      redirect(top);  // Redirect self() to top widget
  }

  

User interaction: Bindings

The buttons and entries generate events if they are pressed, hit, etc. All we have to do is to bind the event to an object which is capable to process it. Such an object is called a handler. The fileselector itself is a pretty good handler, since it is derived (via toplevel and widget) from the handler class. So all events will be handled by "this". We want to handle events corresponding to pressing the buttons (ok or cancel), selecting a file from the listbox (select), and hitting Return in the entry widgets. A Return in the file entry is interpreted to be the same as pressing the Ok button. Return in the mask entry should set a new mask and update (rescan) the directroy contents in the listbox. A Return in the path entry should probably result in a change dir, and an update of the filebox. We deal with this actions in the handlers application operator()(). In general, it is convenient to supply an extra argument to the event, so we can determine what actually happened.
  void fileselector::bindings() {
      _cancelbutton->bind(this, "Cancel");            // cancel pressed
      _okbutton->bind(this, "Ok");                    // ok button pressed
      _listbox->bind(this, "Select-Double" );  // double click is default binding
      _listbox->bind("<Button-1>", this, "Select" ); // single click
      _fileentry->bind("<Return>", this, "Ok") ;      // like pressing ok
      _maskentry->bind("<Return>", this, "MaskReturn") ; // new mask
      _pathentry->bind("<Return>", this, "PathReturn") ; // new path
  }

  

Event types

There are two types of events:
  1. Events generated by the Tcl interpreter, since our fileselector can be used as a Tcl command as well.
  2. Events generated by the fileselector's components, as defined above.
As an example of the first event, assume a user feeding the following code to the interpreter:
   hush> fileselector .f                # Generates event
   hush> ...
   
The statement should create a new fileselector called .f

Creating widgets

This event is typical for all widgets and a special member functions is designed to handle this: the create() function defined below.

Dispatching of other event

The default dispatch() member of the widget class will call the create() function to handle the event discribed above. Additionally, it will check the widget's jump table to see if one of the entries matches the current event. If both fail, it will call the widgets operator to handle the "unknown" event.
  widget* fileselector::create(int argc, char* argv[]) { // create new widget
      fileselector *f = new fileselector(argv[0]);    
      f->options(flatten(--argc, ++argv));            // process options
      tk->result(f->path());                          // return path to tk
      _register(f);                                   // register for deletion
      return f;
  }

  int fileselector::operator()() {  
      int argc = _event->argc(); char **argv = _event->argv();
      string first = argv[0];
      if (first == type()) { 
          create(--argc, ++argv);
          return OK;
      }
      string s = _event->arg(1);
      if (s == "Select" || s == "Select-Double") {
          // A select on the listbox:
          _fileentry->del(); // delete old filename
          
          int index = _listbox->selection();
          char* sel = _listbox->get(index);
          char* selection = new char[strlen(sel) +1];
          strcpy(selection, sel);
          
          // Perhaps the selection is a directory ...
          sprintf(buf,"file isdirectory \"%s\"", selection);
          int isdir = atoi(tk->evaluate(buf));
          
          if (isdir) {
              if (s == "Select-Double") dirpath(selection);
          } else {
              _fileentry->insert(selection); 
              if (s == "Select-Double") {
                  strcpy(_filename, _fileentry->get());
                  destroy();
              }
          }
          delete selection;
      } else if (s == "Cancel") {
          strcpy(_filename, "");  // Empty string on cancel
          destroy(); 
      } else if (s == "Ok") {
          strcpy(_filename, _fileentry->get());
          destroy(); 
      } else if (s == "MaskReturn") {
          // A [Return] in the mask entry. Update mask and listbox
          mask(_maskentry->get());
      } else if (s == "PathReturn") {
          // A [Return] in the path entry. Change directory.
          dirpath(_pathentry->get());
      } else if (s == "get") {  
          tk->result(get());          // Pass selected filename to toolkit
      } else if (s == "rescan") { 
          rescan();
      } else self()->eval(quote(--argc, ++argv));
      return OK;
  }

  void fileselector::options(const char*) { 
      // not implemented ...
  }

  

Error handling

An error has occured, probably in the arguments of the script command. Print usage and return error.
  int fileselector::usage(const char* msg) const {
      cerr << type() << ": " << msg << endl;
      cerr << "Usage: " << endl;
      cerr << type() << " <pathname> [options]" << endl;
      cerr << "Or: " << endl;
      cerr << "<pathname> get" << endl;
      cerr << "<pathname> rescan" << endl;
      return ERROR;
  }