Better Ellipsis Buttons

by Tom Moseley

Published 1997-10-01    Printer-friendly version

You know, I'm a lazy programmer. Yep. I've said it before and I'll say it again. Lazy, that's me.

You know one thing that really irritates me? Well, I mean programming-wise. I really dislike going through an application generated by the application wizard and touching it up. You've done the same, I'm sure.

One bit that really gets to me is putting that dang ellipsis button everywhere there's a lookup. It's the same old steps, over and over.

  • Find an entry control with a lookup
  • Reduce the width of the entry control by 12 dialog units
  • Populate the FieldLookupButton control template
  • Make the populated control the right size
  • Position the control
  • Go to the ellipsis button's Actions tab and select the entry control in the little drop prompt
  • Yada yada yada, repeat until bored out of your mind

This is the romantic part of programming, I know, but still... It could be made a little less boring without losing that romantic charm, don'cha think? I mean really, I have better things to do with my time, like... like... well maybe I don't have better things to do, but it's just so darn tedious.

So, I was teaching a template training class in Utah and, during a lull in suggestions, brought this up and, lo and behold, there were others like me, so we set out to write a template. This article was written to show you how we did that.

Step 1 - The Design Specification

Believe it or not, we actually had one. It took a little bit of time for us all to agree to the specification but it came about nevertheless.

  • The template had to be a global extension template, if possible. Like I said, I'm lazy and I don't want to have to populate a procedure extension template everywhere. Besides, if it's a global template the interface is consistent.
  • The template had to be plug and play. We didn't want the template user (us) to have to go to every window and make changes. You populate the template, and your work is done.
  • Because of the previous requirement, the template had to shrink the control with the lookup information. The only alternative was to make the windows all larger, which would have resulted in some completely arbitrary and quite funky window displays. Think about it. The windows would then all get 12 dialog units larger, and maybe the tabs would get bigger, but where would we put all the buttons?
  • After shrinking the entry control, the button would have to be populated. The button would be made square, with each side equal to the height of the entry control. The spacing between the entry control and the button would be 2 dialog units.

I'd like to make a point about a template design specification. In my first template training class, we started our first template with an idea for a template and then spent a little while discussing what the template would do. The next template, we did the same thing, and at the end of the fourth day, we were spending a few hours designing templates, and about double the time writing the templates themselves. During the post-mortem, one student said that he thought that we spent too much time in design, and that I should have just come with templates ready to write.

The point is that template design is more important than application design. With application design, you make a mistake in one place and it affects that one place. A poorly designed template can affect all of the generated code, in ways you can't even imagine. If you don't have a design specification, how do you know what your template's supposed to do? How can you plan for contingencies?

OK, off of the soap-box.

Step 2 - Available technology

Now that we had the specification in place, we had to look at the available technology. This was done by building small test templates and applications to see what was possible, and by examining the templates for the embed points to write the code.

The first thing we had to determine was if it was possible for a global extension template to look into the lookup field prompts of an entry control. A quick look at FIELD.TPW showed us that the prompts were held in the group %FieldTemplateStandardEntryPrompt, in the form of a field template. We created a small extension template...

#TEMPLATE(Ellipsis,'Ellipsis Buttons for Everyone!')
#EXTENSION(AutoEllipsisButtons,'Automatic Ellipsis Buttons'),APPLICATION
#AT(%GatherSymbols)
  #FOR(%Control),WHERE(%ControlType = 'ENTRY')
    #IF(%PreLookupProcedure OR %PostLookupProcedure)
!Control Found: %Control
    #ENDIF
  #ENDFOR
#ENDAT

...and populated that into an application with a few lookup controls. The %GatherSymbols embed point is the first embed point in generated code, so we could quickly look at the generated code to see if the controls were found.

The following two lines of code are the real magic here. What we wanted the template to do is look at every control on the window, where the control type was entry. The two prompt symbols that specify a lookup are %PreLookupProcedure and %PostLookupProcedure, and we only wanted to generate code if either of those two prompts had a value. That's why the following two lines of code are at the top of each of the template snippets.

#FOR(%Control),WHERE(%ControlType = 'ENTRY')
    #IF(%PreLookupProcedure OR %PostLookupProcedure)

After generating code, we looked at the source and found a block of...

!Control Found: ?CHI:Parent1
!Control Found: ?CHI:Parent2
!Control Found: ?CHI:Parent3
!Control Found: ?CHI:Parent4
!Control Found: ?CHI:Parent5
!Control Found: ?CHI:Parent6
!Control Found: ?CHI:Parent7
!Control Found: ?CHI:Parent8
!Control Found: ?CHI:Parent9

...which corresponded exactly to the list of control with lookups, so that part worked.

We know that creating controls is possible. To get this to work we needed to do two things. First, we needed to declare a variable or equate to use as a reference for the control. Second, after the window was opened yet before ACCEPT was encountered, we needed to create the ellipsis control(s), size them, and shrink the entry control.

Then we had to find out where to put our lookup EMBED code. We couldn't use any of the regular control event handling embed points, because those are all dependent on %Control, which are the populated controls in the window formatter. The ellipsis buttons we needed to generate code for weren't in the window formatter. We needed to create our own CASE ACCEPTED() structure, processing the accepted events for our buttons. We chose to write this structure before the built-in window and control event handling, simply to make looking at and admiring the code easier.

An examination of the code generated in FIELD.TPW for an accepted lookup showed that the code wasn't modular enough to be used. We determined that we'd need to copy the file handling code in the FieldLookupButton control template, as this code was almost entirely suitable and could be cut and pasted into our template, with one small change.

So, the available technology supported the design. It was time for the next step...

Step 3 - Take a break

Sit back, put a relaxing CD in the player, and listen to a song for a few minutes. It's time for a little breather.

Step 4 - Baby steps: Declaring ellipsis button handling equates

Now that we were writing template code, it was time to start taking baby steps. Now, come on. Get up off the floor and stop drooling. By baby steps I mean that when you're writing templates, you do a little bit at a time to ensure that you don't get too far off course. I remember one time I'd forgotten this lesson, spent a full day rewriting my code based on this revolutionary new file structuring premise, only to find our the compiler didn't agree that the idea was too cool. Take baby steps and avoid some wasted time.

Instead of declaring integers to control our controls, we decided to use equates. The reason is simple, really. We didn't need to do anything except single controls, and equates don't consume memory. In looking at generated and template code, we determined that we wanted to use the embed point right after the window is generated, which is called %DataSectionAfterWindow. We wrote the following template code at that embed point:

#AT(%DataSectionAfterWindow)
  #FOR(%Control),WHERE(%ControlType = 'ENTRY')
    #IF(%PreLookupProcedure OR %PostLookupProcedure)
EButton:%ControlUse EQUATE(13475 + %Control)
    #ENDIF
  #ENDFOR
#ENDAT

Note that Ebutton was our own little "prefix" showing what template created the equate. You might be wondering why we used 13475 for our equate offset. There are two reasons.

  1. The number had to be large enough to avoid stepping on existing controls.
  2. Other templates use the same technique. Fortunately, arbitrary numbers usually end in zeros, and 13475 certainly seemed like a safe separation.

Anyway, the generated code for this looked like:

EButton:CHI:Parent1 EQUATE(13475 + ?CHI:Parent1)
EButton:CHI:Parent2 EQUATE(13475 + ?CHI:Parent2)
EButton:CHI:Parent3 EQUATE(13475 + ?CHI:Parent3)
EButton:CHI:Parent4 EQUATE(13475 + ?CHI:Parent4)
EButton:CHI:Parent5 EQUATE(13475 + ?CHI:Parent5)
EButton:CHI:Parent6 EQUATE(13475 + ?CHI:Parent6)
EButton:CHI:Parent7 EQUATE(13475 + ?CHI:Parent7)
EButton:CHI:Parent8 EQUATE(13475 + ?CHI:Parent8)
EButton:CHI:Parent9 EQUATE(13475 + ?CHI:Parent9)

One question that came up was, why were we using ?CHI:Parent1, ?CHI:Parent2, etc. When the window compiles, every control is assigned a number, used to access the control in code. That number is called a field equate. The first control on the window is assigned a field equate of 1, the next 2, etc. These values are accessible through using a field equate symbol. ?CHI:Parent1 is the field equate of the control that fills the CHI:Parent1 field. While CHI:ParentID might contain anything at all, ?CHI:ParentID is assigned a number that tells the field number on the window.

Anyway, our first small step showed that the code that was generated was OK. Each EButton equate, one for each entry control with a lookup, was declared and all had unique values.

Step 5 - Baby steps: Creating ellipsis buttons and sizing entry controls

Now that we had the handles to create the controls, the next baby step was to create the controls themselves. A quick look at WINDOW.TPW, the template containing the window handling code, showed that the embed point %BeforeAccept is directly before the accept statement. We used that one.

So what did the code have to do? It needed to...

  • Create a button control
  • Set the button control's text to "..."
  • Position and size the button control
  • Put the button control in the same structure (TAB or GROUP) as the entry control.
  • Unhide and enable the button control
  • Shrink the entry control

And the code that does this looks like this:

#AT(%BeforeAccept)
  #FOR(%Control),WHERE(%ControlType = 'ENTRY')
    #IF(%PreLookupProcedure OR %PostLookupProcedure)
CREATE(EButton:%ControlUse,CREATE:Button,%Control{Prop:Parent})
EButton:%ControlUse{Prop:Text} = '...'
EButton:%ControlUse{Prop:XPos} = (%Control{Prop:XPos} |
                               + %Control{Prop:Width} + 2) |
                               - %Control{Prop:Height}
EButton:%ControlUse{Prop:YPos} = %Control{Prop:YPos}
EButton:%ControlUse{Prop:Height} = %Control{Prop:Height}
EButton:%ControlUse{Prop:Width} = %Control{Prop:Height}
UNHIDE(EButton:%ControlUse)
ENABLE(EButton:%ControlUse)
%Control{Prop:Width} = %Control{Prop:Width} - (%Control{Prop:Height} + 2)
    #ENDIF
  #ENDFOR
#ENDAT

Understand that this is the code after several iterations trying to get the correct effect on the window. You know how I'm stressing baby steps? We took the time to get this code correct first, before going on to bigger and better things.

As you read the code above, it should be fairly straightforward. The only bit that got me was getting the buttons to be on the correct tab. That's where the third attribute to CREATE comes into play, and Prop:Parent.  The third attribute of CREATE is used to specify a parent control, and Prop: Parent contains a reference to a Parent control.  By using CREATE(EButton:%ControlUse,CREATE:Button,%Control{Prop:Parent}), we're setting the ellipsis button's parent to be the same as the entry control's parent.

So, the code that this bit generates...

CREATE(EButton:CHI:Parent1,CREATE:Button,?CHI:Parent1{Prop:Parent})
EButton:CHI:Parent1{Prop:Text} = '...'
EButton:CHI:Parent1{Prop:XPos} = (?CHI:Parent1{Prop:XPos} |
                               + ?CHI:Parent1{Prop:Width} + 2) |
                               - ?CHI:Parent1{Prop:Height}
EButton:CHI:Parent1{Prop:YPos} = ?CHI:Parent1{Prop:YPos}
EButton:CHI:Parent1{Prop:Height} = ?CHI:Parent1{Prop:Height}
EButton:CHI:Parent1{Prop:Width} = ?CHI:Parent1{Prop:Height}
UNHIDE(EButton:CHI:Parent1)
ENABLE(EButton:CHI:Parent1)
?CHI:Parent1{Prop:Width} = ?CHI:Parent1{Prop:Width} |
                         - (?CHI:Parent1{Prop:Height} + 2)

...creates a window like this...

Look Ma! Buttons!
Look Ma! Buttons!

...which is just what we wanted. There were four more entry controls on the second tab, so it looked as if all was well. Onward and upward.

Step 6 - Baby Steps: Getting into position for lookup code

So we had our controls created. Not it was time to find the right place to put the lookup code. The embed point %AcceptLoopBeforeEventHandling is the first embed point inside the accept loop, and this is where we decided to put our code.

Our first pass at the code looked like this...

#AT(%AcceptLoopBeforeEventHandling)
CASE ACCEPTED()
  #FOR(%Control),WHERE(%ControlType = 'ENTRY')
    #IF(%PreLookupProcedure OR %PostLookupProcedure)
OF EButton:%ControlUse
  !Lookup Code goes here
    #ENDIF
  #ENDFOR
END
#ENDAT

...which generated code that would compile, but it was ugly code. How was it ugly? Well, in every window procedure, whether or not there were lookup controls, we got the bit of code...

CASE ACCEPTED()
END

...which just isn't acceptable. I mean, it is -- in that the compiler probably just negates the whole thing -- but it's ugly. What we wanted to do was only generate the CASE statement when there was code in it. #SUSPEND to the rescue!

#SUSPEND is sorta like a template LOGOUT statement. When the #SUSPEND is encountered, source code generation goes into a bucket, waiting until code generation is released, or until the template system is told to resume generation without the code in the bucket. Code generation is released in one of two ways:

  • An explicit #RELEASE command is encountered
  • Code is generated that is not preceded by a #?

Code generation is resumed without the code in the bucket if and only if an explicit #RESUME command is encountered.

So, by using #SUSPEND, the code looks like...

#AT(%AcceptLoopBeforeEventHandling)
  #SUSPEND
#?CASE ACCEPTED()
    #FOR(%Control),WHERE(%ControlType = 'ENTRY')
      #IF(%PreLookupProcedure OR %PostLookupProcedure)
OF EButton:%ControlUse
  !Lookup code goes here
      #ENDIF
    #ENDFOR
#?END
  #RESUME
#ENDAT

If an entry control on the window has a lookup procedure defined, the "OF Ebutton:..." code is generated. This code generation, not preceded by a #?, triggers the writing of the CASE ACCEPTED() and END statements. This means the code is visible only in procedures with the appropriate controls.

That said, the line "!Lookup code goes here " doesn't do much for us now, does it.

Step 7 - Baby Steps: Writing lookup code

As we discussed above, the FieldLookupButton lookup code is almost perfect for our cause. The only problems were the first and last line...

#FIX(%Control,%ControlToLookup)
.
.
.
#FIX(%Control,%LookupControl)

These lines are here because in the FieldLookupButton template, the control that is active when code generation takes place is not the entry control, but the button on the window. When our template generates code, the lookup control is the active control, so we didn't need that little bit. Everything else, though, was perfect. We took that code and put it into a template group called %WriteButtonEventHandling.

#GROUP(%WriteButtonEventHandling)
  #IF(NOT %PostLookupKey)
    #FIND(%Field,%ControlUse)
    #FOR(%Relation),WHERE(%RelationKey = %PreLookupKey)
      #IF(%FileRelationType = 'MANY:1')
        #FOR(%FileKeyField),WHERE(%FileKeyFieldLink)
          #IF(%FileKeyFieldLink = %PreLookupField)
            #BREAK
          #ENDIF
%FileKeyFieldLink = %FileKeyField
        #ENDFOR
      #ENDIF
    #ENDFOR
%PreLookupField = %ControlUse
GlobalRequest = SelectRecord
%PreLookupProcedure
LocalResponse = GlobalResponse
IF LocalResponse = RequestCompleted
    #FIND(%Field,%ControlUse)
    #FOR(%Relation),WHERE(%RelationKey = %PreLookupKey)
      #IF(%FileRelationType = 'MANY:1')
        #FOR(%FileKeyField),WHERE(%FileKeyFieldLink)
          #IF(%FileKeyFieldLink = %PreLookupField)
            #BREAK
          #ENDIF
  %FileKeyField = %FileKeyFieldLink
        #ENDFOR
      #ENDIF
    #ENDFOR
  %ControlUse = %PreLookupField
END
  #ELSE
    #FOR(%Relation),WHERE(%RelationKey = %PostLookupKey)
      #IF(%FileRelationType = 'MANY:1')
        #FOR(%FileKeyField),WHERE(%FileKeyFieldLink)
          #IF(%FileKeyFieldLink = %PostLookupField)
            #BREAK
          #ENDIF
%FileKeyFieldLink = %FileKeyField
        #ENDFOR
      #ENDIF
    #ENDFOR
%PostLookupField = %ControlUse
GlobalRequest = SelectRecord
%PostLookupProcedure
LocalResponse = GlobalResponse
IF LocalResponse = RequestCompleted
    #FOR(%Relation),WHERE(%RelationKey = %PostLookupKey)
      #IF(%FileRelationType = 'MANY:1')
        #FOR(%FileKeyField),WHERE(%FileKeyFieldLink)
          #IF(%FileKeyFieldLink = %PostLookupField)
            #BREAK
          #ENDIF
  %FileKeyField = %FileKeyFieldLink
        #ENDFOR
      #ENDIF
    #ENDFOR
  %ControlUse = %PostLookupField
END
  #ENDIF
ForceRefresh = True
LocalRequest = OriginalRequest
DO RefreshWindow

...which left us with one thing left to do when writing the template. We went back to the %AcceptLoopBeforeEventHandling EMBED code and replaced !LookupCodeGoesHere with #INSERT(%WriteButtonEventHandling), which made the code look like this:

#AT(%AcceptLoopBeforeEventHandling)
  #SUSPEND
#?CASE ACCEPTED()
    #FOR(%Control),WHERE(%ControlType = 'ENTRY')
      #IF(%PreLookupProcedure OR %PostLookupProcedure)
OF EButton:%ControlUse
  #INSERT(%WriteButtonEventHandling)
      #ENDIF
    #ENDFOR
#?END
  #RESUME
#ENDAT

...and that was the template. The requirements from Step 1 were met, and our application had ellipsis buttons next to all of the lookup controls.

Step 8 - Version 2?

Is there more to do with this template? Certainly there is. You could take it farther. Wherever there's a date field, you could have the ellipsis button call a calendar; for numeric fields, call a calculator. If you decide to expand this functionality, write it up for Clarion Online.

Post Mortem

Well, with one small template we've made the CW application much more powerful and reduced the amount of fidding necessary to get your application to work. Is the template perfect? Certainly not. The next time I do this, I will probably write the code into routines, but this is how we did it. Can more be done? Certainly, and we'll examine ways to add power to the wizard with templates such as this in the future.

Until next time, then...

Bye!

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $159

(reg $189, save $30)

(includes all back issues since '99)

Renewals from $109

Two years: $249

(reg $289, save $40)

Renewals from $199

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links