|
|
/*++
Copyright (c) 2000-2001, Microsoft Corporation All rights reserved.
Module Name:
subclass.c
Abstract:
This file contains functions that support our subclassing efforts. Here is how it works:
Each window we create must be subclassed so we can maintain the illusion of Unicodality on it. We therefore store a pointer to some memory that contains all of the information we need to properly call the wndproc, stored in a GODOTWND structure. Since the caller can actually subclass the window, we need to store a linked list of these structures.
NOTE: From comctl32.dll's SUBCLASS.C:
Win95's property code is BROKEN. If you SetProp using a text string, USER // adds and removes atoms for the property symmetrically, including when the
// window is destroyed with properties lying around (good). Unfortunately, if
// you SetProp using a global atom, USER doesn't do things quite right in the
// window cleanup case. It uses the atom without adding references in SetProp
// calls and without deleting them in RemoveProp calls (good so far). However,
// when a window with one of these properties lying around is cleaned up, USER
// will delete the atom on you. This tends to break apps that do the
// following:
//
// - MyAtom = GlobalAddAtom("foo"); // at app startup
// - SetProp(SomeWindow, MyAtom, MyData);
// - <window gets destroyed, USER deletes atom>
// - <time passes>
// - SetProp(SomeOtherWindow, MyAtom, MyData); // fails or uses random atom
// - GlobalDeleteAtom(MyAtom); // fails or deletes random atom
In order to avoid this bug in the Win9x property code, we do not store this as an atom, instead choosing to use an actual string and letting the system do all the work to manage the propname. We also wrap all of the property functions, both A and W, to fail the creation, altering, or removal of our one property. This does not solve the problem completely since they can always work outside Godot and wreak havoc, but we cannot solve THAT without replacing USER32.DLL on the machine! We still do a call to GlobalAddAtom and just do a lot of aggressive calls to refcount it this way. Slightly slower perf-wise but better guarantees!
In order to make sure that the user does not directly have their wndproc called by the OS (which is not Unicode!), we do not ever directly give them the true wndproc, but instead we give them a special pointer. Per RaymondC, addresses with 64k of the 2gb boundary (0x7FFF0000 - 0x7FFFFFFF) are always invalid proc addresses, and we need these IDs to be invalid so that someone not going through CallWindowProc will fault right away rather then calling into random memory. The first pointer for each window is always going to be 0x7FFFFFFF - 1, and each additional subclass a user adds via SetWindowLong will subtract another number (calculated via CWnd). Since the user never could have more than 64,000 subclasses, this should give us plenty of room. Any time we get a call to our CallWindowProc wrapper, we route these "GodotID" values appropriately.
Note that we also store DLGPROCs via the same means. No particular GODOTWND will ever have both a DLGPROC and a WNDPROC in it, but they will always have unique IDs for each one so we need separate GODOTWNDs here. Which one it is can be determined by the fWndproc member.
When they add a sublass (via SetWindowLong), we add the items to the linked list using a "PUSH" type operation. The most recent subclass is always on top, and the base subclass is always on the bottom.
If they unsubclass (call SetWindowLong, passing in one of our own GodotID values) in the right order as they are supposed to, we simply use a "POP" type operation on our linked list. If they unsubclass in the wrong order, we have a little extra work to do. An outstanding caller might still want to call via this (now invalid) godotID! To solve this problem, we do not delete anything in this case but simply NULL out the userLpfn for this GODOTWND. Then any time they are looking to call a godotID with no userLpfn, we simply skip down to the first valid wndproc we can find.
Sometimes, the wndproc we need to call is expecting ANSI. Perhaps they subclassed via SetWindowLongA, or perhaps it is the original wndproc of a system class (so that the actual wndproc comes from user32.dll or worse from user.exe!). Therefore we store whether the prop is expecting Unicode or ANSI values.
Functions found in this file: InitWindow CleanupWindow GetWindowLongInternal SetWindowLongInternal FauxWndProcOfHwnd GetUnicodeWindowProp DoesWndprocExpectAnsi WndprocFromFauxWndproc IsSystemClass CWnd
Revision History:
28 Feb 2001 v-michka Created.
--*/
#include "precomp.h"
//
// The wnd structure. We store a linked list of these, per window.
// In our most optimal case, this is a list with just one wnd in it.
// WARNING: This structure should be DWORD-aligned!
// WARNING: Do not ever change this structure in a way that would break old clients!!!
//
struct GODOTWND { WNDPROC userLpfn; // the lpfn to be calling
UINT godotID; // the fake pointer we hand to the user
BYTE fUnicode : 1; // Is the user lpfn expecting Unicode? All but system wndprocs should
BYTE fWndproc : 1; // wndproc or dlgproc in the userLpfn member?
BYTE bVersion : 8; // Version of the structure, will be 0 for now.
ULONG Reserved : 22; // RESERVED for future use
struct GODOTWND* next; // Pointer to the next GODOTWND to chain to
};
// We keep UNICODE and ANSI versions of this string for ease of comparison.
// Since we never support the user mucking with (or even seeing!) these
// properties, and we do not want to have to convert the string just to
// support the comparisons that we have to do.
// WARNING: You must keep these two strings in synch!!!
const char m_szMemBlock[] = "GodotMemoryBlock"; const WCHAR m_wzMemBlock[] = L"GodotMemoryBlock"; ATOM m_aMemBlock;
const WCHAR c_wzCaptureClass[] = L"ClsCapWin"; const WCHAR c_wzOleaut32Class[] = L"OLEAUT32";
// our own hack ANSI class
#define ANSIWINDOWCLASS (LPWSTR)0x00000001
// some external forward declares
int __stdcall GodotGetClassNameW(HWND hWnd, LPWSTR lpClassName, int nMaxCount);
// our forward declares
/*BOOL IsAnAnsiClass(LPCWSTR lpszClass);*/ int CWnd(struct GODOTWND* head);
/*-------------------------------
IsInternalWindowProperty
// If this is our special wnd prop for the memory
// handle, return FALSE
-------------------------------*/ BOOL IsInternalWindowProperty(LPCWSTR lpsz, BOOL fUnicode) { if(FSTRING_VALID(lpsz)) { if(fUnicode) return(CompareHelper((LPCWSTR)m_wzMemBlock, lpsz, FALSE) == 0); else return((lstrcmpiA((LPCSTR)m_szMemBlock, (LPCSTR)lpsz) == 0)); } else return(MAKEINTATOM(lpsz) == MAKEINTATOM(m_aMemBlock)); }
/*-------------------------------
InitWindow
Create the first GODOTWND for the window. Assumes that the lpfn passed in is (in fact) the subclass.
We use lpszClass to find out whether this is an ANSI or a Unicode window:
1) If the caller passes ANSIWINDOWCLASS then we are sure it is ANSI 2) If the caller is being lazy and passes NULL, then we find out the class name 3) If they pass the class name, we check it
NOTE: We do not put critical sections here because only the creating thread of a window can ever really initialize the window with us. Kind of a built-in guard. -------------------------------*/ BOOL InitWindow(HWND hwnd, LPCWSTR lpszClass) { if(GetPropA(hwnd, m_szMemBlock) != 0) { // Why get called twice? The only case where this ought
// to happen is child controls of dialogs that are created
// by the caller prior to WM_INITDIALOG. We can just skip
// out in that case.
return(FALSE); } else { BOOL fUnicode = TRUE; struct GODOTWND* newWnd;
if(!FSTRING_VALID(lpszClass)) { // no class name given; lets get it the hard way
WCHAR * wzClassName[128]; // ATOM max is 255 bytes
int cchClassName;
ZeroMemory((LPWSTR)wzClassName, 128); cchClassName = GodotGetClassNameW(hwnd, (LPWSTR)wzClassName, 128); wzClassName[cchClassName - 1] = 0;
if(lpszClass == ANSIWINDOWCLASS) { // This is our hack flag that indicates that the caller
// is assuring us that this is an ANSI wndproc.
if(gwcscmp(c_wzOleaut32Class, (LPWSTR)wzClassName) == 0) { // The un-subclass-able window
return(FALSE); }
fUnicode = FALSE; } else { if(gwcscmp(c_wzCaptureClass, (LPWSTR)wzClassName) == 0) { // Its a capture window, so lets remember this for all time
SetCaptureWindowProp(hwnd); }
fUnicode = TRUE; } }
newWnd = GodotHeapAlloc(sizeof(struct GODOTWND));
if(newWnd) { newWnd->fWndproc = TRUE; newWnd->godotID = ZEORETHGODOTWND - 1;
// Do the subclass
newWnd->userLpfn = (WNDPROC)SetWindowLongA(hwnd, GWL_WNDPROC, (LONG)&WindowProc);
// Is this a Unicode proc? Well, it is if we have
// not already decided it is an ANSI proc and if
// the actual proc is in the upper 2gb of virtual
// mem reserved for the system.
// CONSIDER: This assumption might suck if a true
// system component ever used Godot!
newWnd->fUnicode = (BYTE)(fUnicode && ((UINT)newWnd->userLpfn < LOWESTSYSMEMLOC)); newWnd->next = NULL;
// We have to aggressively refcount our prop to keep anyone from
// losing it. Thus we do an initial GlobalAddAtom on it and then
// subsequently call SetPropA on it with the same string.
if(!m_aMemBlock) m_aMemBlock = GlobalAddAtomA(m_szMemBlock); // Tag the window with the prop that
// contains our GODOTWND linked list
if(SetPropA(hwnd, m_szMemBlock, (HANDLE)newWnd)) { return(TRUE); } }
// If we make it here, then something crucial failed
if(newWnd) GodotHeapFree(newWnd); return(FALSE); } }
/*-------------------------------
CleanupWindow
We need to make sure we get rid of all the members of the linked list that we alloc'ed. The window is being destroyed. -------------------------------*/ BOOL CleanupWindow(HWND hwnd) { struct GODOTWND* current; struct GODOTWND* next; BOOL RetVal;
__try { EnterCriticalSection(&g_csWnds);
current = GetPropA(hwnd, m_szMemBlock); while (current != NULL) { next = current->next; GodotHeapFree(current); current = next; }
// If this is a common dlg, then the prop must be removed.
RemoveComdlgPropIfPresent(hwnd);
// We must do this remove; if not, then we will be causing the
// prop to be cleaned up by the system and will ruin the other
// windows out there.
RetVal = (RemovePropA(hwnd, m_szMemBlock) != NULL); } __finally { LeaveCriticalSection(&g_csWnds); } return(RetVal); }
/*-------------------------------
GetWindowLongInternal
Our own internal version of GetWindowLong, which returns a godotID for both WNDPROC and DLGPROC subclasses. If the window is not one of ours, then we just yield to the actual GetWindowLong. If, however, it is one our windows but we do not have a subclass in place (should only be possible for cases such as nIndex==DWL_DLGPROC on non-dialogs) then we return 0 and set last error to ERROR_INVALID_INDEX, just like the API would do. -------------------------------*/ LONG GetWindowLongInternal(HWND hwnd, int nIndex, BOOL fUnicode) { if(nIndex==GWL_WNDPROC || nIndex==DWL_DLGPROC) { struct GODOTWND* current; LONG RetVal = 0;
__try { EnterCriticalSection(&g_csWnds);
current = GetPropA(hwnd, m_szMemBlock);
if(current == NULL) { // No list, so we assume this is not one of our windows
RetVal = GetWindowLongA(hwnd, nIndex); } else { // The top relevant proc must always be valid, so we
// can just return it. Note that we always return the
// GodotID, for both dlgprocs and wndprocs.
while(current) { if(((nIndex==GWL_WNDPROC) && current->fWndproc) || ((nIndex==DWL_DLGPROC) && !current->fWndproc)) { RetVal = (LONG)current->godotID; break; } current = current->next; } } } __except( EXCEPTION_EXECUTE_HANDLER ) { RetVal = 0; }
LeaveCriticalSection(&g_csWnds); if(RetVal==0) SetLastError(ERROR_INVALID_INDEX);
return(RetVal); } else { // No wrapper needed for non-wndproc calls
return(GetWindowLongA(hwnd, nIndex)); } }
/*-------------------------------
SetWindowLongInternal
Our own internal version of SetWindowLong, which does the right thing with subclassing both WNDPROCs and DLGPROCs. Note that the only time we ever *change* the actual subclass that Windows knows of is when someone subclasses a window that we did not create. Then in the case of a SetWindowLongA call we just leave it to the API, or in the SetWindowLongW case we hook up the window, starting with an ANSI proc (which is what should be there now). Otherwise, our own wndproc is already in charge and the only thing that we do is shuffle who we plan to call later. We always return real WNDPROCs when removing a subclass, and godotIDs when adding one. -------------------------------*/ LONG SetWindowLongInternal(HWND hwnd, int nIndex, LONG dwNewLong, BOOL fUnicode) { if(nIndex==GWL_WNDPROC || nIndex==DWL_DLGPROC) { struct GODOTWND* head; LONG RetVal; BOOL fWndproc; // wndproc or dlgproc?
DWORD dwProcessId = 0;
GetWindowThreadProcessId(hwnd, &dwProcessId); if(GetCurrentProcessId() != dwProcessId) { // Not legal to cross the process boundary on a subclass
SetLastError(ERROR_INVALID_WINDOW_HANDLE); return(0); } // No one can muck with wndprocs until we are done here
EnterCriticalSection(&g_csWnds);
RetVal = 0; fWndproc = (nIndex==GWL_WNDPROC); head = GetPropA(hwnd, m_szMemBlock); if(head == NULL) { // No list, so assume this is not (yet?) one of our windows.
// If they called SetWindowLongA then we will yield to the API's wisdom.
if(!fUnicode) { RetVal = SetWindowLongA(hwnd, nIndex, dwNewLong); LeaveCriticalSection(&g_csWnds); return(RetVal); }
// We will now try hook up this ANSI window
if(!InitWindow(hwnd, ANSIWINDOWCLASS)) { // Window init failed, so we need to fail the subclass.
SetLastError(ERROR_OUTOFMEMORY); LeaveCriticalSection(&g_csWnds); return(RetVal); } else { // This is a new subclass proc! We will create a Unicode
// one atop the ANSI one that was just created.
struct GODOTWND* current = GodotHeapAlloc(sizeof(struct GODOTWND));
if(current == NULL) { // Cannot allocate, so bail.
CleanupWindow(hwnd); SetLastError(ERROR_OUTOFMEMORY); LeaveCriticalSection(&g_csWnds); return(RetVal); } else { head = GetPropA(hwnd, m_szMemBlock);
// Lets fill up the new structure
current->userLpfn = (WNDPROC)dwNewLong; current->fWndproc = (BYTE)fWndproc; current->godotID = ZEORETHGODOTWND - (CWnd(head) + 1); current->fUnicode = (BYTE)fUnicode; current->next = head; SetPropA(hwnd, m_szMemBlock, (HANDLE)current); RetVal = current->next->godotID; LeaveCriticalSection(&g_csWnds); return(RetVal); } } } else { if(INSIDE_GODOT_RANGE(dwNewLong)) { // This is one of our faux windows, so they are unsubclassing.
struct GODOTWND* current = head; struct GODOTWND* next = current->next;
if((next != NULL) && (next->godotID == dwNewLong) && (next->fWndproc == fWndproc)) { // Must handle the "head" case separately:
// basically it's just a pop operation.
RetVal = (LONG)current->userLpfn; GodotHeapFree(current);
// CONSIDER: If they unsubclassed out of order, then
// this next godotID might not be valid. We could
// clean up this (next->userLpfn == NULL) case now?
SetPropA(hwnd, m_szMemBlock, (HANDLE)next); LeaveCriticalSection(&g_csWnds); return(RetVal); } while(current != NULL) { next = current->next;
// Make sure there is a "next" wndproc, that it is
// both the proc and proctypethat we want
if((next != NULL) && (next->godotID == dwNewLong) && (next->fWndproc == fWndproc)) { // We do not free it up since it is not at the head.
// It will be zombied by having a NULL userLpfn.
RetVal = (LONG)next->userLpfn; current->userLpfn = NULL; LeaveCriticalSection(&g_csWnds); return(RetVal); } current = current->next; } // We never found it. Let the user know their call failed.
// Note that this will handle cases we could have detected
// such as trying to unsubclass the true head, trying to
// unsubclass a dlgproc with a wndproc nIndex, etc. Since
// they all get the same error, there was just no need.
SetLastError(ERROR_INVALID_PARAMETER); RetVal = 0; LeaveCriticalSection(&g_csWnds); return(RetVal); } else { // This is a new subclass proc!
struct GODOTWND* current = GodotHeapAlloc(sizeof(struct GODOTWND));
if(current == NULL) { // Cannot allocate, so bail.
SetLastError(ERROR_OUTOFMEMORY); LeaveCriticalSection(&g_csWnds); return(RetVal); } else { // The original subclass window stays the same, we just put the
// new info into the linked list. We basically just do a push
// on it so it is at the head. The extra SetProp here will keep
// us from (hopefully) running into Win9x property bugs.
current->userLpfn = (WNDPROC)dwNewLong; current->fWndproc = (BYTE)fWndproc; current->godotID = ZEORETHGODOTWND - (CWnd(head) + 1); current->fUnicode = (BYTE)fUnicode; current->next = head; SetPropA(hwnd, m_szMemBlock, (HANDLE)current);
// Ok, make sure we return the right previous proc
// based on what type of proc they asked to replace.
current = current->next; while(current) { if((fWndproc == current->fWndproc) && (current->userLpfn)) { RetVal = (LONG)current->godotID; break; } current = current->next; } LeaveCriticalSection(&g_csWnds); return(RetVal); } } } } else { // Its not even for a wndproc, so we will just ask the OS
return(SetWindowLongA(hwnd, nIndex, dwNewLong)); } }
/*-------------------------------
GetUnicodeWindowProp
Returns TRUE if this window has the UNICODE window prop set. -------------------------------*/ BOOL GetUnicodeWindowProp(HWND hwnd) { struct GODOTWND* current = GetPropA(hwnd, m_szMemBlock); return(current != NULL); }
/*-------------------------------
DoesProcExpectAnsi
Returns TRUE if the proc for the window is the one provided by the system by default, or if we marked as ANSI for any other reason. Note that this proc can accept a *real* wndproc rather than a GodotID here; we detect either case. -------------------------------*/ BOOL DoesProcExpectAnsi(HWND hwnd, WNDPROC godotID, FAUXPROCTYPE fpt) { struct GODOTWND* wnd; BOOL RetVal = TRUE;
__try { EnterCriticalSection(&g_csWnds); wnd = GetPropA(hwnd, m_szMemBlock); if(wnd==NULL) { RetVal = TRUE; } else if(wnd && (godotID == 0)) { while(wnd != NULL) { // UNDONE: What do we do in the (fpt==fptUnknown && godotID==0) case?
if(((fpt==fptWndproc && wnd->fWndproc) || (fpt==fptDlgproc && !wnd->fWndproc)) && (wnd->userLpfn)) { RetVal = !wnd->fUnicode; break; } wnd = wnd->next; } } else { while(wnd != NULL) { if(((INSIDE_GODOT_RANGE(godotID)) && (wnd->godotID == (UINT)godotID)) || ((OUTSIDE_GODOT_RANGE(godotID)) && wnd->userLpfn == godotID)) { while(wnd != NULL) { if(wnd->userLpfn) { RetVal = !wnd->fUnicode; break; } wnd = wnd->next; } break; } wnd = wnd->next; } } } __except( EXCEPTION_EXECUTE_HANDLER ) { RetVal = TRUE; }
LeaveCriticalSection(&g_csWnds); return(RetVal); }
/*-------------------------------
WndprocFromFauxWndproc
Given a Godot ID, return a valid wndproc. Returns the original faux wndproc if it cannot find a valid real one (probably due to there not being one!). -------------------------------*/ WNDPROC WndprocFromFauxWndproc(HWND hwnd, WNDPROC fauxLpfn, FAUXPROCTYPE fpt) { struct GODOTWND* wnd; WNDPROC RetVal = fauxLpfn; __try { EnterCriticalSection(&g_csWnds); wnd = GetPropA(hwnd, m_szMemBlock);
if(fauxLpfn == 0) { // A "0" faux lpfn is a signal to just
// pass back the top valid userLpfn.
while(wnd != NULL) { if(((fpt==fptWndproc) && (wnd->fWndproc)) || ((fpt==fptDlgproc) && (!wnd->fWndproc))) { RetVal = wnd->userLpfn; break; } wnd = wnd->next; } } else if(OUTSIDE_GODOT_RANGE(fauxLpfn)) { // Not a faux wndproc, so we will just return
// what was passed in, and assume it is valid
} else { // We iterate through our GODOTWND
// linked list, looking for a match.
while (wnd != NULL) { if(wnd->godotID == (UINT)fauxLpfn) { // Found it!!! Now we have to make sure it
// is still a valid one (i.e. that user has
// not unsubclassed out of order!)
while ((wnd != NULL) && (((fpt==fptWndproc) && (wnd->fWndproc)) || ((fpt==fptDlgproc) && (!wnd->fWndproc))) || ((fpt==fptUnknown) && (wnd->userLpfn == NULL))) wnd=wnd->next; break; } wnd = wnd->next; }
// If we do not have a wnd at this point, then there was no
// likely candidate, so return the caller's faux wndproc.
// Otherwiwe, pass the one we found.
if(wnd != NULL) RetVal = wnd->userLpfn; } } __except( EXCEPTION_EXECUTE_HANDLER ) { RetVal = fauxLpfn; }
LeaveCriticalSection(&g_csWnds); return(RetVal); }
/*-------------------------------
CWnd How big is this GODOTWND linked list? -------------------------------*/ int CWnd(struct GODOTWND* head) { int count = 0; struct GODOTWND* current; current = head;
for (current = head; current != NULL; current = current->next) { count++; }
return(count); }
/*
// A little system classes struct
struct ANSICLASS { WCHAR * wzName; };
// These are all the documented system classes. Also, for Toolbar windows, we
// need to mark the Unicodality as FALSE, since MFC uses the SetWindowText and
// GetWindowText APIs for its custom draw min-caption-like things for floating
// toolbars. This is only the low level wndproc which will get this setting,
// so it should not adversely affect anyone.
// The other common controls are added just in case anyone ELSE overloads msgs
// such as window titles for their own purposes the way that MFC decided to do.
// BASE SYSTEM CLASSES are marked with an asterick.
struct ANSICLASS m_rgwzAnsiClasses[] = { {L"#32768"}, // *#32768
{L"#32769"}, // *#32769
{L"#32770"}, // *#32770
{L"#32771"}, // *#32771
{WC_BUTTONW}, // *Button
{WC_COMBOBOXW}, // *ComboBox
{WC_COMBOBOXEXW}, // ComboBoxEx32
{L"ComboLBox"}, // *ComboLBox
{WC_EDITW}, // *Edit
{WC_LISTBOXW}, // *ListBox
{L"MDIClient"}, // *MDIClient
{STATUSCLASSNAMEW}, // msctls_statusbar32
{TRACKBAR_CLASSW}, // msctls_trackbar32
{UPDOWN_CLASSW}, // msctls_updown32
{PROGRESS_CLASSW}, // msctls_progress32
{HOTKEY_CLASSW}, // msctls_hotkey32
{REBARCLASSNAMEW}, // ReBarWindow32
{WC_SCROLLBARW}, // *ScrollBar
{WC_STATICW}, // *Static
{WC_PAGESCROLLERW}, // SysPager
{WC_IPADDRESSW}, // SysIPAddress32
{DATETIMEPICK_CLASSW}, // SysDateTimePick32
{MONTHCAL_CLASSW}, // SysMonthCal32
{ANIMATE_CLASSW}, // SysAnimate32
{WC_HEADERW}, // SysHeader32
{WC_LISTVIEWW}, // SysListView32
{WC_TREEVIEWW}, // SysTreeView32
{WC_TABCONTROLW}, // SysTabControl32
{TOOLBARCLASSNAMEW}, // ToolbarWindow32
{TOOLTIPS_CLASSW}, // tooltips_class32
{NULL} };
//-------------------------------
// IsAnAnsiClass
//
// Returns TRUE if the class is a "system" class (which has
// a default wndproc). Also returns TRUE for other classes
// which need to be marked as "ANSI".
--------------------------------- BOOL IsAnAnsiClass(LPCWSTR lpwz) { if(FSTRING_VALID(lpwz)) { // PERF: We optimize by making sure that the first character matches
// one of the known system classes. Be sure to add the case
// here if you add any classes to m_rgwzAnsiClasses!
switch(*lpwz) case '#': case 'B': case 'b': case 'C': case 'c': case 'E': case 'e': case 'L': case 'l': case 'M': case 'm': case 'R': case 'S': case 's': case 'T': case 't': { int iClass = 0;
while(m_rgwzAnsiClasses[iClass].wzName) { if(CompareHelper(lpwz, (LPWSTR)(m_rgwzAnsiClasses[iClass].wzName), FALSE) == 0) { return(TRUE); } iClass++; } break; } } return(FALSE); }
*/
|