Making Entry Locators recognize EnterKey

by Mike Hanson

Published 1998-07-01    Printer-friendly version

NOTE: Clarion Magazine was not able to obtain an archive of the original source code for this article. If you have the source, we'd very much appreciate it if you would email it to us so we can post it for other readers.

With our transition from DOS to Windows, one of the most frustrating hurdles has been remembering to use TabKey instead of EnterKey. I think that most users have made the transition at this point, with one possible exception: it still seems obvious to me to press EnterKey to complete the entry of an EntryLocator. TabKey, somehow, doesn't seem quite right.

With this in mind, I set out to find the best solution for the problem. When I originally dealt with it a year ago, it was with hand-code. I didn't generally use EntryLocators, so manually tweaking them here and there seemed to be an acceptable solution.

I didn't want to invest the time up front to provide a general solution that would be used only a few times. Of course this type of thinking is usually wrong, and this was no exception. Now that I've got Entry locators all over the place, it's time for a better solution. First, let me discuss the basic approach so that we understand the issues involved.

Hand-Coded Solution

Although there are certainly other methods, my approach was to add the EnterKey as an "Alert" key for the control used by the Entry locator. Then I placed the following code into the "AlertKey" embed for the control:

IF KEYCODE() = EnterKey
  PRESSKEY(TabKey)
END

This works, but many programmers would consider it to be a kludge. Essentially it says, "No I didn't mean EnterKey. I meant TabKey!" This alteration works as well:

IF KEYCODE() = EnterKey
  POST(EVENT:Accepted, FIELD())
END

This looks more elegant, but it's still a kludge. Now our statement is, "No I didn't mean EVENT:AlertKey. I meant EVENT:Accepted!" However, both of these help us to understand the real issue. The Entry locator has rather specific expectations for the user's behavior. If we want it to respond differently, we'll have to trick it into thinking that everything is happening normally.

Template Solution

Solving this with a template is the next most obvious method. It saves you the trouble of adding all of the Alert keys and embeds, and it's the only solution if you are using CW 2.003 or the legacy templates in C4.

You have two options. You can create a local extension template that you would have to add to each browse in your application, or you can create a global template that automatically applies itself to all browses in your application. Personally, this modification in behavior is something that I would want to apply everywhere, so we'll use the global template solution. Additionally, our example template will be for the legacy template chain, because there's a better solution if you’re using ABC. Let's look at each section of the template:

#EXTENSION(mhEntryLocator,'EntryLocators use EnterKey'),APPLICATION

This indicates that it's an "Extension" template, which must be inserted at the "Application" level (i.e.: in the Global Extensions).

Finding Entry Locators

First of all, we must define our variables. It this case, there's just one:

#ATSTART
  #DECLARE(%EntryLocatorControl),UNIQUE
#ENDAT

This code is executed when code generation begins. We are defining a multi-valued variable to hold the names of all the entry locator controls within each procedure. The #ATSTART code is only executed once per generation cycle. Because this is a global template, it will occur before any procedures are generated.

#AT(%GatherSymbols)
  #PURGE(%EntryLocatorControl)
  #FOR(%ActiveTemplate),WHERE(%ActiveTemplate = 'BrowseBox(Clarion)')
    #FOR(%ActiveTemplateInstance)
      #INSERT(%CollectEntryLocatorControls)
    #ENDFOR
  #ENDFOR
#ENDAT

The %GatherSymbols embed is encountered once for each procedure. You can think of this as the next thing that happens after the procedure's own #ATSTART. If you look at the BrowseBox control template's source code, you'll see that it initializes all of its variables in its #ATSTART section. This means that we can peek at them (and even modify them, if necessary) using the %GatherSymbols embed.

In this case, we purge the %EntryLocatorControl variable, then look through all the instances of the BrowseBox template. %ActiveTemplate is a built-in multi-valued symbol containing all of the template types, while %ActiveTemplateInstance contains the list of actual occurrences of that template within the procedure. Therefore, we must home in on the desired %ActiveTemplate, then process each of its %ActiveTemplateInstances.

For each browse box, we need to take note of any entry controls that are defined. To achieve this we call the %CollectEntryLocatorControls group. The group looks like this:

#GROUP(%CollectEntryLocatorControls),AUTO
  #ALIAS(%AccessID, %BrowseAccessID,%ActiveTemplateInstance)
  #ALIAS(%LocatorType, %BrowseLocatorType,%ActiveTemplateInstance)
  #ALIAS(%LocatorControl, %BrowseLocatorControl,%ActiveTemplateInstance)
  #FOR(%AccessID),WHERE(%LocatorType = 'Entry')
    #ADD(%EntryLocatorControl, %LocatorControl)
  #ENDFOR

This is a rather interesting section of template code. The AUTO attribute indicates that we will be declaring variables (actually ALIASing), and we don't need them after the group is finished executing (i.e.; local rather than global variables).

Templates #DECLARE variables while the code is being generated. Global template variables are available to all procedure templates. However, procedure template variables (e.g.: BrowseBox's %AccessID) are coming in and out of scope as the procedures are generated. If your template is a child of another procedure template (i.e.; it uses the REQ(ParentTemplate) attribute), then you can access your parent's variables. Otherwise, those variables are out of scope.

#ALIAS provides a solution to scoping problems at generation time. You specify your own name for the variable, the name of the variable in the other template, and the other template's instance number. Then you can access those variables as if they were your own.

%AccessID is a multi-valued variable containing entries for all of the browse conditions, plus the default condition. Associated with this variable is %LocatorType (as specified by the developer) and %LocatorControl (the name of the locator entry control), if applicable. All we have to do is loop through the access IDs, looking for "Entry" locator types. When we find one, we add the locator control to our own %EntryLocatorControl multi-valued variable.

Adding the EnterKey Alerts

Now that we have the list of all Entry locator controls, we can alert the EnterKey before the ACCEPT loop:

#AT(%BeforeAccept)
  #FOR(%EntryLocatorControl)
%EntryLocatorControl{PROP:Alrt,1} = EnterKey
  #ENDFOR
#ENDAT

This makes the assumption that there are no other alert keys for the EntryLocator controls. You could always change {PROP:Alrt,1} to {PROP:Alrt,100}, or some other number. This should be safe as-is, however, since you are unlikely to need additional alert keys on an Entry locator control.

Trapping the Control Events

There are myriad embeds that can be used to insert control-specific event processing code. The most commonly used is %ControlEventHandling. The #EMBED marker looks like this:

#EMBED(%ControlEventHandling,'Internal Control Event Handling')|
       ,%Control,%ControlEvent, HIDE

Notice that it takes two parameters: %Control and %ControlEvent. With an embed of this type, you have many options for controlling when it should apply. When the code in your #AT section is inserted, %Control and %ControlEvent will be set before you are inserted. If you want to have this code placed for a specific control, then you can specify both parameters in your #AT statement. For example:

#AT(%ControlEventHandling, %YourControl, 'YourEvent')

%YourControl must be a single-valued symbol. In this case, our variable is multi-valued, so we can't use this syntax. Also YourEvent must be a standard event that is "known" by the generator. Unfortunately, AlertKey is not "known" unless you've manually defined an alert key for this control in the IDE.

However, there are many alternate forms. For example:

#AT(%ControlEventHandling),WHERE(%ControlEvent = 'YourEvent')

This will insert code for "YourEvent" into all controls (assuming that YourEvent is applicable for each of the controls). To restrict which controls will actually get code, we can add a #FOR loop to process our multi-valued symbol:

#AT(%ControlEventHandling),WHERE(%ControlEvent = 'YourEvent')
  #FOR(%EntryLocatorControl),WHERE(%EntryLocatorControl = %Control)
SomeCode
  #ENDFOR
#ENDAT

The #FOR statement looks to see if the current %Control is included in our list of %EntryLocatorControls. If you are using C4 rather than CW2003, then you could change it to this:

#AT(%ControlEventHandling),|
    WHERE(INLIST(%Control, %EntryLocatorControl) |
    AND %ControlEvent = 'YourEvent')
SomeCode
#ENDAT

As I mentioned earlier, though, this will not work because "AlertKey" will probably not be an "known" event. Instead, we'll use another embed that isn't event specific.

#EMBED(%ControlPreEventCaseHandling,|
       'Control Handling, before event handling'),%Control

Notice that this embed has only the %Control parameter. This means it does not distinguish between events. Now your #AT section becomes:

#AT(%ControlPreEventCaseHandling)
  #FOR(%EntryLocatorControl),WHERE(%EntryLocatorControl = %Control)
IF EVENT() = EVENT:AlertKey AND KEYCODE() = EnterKey
  POST(EVENT:Accepted, %Control)
END
  #ENDFOR
#ENDAT

This code generates into all the EntryLocator controls. Notice that the check for the event is not generated into the code itself, rather, it is divined by the template.

We could leave it like this, but there is another step we can take to make it a little more efficient. The code that we're generating is basically the same for all controls. All we need to do is substitute FIELD() for %Control in the code, and we can turn this into a routine. That gives us:

#AT(%ControlPreEventCaseHandling)
  #FOR(%EntryLocatorControl),WHERE(%EntryLocatorControl = %Control)
DO MH::TakeEntryLocatorEvent
  #ENDFOR
#ENDAT

Now we need to generate the routine. The %ProcedureRoutines embed is the place to go:

#AT(%ProcedureRoutines),WHERE(ITEMS(%EntryLocatorControl))
MH::TakeEntryLocatorEvent ROUTINE
IF EVENT() = EVENT:AlertKey AND KEYCODE() = EnterKey
  POST(EVENT:Accepted, FIELD())
END
#ENDAT

Notice that we insert this code only if there is at least one Entry locator in the procedure. We could actually make this more efficient by using a procedure, but I'm not going to get into that right now. I think you understand the basic techniques.

ABC OOP Solution

One of the main promises of ABC is that if you don't like it, you can tweak it to do "almost anything". Well this seemed like the perfect test. I didn't think the EntryLocatorClass was perfect, so I wanted to change it.

This is always the basic intention when using OOP. You try to create generic tools. When the tool doesn't fit your needs, though, you try to reuse as much as possible, tweaking only when necessary. The EntryLocatorClass is almost exactly what I want, with the exception of the EnterKey processing.

That means my class will inherit the EntryLocatorClass. It may override one or more methods, and it may have some methods of its own. Before we start, though, we have to understand how the BrowseClass interacts with the EntryLocatorClass.

Where Do We Fit?

Normally the BrowseClass is in control. When you hit an alphanumeric key, it throws this key into the locator's TakeKey method. The StepLocatorClass looks at this key and uses it to perform an immediate search in the browse. The IncrementalLocatorClass is a little different, in that it will stack up the keystrokes together to give you a more detailed search. The EntryLocatorClass is a little different again, in that it throws the key back into the keyboard buffer and changes focus to the entry field.

When I initially started researching it, I assumed that the BrowseClass would pass all events through to my LocatorClass. I was wrong! It turns out that the only event I get from the BrowseClass is the Accepted event. (These types of roadblocks will often temporarily stump you when using ABC.) Since I needed to get the AlertKey events as well, I had to find another way.

One solution was to use a template to generate a call to my own TakeEvent method in all Browse procedures. This was unacceptable, though, because I wanted to prevent the need for a template. And if I had to generate code into every procedure, I may as well not bother overriding the ABC classes.

Yet I still had to get that event somehow. It had to get to my class without requiring that any additional code be generated into the procedures. Everything had to be done in the class methods!

Around this time I recalled a command called "RegisterEvent" that I spotted in Clarion's Resize classes. (The official command, as documented in Clarion 5 beta, will be "REGISTER".) The current syntax is:

REGISTER( event, handler, object [, window] [, control] )

This powerhouse enables you to trap any event produced by any window, and have Clarion automatically call your event handler. You don't need explicit code within the ACCEPT loop, which solves our problem. As long as the event handler method gets registered, we're set.

The parameters are as follows: "Event" is the Clarion event number (e.g.: EVENT:AlertKey). "Handler" is the address of our method (e.g.: ADDRESS(SELF.TakeAlertKey)). "Object" is the address of our object (e.g.: ADDRESS(SELF)). "Window" is the window for which events will be trapped. If it is omitted, then the last opened window is assumed. Finally, "Control" is the control for which events will be trapped. If it is omitted, then only non-field-specific events will be handled.

We will execute this command in our Init method, and it will look like this:

REGISTER(EVENT:AlertKey, ADDRESS(SELF.TakeAlertKey), ADDRESS(SELF),, SELF.Control)

Declaring the Class

Our class header is as follows:

!ABCIncludeFile
OMIT('_EndOfInclude_', _MhAbEntryLocatorPresent_) ! Omit this if already compiled
_MhAbEntryLocatorPresent_ EQUATE(1)
!==============================================================================
INCLUDE('ABBROWSE.INC')
INCLUDE('ABWINDOW.INC')
!==============================================================================
MH::EntryLocatorClass CLASS(EntryLocatorClass), |
                      TYPE,|
                      MODULE('MHABELOC.CLW'), |
                      LINK('MHABELOC.CLW',_ABCLinkMode_),|
                      DLL(_ABCDllMode_)
Init                    PROCEDURE(SIGNED Control=0, |
                                  *? Free, |
                                  BYTE NoCase=0, |
                                  <BrowseClass>)
TakeAlertKey            PROCEDURE,BYTE,PRIVATE
                      END!CLASS
!==============================================================================
_EndOfInclude_

This is going to become part of the ABC libraries, so you must put your source files (INC and CLW) into the LIBSRC directory beneath CLARION4. The !ABCIncludeFile marker at the start of the file indicates that it's part of the ABC library. The OMIT statement ensure that it has not already been included during the current compile. Because we require elements from BrowseClass and WindowClass, we've included their headers here.

Notice that our class inherits from EntryLocatorClass. This means that we gain everything that's included there, plus the BrowseClass will recognize us as a valid Locator class.

The Init method has the same definition as the regular Init method in the Locator classes. This means that when it's called from the procedure, ours gets control first.

The TakeAlertKey method will be the event handler passed to the REGISTER command. Notice that it's PRIVATE. Clarion's event processing system will never refer to it by name, but rather by address. It is never called in any other situation, so it makes sense to make it private.

Defining the Class Methods

The source module looks like this:

  MEMBER
  INCLUDE('MHAbELoc.INC')
  INCLUDE('EQUATES.CLW')
  INCLUDE('KEYCODES.CLW'
  MAP.
MH::EntryLocatorClass.Init |
  PROCEDURE(SIGNED Control,*? Free,BYTE NoCase,<BrowseClass VM>)
  END
  CODE
  PARENT.Init(Control, Free, NoCase, VM)
  Control{PROP:Alrt,1} = EnterKey
  REGISTER(EVENT:AlertKey, |
           ADDRESS(SELF.TakeAlertKey), |
           ADDRESS(SELF),, Control)

MH::EntryLocatorClass.TakeAlertKey PROCEDURE
  CODE
  IF KEYCODE() = EnterKey
    POST(EVENT:Accepted, SELF.Control)
  END!IF
  RETURN Level:Benign

The Init method first calls its parent's Init method, then it alerts the EnterKey and registers the event handler. The TakeAlertKey event handler simply posts an Accepted event for the entry control when the EnterKey is pressed.

Using the New Class in Your APPs

The final step to get this to work is to change the Classes setting in the global options. Under the Browse settings, you can change the default Entry Locator to "MH::EntryLocatorClass". If you don’t want to do this globally, you can change it in each procedure that needs it. From my perspective, though, it makes the most sense to change it globally.

Conclusion

Well, there you have it. We started out with the simple task of adding EnterKey processing to the Entry locators, and we've learned stuff about locators, templates, classes, and the new REGISTER command. How's that for value?

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links