![]() |
|
Published 1998-04-01 Printer-friendly version
| 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.
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.
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.
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.
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:
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.
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.
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.
|
Posted on Wednesday, August 13, 2008 by Carl Barnes One way to deal with a SELECT(?TabX) not generating an EVENT:TabChanging is to put the Tab changing code into a Procedure or Routine. In the normal EVENT:TabChanging execute the procedure/routine. In the situation where you want to force a SELECT(?TabX) first execute the procedure/routine so that code runs, then select the new tab.
|
To add a comment to this article you must log in.
Copyright © 1999-2009 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $169
(includes all back issues since '99)
Renewals from $119
Two years: $269
Renewals from $219