"Tab Popup" Template

by Mike Hanson

Published 1998-04-01    Printer-friendly version

Download the code here

Publisher's Note
At several points in this article, the template code is much too long to fit in a normal width document. We've used continuation characters ( the rule character | ) to show where the code is continued. This code will not work as shown in this article. You should use the enclosed templates instead. We hope this message helps eliminate the confusion that could be caused.

I was just reading a review of the recent release of PackRat®, and the contributor commented that the package made excessive use of tabs throughout. Even with the default setup, you had to scroll the tabs to see them all. He suggested that the developers of PackRat could provide a popup menu of available tabs via a right-click of the mouse.

This struck a cord with me. I often have a desire to use numerous tabs, as they are a fantastic organization tool, and they provide crucial guidance to the user. However, I usually implement as few as possible: Making them all visible takes up too much vertical space on the window, while using the HSCROLL or JOIN attributes hides the tabs, so that they seem inaccessible to the user.

The idea of using a popup made perfect sense to me. It was also an opportune situation to apply an OOP+TPL solution. Prior to Clarion 4, I might have attacked this with a brute force template that generated significant code into each procedure. Now I fully understand the benefits of writing the code once in an OOP class, then having the template write only enough code to implement the object.

How does it work?

Basically, we have to trap the mouse right-click (MouseRight) when the cursor is over the tab area. It uses a popup to display all the tabs, with the currently selected tab accompanied by a checkmark. The tab is changed by assigning a new value to ?Sheet{PROP:Selected}, then posting EVENT:NewSelection to the ?Sheet control.

There is one minor problem with this method. When a tab is normally changed by a user, EVENT:TabChanging is fired first, followed by EVENT:NewSelection. For the TabChanging event, The sheet's PROP:Selected and PROP:ChoiceFEq will contain their old values, and for the NewSelection event they have the new values.

Unfortunately, there is no way that I am aware of to make the program change the tab such that both of these events are automatically generated. Some have suggested SELECT(?NewTab), but that does the equivalent of ?Sheet{PROP:ChoiceFEq} = ?NewTab, and doesn't fire any events. I've tried many other methods without success. If you know of a solution for this, please let me know.

Because most developers don't make use of EVENT:TabChanging, we will simplify our solution by not generating this event when using the Popup to change tabs. You should be aware of this, however, if you do intend utilize this obscure event.

What is the Template's Role?

To keep the interface style consistent, it is likely that we would want it to affect all SHEETs in the application. To simplify the template’s use, and to keep the usage consistent, all code will be generated by a single global extension template.

In addition to handling the popup, it would be nice to have the template add the HSCROLL or JOIN attribute to the sheets. Not everyone would want this, though, so it should be an option. (This template utilizes a global setting, but you could apply the techniques from my prior articles to add a procedure extension template for local overrides.)

Also, we should make it optional for the popup to appear for a sheet without one of the scroll attributes. You may feel that the popup is redundant if the user can see all of the options.

By the way, if one of the scroll attributes exists on the sheet, then I do not know of a way to determine if the tabs actually fit on the sheet (making the scroll buttons redundant). All I do is check for one of the scroll attributes, and if one exists, make the assumption that the tabs won't fit. There really isn't any harm to providing the popup for all tabs (even if they are all visible), so I'm not too concerned about this.

The extension header looks like this:

#EXTENSION(mhTabPopup,'TabPopups for all SHEETs'),APPLICATION
#BOXED('Mike Hanson''s Public Domain Templates')
  #DISPLAY('TabPopups for all SHEETs')
#ENDBOXED
#DISPLAY
#BOXED('')
  #PROMPT('Global Scroll Defaults:',DROP('No change|Spread Scroll '&|
        'buttons|Joined Scroll buttons')),%mhTabPopupScroll
  #ENABLE(%mhTabPopupScroll <> 'No change'),CLEAR
    #PROMPT('Override existing HSCROLL and JOIN properties',CHECK),&|
             %mhTabPopupScrollOverride,AT(10,,180)
  #ENDENABLE
  #ENABLE(%mhTabPopupScroll = 'No change')
    #PROMPT('Always provide popup (regardless of scroll settings)',CHECK),&|
             %mhTabPopupAllSheets,DEFAULT(%True),AT(10,,180)
  #ENDENABLE
#ENDBOXED

If we are using a class, then it must be available within the procedure's module. This concept of "module maps" is new to Clarion 4+ABC. In the legacy templates, we would have made this into a global resource. Here's the code that adds the include file to the module map:

#AT(%CustomModuleDeclarations)
  #FOR(%Control),WHERE(%ControlType = 'SHEET')
    #ADD(%ModuleIncludeList,'MhAbTabP.INC')
    #BREAK
  #ENDFOR
#ENDAT

Whenever the Generate command is given, AppGen forgets about "extras" added by the templates. The embeds for %CustomModuleDeclarations and %CustomGlobalDeclarations are always processed, even if none of the procedures have changed and no code is generated. This enables you to ensure that all necessary resources are #PROJECTed.

Next we must create a token where we will remember whether the current procedure contains one or more SHEET controls, like so:

#ATSTART
  #DECLARE(%mhNeedsTagPopup)
#ENDAT

The #ATSTART section is only processed once. However, each procedure will process the %GatherSymbols embed. This is where we will look for the SHEET controls on each procedure's window:

#AT(%GatherSymbols)
  #SET(%mhNeedsTagPopup, %False)
  #FOR(%Control),WHERE(%ControlType = 'SHEET')
    #SET(%mhNeedsTagPopup, %True)
    #BREAK
  #ENDFOR
#ENDAT

All we really do here is look for at least one SHEET, then we get out. As you will see when we get to the OOP class, we will use one object per procedure (sort of like having one WindowManager per procedure.) This enables us to generate cleaner code. Here's the object instantiation:

#AT(%DataSection),WHERE(%mhNeedsTagPopup)
mhTabPopup MH::TabPopup
#ENDAT

Notice that the local object is called "mhTabPopup", while the class is called "MH::TabPopup". Also look at the WHERE attribute on the #AT statement. This piece of code will only be inserted if a SHEET control was found during the %GatherSymbols processing. Now that we have an object, we need to initialize it.

#AT(%WindowManagerMethodCodeSection,'Init','(),BYTE'),|
    WHERE(%mhNeedsTagPopup),PRIORITY(5600)
mhTabPopup.Init
  #FOR(%Control),WHERE(%ControlType = 'SHEET')
    #IF(EXTRACT(%ControlStatement, 'LEFT', 1))
mhTabPopup.AddSheet(%Control, |
      %(EXTRACT(%ControlStatement,'LEFT',1)))
    #ELSIF(EXTRACT(%ControlStatement, 'RIGHT', 1))
mhTabPopup.AddSheet(%Control, |
      %(EXTRACT(%ControlStatement,'RIGHT',1)))
    #ELSE
mhTabPopup.AddSheet(%Control)
    #ENDIF #ENDFOR
  #!-----
  #PRIORITY(8050)
  #CASE(%mhTabPopupScroll)
  #OF('No change')
    #SET(%ValueConstruct, 'mhTabPopup:Scroll:None')
  #OF('Spread Scroll buttons')
    #SET(%ValueConstruct, 'mhTabPopup:Scroll:HScroll')
  #OF('Joined Scroll buttons')
    #SET(%ValueConstruct, 'mhTabPopup:Scroll:Join')
  #ENDCASE
mhTabPopup.PrepareWindow(%ValueConstruct,|
                         %mhTabPopupScrollOverride)
#ENDAT

There are a few neat things here. First, the WHERE attribute causes the code to be generated only if a SHEET control exists on the window. (The rest of the #AT statements will also contain this WHERE clause in some form.) The Init method is called without any parameters. (To know which parameters to use, you must be familiar with the OOP class itself, which will be discussed later in this article.) For each SHEET control, the AddSheet method is called. This tells our single object all the sheets for which it is responsible.

The next item is the #PRIORITY(8050) statement. You'll notice that the #AT statement includes a PRIORITY(5600) attribute. This means that the first section of code will be generated at 5600 (before the window is opened). The #PRIORITY(8050) statement causes our insertion point to jump to that new position for the rest of the code.

In this case it's just a call to the PrepareWindow method. It is the responsibility of this method to make any modifications necessary to the newly opened window. This includes any general settings for the window, as well as any settings for the individual sheet controls.

The %mhTabPopupScroll and %mhTabPopupScrollOverride settings are passed in here, because they affect the initial properties of the SHEET controls before the window is first displayed. The parameters are determined by your global template settings. (As I mentioned earlier, you could create a local template that would override the global settings whenever desired.)

The next thing the template must do is trap the user's right-click actions. This is done with the TakeEvent method:

#AT(%WindowManagerMethodCodeSection,'TakeEvent','(),BYTE'),|
    WHERE(%mhNeedsTagPopup),PRIORITY(1300)
ReturnValue = mhTabPopup.TakeEvent(%mhTabPopupAllSheets)
IF ReturnValue THEN RETURN ReturnValue.
#ENDAT

At PRIORITY(1300), the code will be generated above the LOOP structure of the overridden TakeEvent method. The global %mhTabPopupAllSheets setting is passed in here, because it affects the interpretation of right-click actions.

Your objects will normally have both Init and Kill methods. The Kill method is called with the follow template section:

#AT(%EndOfProcedure),WHERE(%mhNeedsTagPopup)
mhTabPopup.Kill
#ENDAT

There is one quirk that must be accommodated. Tabs are often used in conjunction with Browses to change the sort order and filtering criteria. This means that when there is an EVENT:NewSelection for a SHEET, there is often a corresponding EVENT:NewSelection for the Browse.

The Browser's TakeNewSelection method checks for the user pressing MouseRight to support it's own popup menu. Unfortunately, it sees our MouseRight and mistakenly responds to it. The following code causes the Browser's TakeNewSelection method to be overridden for proper MouseRight handling:

#AT(%BrowserMethodCodeSection),PRIORITY(4000), |
    WHERE(%pClassMethod='TakeNewSelection' AND |
    %pClassMethodPrototype='()' AND %mhNeedsTagPopup)
mhTabPopup.TakeBrowseNewSelection
#ENDAT

There is something interesting to note here. The #EMBEDs for the browser class methods take the template instance as the second parameter of the #AT statement, before the class name and prototype. We need our code to generate in all Browse instances, and the #AT statement will not allow us to omit the second parameter without also omitting all following parameters.

The way we get around this is to look at the original #EMBED statement to see the names of the "class name" and "prototype" parameters. Once we know their names, we add them to the WHERE condition. (As an alternative, we could have also used these in an #IF statement within the #AT/#ENDAT structure.)

As you can see, the template merely generates the simplest of hooks into the OOP class. This means that generation and compiles will happen faster, and the complexity stays hidden in the object library.

What is the OOP Library's Role?

Most of the brains of our solution are in the OOP object. Most OOP libraries are comprised of one or more INC and CLW files. The INC file describes the classes, queues, and equates to the rest of the world. The CLW contains the actual executable source code that does the work.

You should understand the difference between "declaring" and "defining". When you declare something, you are describing it for the outside world. When you define it, you are giving all of the internal details. Hence, the INC file includes the declaration, while the CLW contains the definition.

What's In Our INC File?

Whenever you have an OOP class, you should declare it in an include file. There are a number of benefits to placing our INC files into Clarion's LIBSRC directory. The primary benefit is that it will automatically incorporate our class as part of the base classes and export them from support DLLs to other APPs. Our INC file looks like this:

!ABCIncludeFile
OMIT('_EndOfInclude_',_mhTabPopupClassPresent_)
 ! Omit this if already compiled
_mhTabPopupClassPresent_ EQUATE(1)
!====================================================================
ITEMIZE(0),PRE(mhTabPopup:Scroll)
None        EQUATE
HScroll     EQUATE
Join        EQUATE
END!ITEMIZE
!====================================================================
MH::TabPopup:SheetList QUEUE,TYPE
Control                  LONG
                       END!QUEUE
!====================================================================
MH::TabPopup   CLASS,TYPE,MODULE('MHABTABP.CLW'),|
               LINK('MHABTABP.CLW',_ABCLinkMode_),|
               DLL(_ABCDllMode_)
IgnoreMouseRight BYTE,PRIVATE
Sheet            &MH::TabPopup:SheetList,PRIVATE
Init             PROCEDURE
Kill             PROCEDURE
AddSheet         PROCEDURE(LONG SheetControl, SHORT Width)
PrepareWindow    PROCEDURE(BYTE Scroll, BYTE ScrollOverride)
TakeEvent        FUNCTION(BYTE AllSheets),BYTE
TakeBrowseNewSelection PROCEDURE
               END!CLASS
!====================================================================
_EndOfInclude_

The !ABCIncludeFile tells Clarion that this is an ABC-compatible base class that should be included as "Internal" in all APPs where %GlobalExternal is OFF, and as "External" in all APPs where %GlobalExternal is ON.

Your INC file must fit a certain style for it to work with the !ABCIncludeFile standard. When I asked David Bayliss to define the standard, he say that the include file should look "like" Clarion's own INC files. Although this seems rather ambiguous, I've only encountered only two pitfalls myself:

  • Your method prototype lines must not be too long. If you get an EXP entry showing the method name without the mangled prototype information, then your line is too long. Start by removing spaces. If that doesn't do it, then start removing the parameter names (leaving only the type information). I know that this makes it somewhat harder to maintain, but that’s the price you must pay.
  • Your prototype cannot be broken across multiple lines (using the "|" line continuation character). Invalid EXP lines also indicate this problem. You must utilize the same techniques as above to get your prototype to fit onto a single line.

The OMIT statement skips the file if it has already been included in the current scope.

Next come some ITEMIZEd equates that will be referenced in the calling code and class itself. These are handy for providing non-ambiguous parameters to be passed to methods (and sometimes returned from functions).

A single MH::TabPopup object will deal with all SHEET controls on the window. To do this, it must remember all of those controls. It does this with a queue. However, you cannot define a queue within an object. Instead, you must use a reference to a queue. This MH::TabPopup:SheetList QUEUE,TYPE fulfills this purpose.

Now we come to the CLASS,TYPE definition itself. Don't worry about the strange attributes; they're a little strange only if you don't have a sense of what the compiler and linker are trying to achieve. I just copied them from the AB*.INC files, and modified them to match my own source names.

The IgnoreMouseRight property is used to remember whether the tab has just changed, so that it will know that all browse TakeNewSelection methods should be intercepted.

Next comes the reference to the queue of sheet controls. We will allocate a real queue in the Init method and dispose of it in the Kill method. These two methods perform this and other basic housekeeping operations.

The AddSheet method is called one or more times after Init to inform the object of all SHEET controls on the window. The PrepareWindow event sets any general window properties and specific SHEET control properties after the window is opened, but before it is displayed.

The TakeEvent method does most of the work. It watches for the MouseRight action, and acts accordingly. It's also responsible for setting the IgnoreMouseRight property when the tab is changing. This variable is used by our TakeBrowseNewSelection method, which ensures that the Browse's TakeNewSelection method doesn't see any stray MouseRight keycodes.

What's In Our CLW File?

There are three kinds of "modules" in Clarion programming. The first is the "PROGRAM" module. It represents the main module of the program. If it compiles to an EXE, then the CODE section of the program module will be executed first. There must be only one program module per EXE/DLL.

The second type is MEMBER('Program'). This module is explicitly associated with a particular program. It shares all of the program's global data, files, procedures, etc. The majority of modules generated from your application are normally of this type.

The third type is a hybrid of the other two. It begins with a MEMBER statement (without a program name). It's really a PROGRAM module, except that it doesn't have a CODE section that gets executed when you start the EXE. Also, it is generic; it isn't associated with a specific procedure. This type of module represents the majority of the ABC class libraries, and we will use this type for our OOP module.

When using this third type of module, we have to remember that it cannot access your global program resources. Anything that it knows about must be explicitly explained to it, either in the form of include files or passed parameters. The beginning of the file looks like this:

  MEMBER
  INCLUDE('MhAbTabP.INC')
  INCLUDE('KEYCODES.CLW')
  INCLUDE('EQUATES.CLW')
  INCLUDE('ABERROR.INC')
  MAP
    InTabArea(LONG C, SHORT W),BYTE
    FormatTabPopup(LONG C),STRING
  END!MAP

Note the INCLUDE statements. First comes our own INC file. This lets the compiler know about the class that is about to be defined.

We also include KEYCODES.CLW, EQUATES.CLW and ABERROR.INC. These are included in the Program module, but we must redeclare them here because we cannot access the global entities.

The MAP section declares two procedures that are used by the module to perform its work. It's up to you whether the support procedures for the class are defined as local module procedures, or as private/protected class members. It really depends on whether those support procedures will be needed elsewhere. If the operations performed by the support procedures are very specific to the calling procedures in the module, then you can probably put them in the module map, which is what we're doing here.

Now we come to the Init and Kill methods:

MH::TabPopup.Init PROCEDURE
  CODE
  SELF.Sheet &= NEW MH::TabPopup:SheetList
  SELF.IgnoreMouseRight = False

MH::TabPopup.Kill PROCEDURE
  CODE
  FREE(SELF.Sheet)
  DISPOSE(SELF.Sheet)

The Init method creates a new QUEUE to be referenced via the Sheet property. The IgnoreMouseRight private property is also initialized at this time. The Kill method frees the queue that was allocated in Init, then it disposes of it. It's always important that you clean up after yourself.

Sometimes we cannot tell the class everything it needs to know via the Init method. There are at least two additional techniques that can be used: We could access the class properties directly, or we could use an extra method. Unless it is a very simple and obvious assignment, I don't like to access class properties from the outside world. I feel that it's safer to use an additional method. In this case, I've created AddSheet:

MH::TabPopup.AddSheet PROCEDURE(LONG SheetControl,|
                                <SHORT Width>)
  CODE
  SELF.Sheet.Control = SheetControl
  SELF.Sheet.TabWidth = Width
  ADD(SELF.Sheet)
  ASSERT(~ERRORCODE())

The AddSheet method is called one or more times after Init to let the object know the sheet controls for which it is responsible. The Width parameter is passed by the template, because there is currently no support for ?Sheet{PROP:TabWidth}.

Now the object knows what it's supposed to do. The next thing we must achieve is any initialization after the window is opened. This is accomplished with the PrepareWindow method:

MH::TabPopup.PrepareWindow PROCEDURE(BYTE Scroll,|
                                     BYTE ScrollOverride)
S    LONG,AUTO
C    LONG,AUTO
  CODE
  0{PROP:Alrt, 251} = MouseRight
  IF Scroll <> mhTabPopup:Scroll:None
    LOOP S = 1 TO RECORDS(SELF.Sheet)
      GET(SELF.Sheet, S)
      ASSERT(~ERRORCODE())
      C = SELF.Sheet.Control
      IF C{PROP:Wizard} THEN CYCLE.
      IF ScrollOverride OR (~C{PROP:HScroll}|
                        AND ~C{PROP:Join})
        CASE Scroll
        OF mhTabPopup:Scroll:HScroll
          C{PROP:HScroll} = True
        OF mhTabPopup:Scroll:Join
          C{PROP:Join} = True
        END!CASE
      END!IF
    END!LOOP
  END!IF

This method assigns the MouseRight alert key to the window. If directed to do so by its parameters (passed by the generated template code), it will also assign scroll attributes to the SHEET controls. Notice that it ignores sheets with the wizard attribute.

Now that everything is properly initialized, we must concern ourselves with trapping the user's activities. This is done with the TakeEvent method:

MH::TabPopup.TakeEvent FUNCTION(BYTE AllSheets)
S   LONG,AUTO
C   LONG,AUTO
T   BYTE,AUTO
  CODE
  IF EVENT() <> EVENT:NewSelection
    SELF.IgnoreMouseRight = False
  END
  CASE EVENT()
  OF EVENT:PreAlertKey
    IF KEYCODE() = MouseRight
      RETURN Level:Notify
    END!IF
  OF EVENT:AlertKey
    IF KEYCODE() = MouseRight
      LOOP S = 1 TO RECORDS(SELF.Sheet)
        GET(SELF.Sheet, S)
        ASSERT(~ERRORCODE())
        C = SELF.Sheet.Control
        IF C{PROP:Wizard} THEN CYCLE.
        IF InTabArea(C, SELF.Sheet.TabWidth) |
           AND (AllSheets OR C{PROP:HScroll} |
           OR C{PROP:Join})
          T = POPUP(FormatTabPopup(C))
          IF T <> 0 AND T <> C{PROP:Selected}
            C{PROP:Selected} = T
            POST(EVENT:NewSelection, C)
            SELF.IgnoreMouseRight = True
          END!IF
          RETURN Level:Benign
        END!IF
      END!LOOP
    END!IF
  END!CASE
  RETURN Level:Benign

The first item of interest is the "IF EVENT() <> EVENT:NewSelection". This is part of the interception of MouseRight from the browse's TakeNewSelection method. The NewSelection events for the browses always come immediately after the NewSelection for the sheet. Therefore, as soon as we see a non-NewSelection event, we can turn off the ignore mode.

The next thing you should notice is the handling of EVENT:PreAlert. This is one of the more confusing aspects of Clarion. Normally an Alert key will contravene control from the normal program flow. Because we are alerting a key for the entire window, it is important that we don't screw things up. Except for very special situations, it is beneficial to tell Clarion to go about its normal business.

We do this by issuing a CYCLE statement within the ACCEPT loop. The ACCEPT loop is actually buried within the WindowManager object, but it monitors the return value from the TakeEvent method, which should return Level:Benign (continue normally), Level:Notify (CYCLE), or Level:Fatal (BREAK).

Next we check for the MouseRight Alert key. If it has been pressed, then we check all of the SHEETs to see whether any of them apply. The mouse click must have occurred InTabArea(), plus the SHEET must have a scroll attribute or the override must be specified.

Notice again that we ignore Wizard sheets. The main reason for this is that there is really no well-defined region that represents a "handle" to the sheet. If you make extensive use of wizard sheets, you may want to place a box on the top of the wizard sheet, then change the class to check for mouse clicks in that area.

If the MouseRight occurred over a sheet's tabs, then we use FormatTabPopup() to create the string for the POPUP function, and respond accordingly. When changing tabs we assign the PROP:Selected for the sheet, and post the EVENT:NewSelection to the sheet. We also remember that we must IgnoreMouseRight during the upcoming EVENT:NewSelection for the browses.

The InTabArea() function looks like this:

InTabArea FUNCTION(LONG C, SHORT W)
X1   LONG,AUTO
X2   LONG,AUTO
Y1   LONG,AUTO
Y2   LONG,AUTO
  CODE
  IF ~C{PROP:Visible}
    RETURN False
  ELSE
    IF C{PROP:NoSheet}
      X1 = C{PROP:XPos}
      X2 = X1 + C{PROP:Width} - 1
      Y1 = C{PROP:YPos}
      Y2 = Y1 + C{PROP:Height} - 1
    ELSIF C{PROP:Below}
      X1 = C{PROP:XPos}
      X2 = X1 + C{PROP:Width} - 1
      Y2 = C{PROP:YPos} + C{PROP:Height} - 1
      Y1 = Y2 - C{PROP:TabRows}*12 + 1
    ELSIF C{PROP:Left}
      X1 = C{PROP:XPos}
      X2 = X1 + CHOOSE(~W, 20, W) - 1
      Y1 = C{PROP:YPos}
      Y2 = Y1 + C{PROP:Height} - 1
    ELSIF C{PROP:Right}
      X2 = C{PROP:XPos} + C{PROP:Width} - 1
      X1 = X2 - CHOOSE(~W, 20, W) + 1
      Y1 = C{PROP:YPos}
      Y2 = Y1 + C{PROP:Height} - 1
    ELSE
      X1 = C{PROP:XPos}
      X2 = X1 + C{PROP:Width} - 1
      Y1 = C{PROP:YPos}
      Y2 = Y1 + C{PROP:TabRows}*12 - 1
    END!IF
    IF INRANGE(MOUSEX(), X1, X2) AND |
       INRANGE(MOUSEY(), Y1, Y2)
      RETURN True
    ELSE
      RETURN False
    END!IF
  END!IF

If the SHEET is visible, the function sets X1, X2, Y1, and Y2 to define the boundaries of the area occupied by the tabs. This area will depend on various sheet properties. If you are using PROP:Left or PROP:Right it also uses the W (TabWidth) parameter. If the width is no available, then we just make a rough guess of 20.

The FormatTabPopup() function scans through the window looking for tab controls assigned to the sheet control, and it formats a string of choices to be passed to the POPUP function:

FormatTabPopup   FUNCTION(LONG C)
P   CSTRING(1000),AUTO
T   LONG,AUTO
  CODE
  P = ''
  LOOP T = FIRSTFIELD() TO LASTFIELD()
    IF T{PROP:Type} = CREATE:Tab AND T{PROP:Parent} = C
      P = P & CHOOSE(~P, '', '|') & |
          CHOOSE(C{PROP:ChoiceFEq}=T, '+', '') & |
          T{PROP:Text}
    END!IF
  END!LOOP
  RETURN P

CHOOSE(~P, '', '|') prefaces the new choice with a vertical bar (pipe symbol) if this is not the first. (The vertical bar is used by the POPUP function to separate choices.) CHOOSE(C{PROP:ChoiceFEq=T, '+', '') adds a checkmark to the choice if the corresponding tab is currently active. This is essentially a "You are here!" marker.

The final method is TakeBrowseNewSelection. It is called before the regular TakeNewSelection method for all browses on windows with SHEET controls:

MH::TabPopup.TakeBrowseNewSelection PROCEDURE
  CODE
  IF SELF.IgnoreMouseRight
    IF KEYCODE() = MouseRight
      SETKEYCODE(0)
    ELSE
      SELF.IgnoreMouseRight = False
    END!IF
  END!IF

If we are currently ignoring MouseRight, and KEYCODE() still returns MouseRight, then we simply SETKEYCODE(0). You may be wondering why we don't just do this once after the SHEET grabbed the MouseRight in the first place. Well, SETKEYCODE() affects the return value of KEYCODE() only during the processing of the current event. The KEYCODE() value is reverted to MouseRight for the next event. Therefore, we must trap it each time.

If the keycode is not MouseRight, then we know that something else is going on, so we can safely stop ignoring the stray MouseRight.

Conclusion

Well, that's all there is to it. You have a good example of an ABC-compatible object library that is utilized by your procedures using a minimum of generated code. The API is lucid and efficient, and the complexity is hidden in the object code.

We've also managed to keep the majority of complexity contained within the TakeEvent(), InTabArea(), and FormatTabPopup() functions. If your requirements for this template change in the future, you should be able to easily return to make modifications without scratching your head to wonder what you were doing.

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