Show tooltip on invalid input in edit control
Asked Answered
S

3

11

I have subclassed edit control to accept only floating numbers. I would like to pop a tooltip when user makes an invalid input. The behavior I target is like the one edit control with ES_NUMBER has :

enter image description here

So far I was able to implement tracking tooltip and display it when user makes invalid input.

However, the tooltip is misplaced. I have tried to use ScreenToClient and ClientToScreen to fix this but have failed.

Here are the instructions for creating SCCE :

1) Create default Win32 project in Visual Studio.

2) Add the following includes in your stdafx.h, just under #include <windows.h> :

#include <windowsx.h>
#include <commctrl.h>

#pragma comment( lib, "comctl32.lib")

#pragma comment(linker, \
    "\"/manifestdependency:type='Win32' "\
    "name='Microsoft.Windows.Common-Controls' "\
    "version='6.0.0.0' "\
    "processorArchitecture='*' "\
    "publicKeyToken='6595b64144ccf1df' "\
    "language='*'\"")

3) Add these global variables:

HWND g_hwndTT;
TOOLINFO g_ti;

4) Here is a simple subclass procedure for edit controls ( just for testing purposes ) :

LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam, 
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            POINT pt;
            if( ! isdigit( wParam ) )  // if not a number pop a tooltip!
            {
                if (GetCaretPos(&pt))  // here comes the problem
                {
                    // coordinates are not good, so tooltip is misplaced
                    ClientToScreen( hwnd, &pt );


                    /************************** EDIT #1 ****************************/
                    /******* If I delete this line x-coordinate is OK *************/
                    /*** y-coordinate should be little lower, but it is still OK **/
                    /**************************************************************/

                    ScreenToClient( GetParent(hwnd), &pt );

                    /************************* Edit #2 ****************************/

                    // this adjusts the y-coordinate, see the second edit
                    RECT rcClientRect;
                    Edit_GetRect( hwnd, &rcClientRect );
                    pt.y = rcClientRect.bottom;

                    /**************************************************************/

                    SendMessage(g_hwndTT, TTM_TRACKACTIVATE, 
                        TRUE, (LPARAM)&g_ti);
                    SendMessage(g_hwndTT, TTM_TRACKPOSITION, 
                        0, MAKELPARAM(pt.x, pt.y));
                }
                return FALSE;
            }
            else
            {
                SendMessage(g_hwndTT, TTM_TRACKACTIVATE, 
                    FALSE, (LPARAM)&g_ti);
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
} 

5) Add the following WM_CREATE handler :

case WM_CREATE:
    {
        HWND hEdit = CreateWindowEx( 0, L"EDIT", L"edit", WS_CHILD | WS_VISIBLE |
            WS_BORDER | ES_CENTER, 150, 150, 100, 30, hWnd, (HMENU)1000, hInst, 0 );

        // try with tooltip
        g_hwndTT = CreateWindow(TOOLTIPS_CLASS, NULL,
            WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
            0, 0, 0, 0, hWnd, NULL, hInst, NULL);

        if( !g_hwndTT )
            MessageBeep(0);  // just to signal error somehow

        g_ti.cbSize = sizeof(TOOLINFO);
        g_ti.uFlags = TTF_TRACK | TTF_ABSOLUTE;
        g_ti.hwnd = hWnd;
        g_ti.hinst = hInst;
        g_ti.lpszText = TEXT("Hi there");

        if( ! SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&g_ti) )
            MessageBeep(0);  // just to have some error signal

        // subclass edit control
        SetWindowSubclass( hEdit, EditSubProc, 0, 0 );
    }
    return 0L;  

6) Initialize common controls in MyRegisterClass ( before return statement ) :

// initialize common controls
INITCOMMONCONTROLSEX iccex;
iccex.dwSize = sizeof(INITCOMMONCONTROLSEX);
iccex.dwICC = ICC_BAR_CLASSES | ICC_WIN95_CLASSES | 
    ICC_TAB_CLASSES | ICC_TREEVIEW_CLASSES | ICC_STANDARD_CLASSES ;

if( !InitCommonControlsEx(&iccex) ) 
    MessageBeep(0);   // signal error 

That's it, for the SSCCE.

My questions are following :

  1. How can I properly position tooltip in my main window? How should I manipulate with caret coordinates?

  2. Is there a way for tooltip handle and toolinfo structure to not be global?

Thank you for your time.

Best regards.

EDIT #1:

I have managed to achieve quite an improvement by deleting ScreenToClient call in the subclass procedure. The x-coordinate is good, y-coordinate could be slightly lower. I still would like to remove global variables somehow...

EDIT #2:

I was able to adjust y-coordinate by using EM_GETRECT message and setting y-coordinate to the bottom of the formatting rectangle:

RECT rcClientRect;
Edit_GetRect( hwnd, &rcClientRect );
pt.y = rcClient.bottom;

Now the end-result is much better. All that is left is to remove global variables...

EDIT #3:

It seems that I have cracked it! The solution is in EM_SHOWBALLOONTIP and EM_HIDEBALLOONTIP messages! Tooltip is placed at the caret position, ballon shape is the same as the one on the picture, and it auto-dismisses itself properly. And the best thing is that I do not need global variables!

Here is my subclass procedure snippet:

case WM_CHAR:
{
    // whatever... This condition is for testing purpose only
    if( ! IsCharAlpha( wParam ) && IsCharAlphaNumeric( wParam ) )
    {
        SendMessage(hwnd, EM_HIDEBALLOONTIP, 0, 0);
        return ::DefSubclassProc( hwnd, message, wParam, lParam );
    }
    else
    {
        EDITBALLOONTIP ebt;

        ebt.cbStruct = sizeof( EDITBALLOONTIP );
        ebt.pszText = L" Tooltip text! ";
        ebt.pszTitle = L" Tooltip title!!! ";
        ebt.ttiIcon = TTI_ERROR_LARGE;    // tooltip icon

        SendMessage(hwnd, EM_SHOWBALLOONTIP, 0, (LPARAM)&ebt);

        return FALSE;
    }
 }
 break;
Sidero answered 27/5, 2014 at 14:52 Comment(8)
MSDN Docs for TTM_TRACKPOSITION says that the x/y values are "in screen coordinates"Perrine
@EdwardClements: Indeed, after removing ScreenToClient I have placed it properly. I wish y-coordinate to be a little lower, so maybe I should just add pt.y += 10 before passing it to ClientToScreen... Still, I do not know how to get rid of global variables... Thank you for pointing out what I have missed. Best regards.Sidero
If you use DrawText with the DT_CALCRECT flag, you can get the height of the last entered char (it's the same value for both . and I). If you add this to the yPos, you get the same yPos as the image you've shown. I agree with @EdwardClements, that a good place to stuff the info would be into the USERDATA field of the editWnd. However, I personally would use the SetProp function for this task, it's much cleaner and clearer imho. (Dont forget to call RemoveProp before destroying the window)Pantalets
@enhzflep: I have managed to get proper coordinate as described in my second edit. In the past, you have suggested me to send structure as dwRefData in my call to SetWindowSubclas API. I am interested to apply Edward's solution but I will need help, since this will be my first time to do this. If you are interested to help I would appreciate it. Thank you for your time :)Sidero
I did notice that, just thought I'd add code that I believe reproduces the image. One other advantage to using SetProp, is that the control can delete the info itself (rather than the parent wnd having to do so) Passing it via the dwRefData variable requires that you keep a reference to the data in the parent window's WndProc. Anyway, there's all kinds of alternatives. I'll post my complete (C::B) code as an 'answer' to ensure completeness.Pantalets
@enhzflep: Thank you, I really appreciate it. All that would be left for me is to set delay time for a tracking tooltip. So far there is nothing I can use...Sidero
@enhzflep: I apologize for disturbing, but may I ask you to help me with this problem? I am really stuck... How are things going "down there"? :)Sidero
@Sidero - no problem. I'll see what I can do. Things good down here - bloody cold. 4°! Just a quick comment - you should be able to use the HDC of a visible window (e.g a dialog), rather than one gained from a printer (for testing purposes). This allows you to interactively resize the window and watch the size of the text change. Just do the computation in a func called from WM_PAINT handler and Invalidate the window when you get a WM_SIZE. Also, you could start at an arbitrary text size and decrease it iteratively till it's no longer too big. I'll see what I can work out. :)Pantalets
P
4

I'm giving the comment as an answer (I should have done that earlier) so that it's clear that the question has been answered:

MSDN Docs for TTM_TRACKPOSITION says that the x/y values are "in screen coordinates".

I'm not totally sure, but the y-coordinate probably corresponds to the top of the caret, you could add half of the edit box height if you want to position your tooltip in the middle of the edit box.

EDIT re Global variables, you could bundle all your global variables into a structure, allocate memory for the structure and pass the pointer of the structure using the SetWindowLongPtr API call for the edit window using the GWLP_USERDATA, the window proc can then retrieve the values using GetWindowLongPtr...

Perrine answered 27/5, 2014 at 16:53 Comment(7)
I have just updated the post with new findings. I am nearly "there" regarding the tooltip coordinates. I still can't figure out how to get rid of global variables though... +1 for now.Sidero
I could pass the structure when I use SetWindowSubclass, since 4th parameter is usually used for that... I just am confused a little: Since I will use this procedure to subclass multiple edit controls I fear that there will be some side-effects with the tooltip... I need a break, before I test your approach and mine. Thank you for all your help. Best regards.Sidero
I have never used the way proposed by member enhzflep so far, nor yours, so if you could help me with some pseudo code I would appreciate it.Sidero
I will try to handle the problem myself. Your answer covered what I needed. Still if you are able to help me with proper usage of SetProp or SetWindowPtr I would appreciate it.Sidero
Hope it's okay. Just realized I should have placed if (tmp) in the WM_CHAR case in such a way that it not only controlled the sending of the messages, but also that of the coordinate calculating code too.Pantalets
It seems that I have cracked it! Can you please check my third edit and verify my conclusions, since you are better/more experienced than me. Tank you. Best regards!Sidero
Looks ok to me -- short and elegant! [also looked at your answer, you could remove calls to DefSubclassProc() for WM_CHAR and WM_NCDESTROY and just break]Perrine
S
7

After further testing, I have decided to put this as an answer so others can clearly spot it.

The solution is in using EM_SHOWBALLOONTIP and EM_HIDEBALLOONTIP messages. You do not need to create tooltip and associate it to an edit control! Therefore, all I need to do now is simply subclass edit control and everything works :

LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message, 
WPARAM wParam, LPARAM lParam, 
UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            if( ! isdigit( wParam ) )  // if not a number pop a tooltip!
            {
                EDITBALLOONTIP ebt;

                ebt.cbStruct = sizeof( EDITBALLOONTIP );
                ebt.pszText = L" Tooltip text! ";
                ebt.pszTitle = L" Tooltip title!!! ";
                ebt.ttiIcon = TTI_ERROR_LARGE;    // tooltip icon

                SendMessage(hwnd, EM_SHOWBALLOONTIP, 0, (LPARAM)&ebt);
                return FALSE;
            }
            else
            {
                SendMessage(hwnd, EM_HIDEBALLOONTIP, 0, 0);
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
} 

That's it!

Hopefully this answer will help someone too!

Sidero answered 29/5, 2014 at 11:44 Comment(0)
P
4

I'm giving the comment as an answer (I should have done that earlier) so that it's clear that the question has been answered:

MSDN Docs for TTM_TRACKPOSITION says that the x/y values are "in screen coordinates".

I'm not totally sure, but the y-coordinate probably corresponds to the top of the caret, you could add half of the edit box height if you want to position your tooltip in the middle of the edit box.

EDIT re Global variables, you could bundle all your global variables into a structure, allocate memory for the structure and pass the pointer of the structure using the SetWindowLongPtr API call for the edit window using the GWLP_USERDATA, the window proc can then retrieve the values using GetWindowLongPtr...

Perrine answered 27/5, 2014 at 16:53 Comment(7)
I have just updated the post with new findings. I am nearly "there" regarding the tooltip coordinates. I still can't figure out how to get rid of global variables though... +1 for now.Sidero
I could pass the structure when I use SetWindowSubclass, since 4th parameter is usually used for that... I just am confused a little: Since I will use this procedure to subclass multiple edit controls I fear that there will be some side-effects with the tooltip... I need a break, before I test your approach and mine. Thank you for all your help. Best regards.Sidero
I have never used the way proposed by member enhzflep so far, nor yours, so if you could help me with some pseudo code I would appreciate it.Sidero
I will try to handle the problem myself. Your answer covered what I needed. Still if you are able to help me with proper usage of SetProp or SetWindowPtr I would appreciate it.Sidero
Hope it's okay. Just realized I should have placed if (tmp) in the WM_CHAR case in such a way that it not only controlled the sending of the messages, but also that of the coordinate calculating code too.Pantalets
It seems that I have cracked it! Can you please check my third edit and verify my conclusions, since you are better/more experienced than me. Tank you. Best regards!Sidero
Looks ok to me -- short and elegant! [also looked at your answer, you could remove calls to DefSubclassProc() for WM_CHAR and WM_NCDESTROY and just break]Perrine
P
2

As a follow-up to comments regarding the use of the SetProp function to remove the need to hold onto a pair of globals for the tool-tip data, I present the following solution.

Note: By error-checking on calls to GetProp, I've designed a WndProc for the subclassed edit control that would function regardless of whether or not it was desired to make use of tool-tips. If the property isn't found, I simply omit any tool-tip handling code.

Note 2: One downside to all of the available approaches to making the tooltip info non-global is that it introduces coupling between the subclassed WndProc and the parent window's wndProc.

  • By using dwRefData, one must check that it holds a non-NULL pointer.
  • By using SetWindowLongPtr, one must remember an index into the user-data.
  • By using SetProp, one must remember a textual property name. I find this easier.

Removing the call to SetProp removes the tool-tip functionality. I.e you could use the same subclassed wndProc for edit controls whether they took advantage of tooltips or not.

Anyhoo, on with the (Code::Blocks) code.

#define _WIN32_IE 0x0500
#define _WIN32_WINNT 0x0501

#if defined(UNICODE) && !defined(_UNICODE)
    #define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
    #define UNICODE
#endif

#include <tchar.h>
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <ctype.h>
#include <cstdio>

/*  Declare Windows procedure  */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);

/*  Make the class name into a global variable  */
TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp");



HWND g_hwndTT;
TOOLINFO g_ti;
typedef struct mToolTipInfo
{
    HWND hwnd;
    TOOLINFO tInfo;
} * p_mToolTipInfo;


LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message,
    WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    p_mToolTipInfo tmp = (p_mToolTipInfo)GetProp(hwnd, _T("tipData"));

    switch (message)
    {
    case WM_CHAR:
        {
            POINT pt;

            if( ! isdigit( wParam ) )  // if not a number pop a tooltip!
            {
                if (GetCaretPos(&pt))  // here comes the problem
                {
                    // coordinates are not good, so tooltip is misplaced
                    ClientToScreen( hwnd, &pt );

                    RECT lastCharRect;
                    lastCharRect.left = lastCharRect.top = 0;
                    lastCharRect.right = lastCharRect.bottom = 32;

                    HDC editHdc;
                    char lastChar;
                    int charHeight, charWidth;

                    lastChar = (char)wParam;
                    editHdc = GetDC(hwnd);
                    charHeight = DrawText(editHdc, &lastChar, 1, &lastCharRect, DT_CALCRECT);
                    charWidth = lastCharRect.right;
                    ReleaseDC(hwnd, editHdc);

                    //pt.x += xOfs + charWidth; // invalid char isn't drawn, so no need to advance xPos to reflect width of last char
                    pt.y += charHeight;

                    if (tmp)
                    {
                        SendMessage(tmp->hwnd, TTM_TRACKACTIVATE, TRUE, (LPARAM)&tmp->tInfo);
                        SendMessage(tmp->hwnd, TTM_TRACKPOSITION, 0, MAKELPARAM(pt.x, pt.y));
                    }
                }
                return FALSE;
            }
            else
            {
                if (tmp)
                    SendMessage(tmp->hwnd, TTM_TRACKACTIVATE,
                    FALSE, (LPARAM)&tmp->tInfo  );
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;

    case WM_DESTROY:
        {
            p_mToolTipInfo tmp = (p_mToolTipInfo)GetProp(hwnd, _T("tipData"));
            if (tmp)
            {
                delete(tmp);
                RemoveProp(hwnd, _T("tipData"));
            }
        }
        return 0;

    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
}






HINSTANCE hInst;

int WINAPI WinMain (HINSTANCE hThisInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR lpszArgument,
                     int nCmdShow)
{
    HWND hwnd;               /* This is the handle for our window */
    MSG messages;            /* Here messages to the application are saved */
    WNDCLASSEX wincl;        /* Data structure for the windowclass */

    /* The Window structure */
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;      /* This function is called by windows */
    wincl.style = CS_DBLCLKS;                 /* Catch double-clicks */
    wincl.cbSize = sizeof (WNDCLASSEX);

    /* Use default icon and mouse-pointer */
    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;                 /* No menu */
    wincl.cbClsExtra = 0;                      /* No extra bytes after the window class */
    wincl.cbWndExtra = 0;                      /* structure or the window instance */
    /* Use Windows's default colour as the background of the window */
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    /* Register the window class, and if it fails quit the program */
    if (!RegisterClassEx (&wincl))
        return 0;

    /* The class is registered, let's create the program*/
    hwnd = CreateWindowEx (
           0,                   /* Extended possibilites for variation */
           szClassName,         /* Classname */
           _T("Code::Blocks Template Windows App"),       /* Title Text */
           WS_OVERLAPPEDWINDOW, /* default window */
           CW_USEDEFAULT,       /* Windows decides the position */
           CW_USEDEFAULT,       /* where the window ends up on the screen */
           544,                 /* The programs width */
           375,                 /* and height in pixels */
           HWND_DESKTOP,        /* The window is a child-window to desktop */
           NULL,                /* No menu */
           hThisInstance,       /* Program Instance handler */
           NULL                 /* No Window Creation data */
           );

    /* Make the window visible on the screen */
    ShowWindow (hwnd, nCmdShow);

    /* Run the message loop. It will run until GetMessage() returns 0 */
    while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Translate virtual-key messages into character messages */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */
        DispatchMessage(&messages);
    }

    /* The program return-value is 0 - The value that PostQuitMessage() gave */
    return messages.wParam;
}


/*  This function is called by the Windows function DispatchMessage()  */
LRESULT CALLBACK WindowProcedure (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)                  /* handle the messages */
    {
        case WM_CREATE:
        {
            HWND hEdit = CreateWindowEx( 0, _T("EDIT"), _T("edit"), WS_CHILD | WS_VISIBLE |
                WS_BORDER | ES_CENTER, 150, 150, 100, 30, hWnd, (HMENU)1000, hInst, 0 );

            p_mToolTipInfo tmp = new mToolTipInfo;
            SetProp(hEdit, _T("tipData"), tmp);

            // try with tooltip
            //g_hwndTT = CreateWindow(TOOLTIPS_CLASS, NULL,
            tmp->hwnd = CreateWindow(TOOLTIPS_CLASS, NULL,
                WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
                0, 0, 0, 0, hWnd, NULL, hInst, NULL);

            //if( !g_hwndTT )
            if( !tmp->hwnd )
                MessageBeep(0);  // just to signal error somehow

//            g_ti.cbSize = sizeof(TOOLINFO);
//            g_ti.uFlags = TTF_TRACK | TTF_ABSOLUTE;
//            g_ti.hwnd = hWnd;
//            g_ti.hinst = hInst;
//            g_ti.lpszText = _T("Hi there");
            tmp->tInfo.cbSize = sizeof(TOOLINFO);
            tmp->tInfo.uFlags = TTF_TRACK | TTF_ABSOLUTE;
            tmp->tInfo.hwnd = hWnd;
            tmp->tInfo.hinst = hInst;
            tmp->tInfo.lpszText = _T("Hi there");

//            if( ! SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&g_ti) )
            if( ! SendMessage(tmp->hwnd, TTM_ADDTOOL, 0, (LPARAM)&tmp->tInfo) )
                MessageBeep(0);  // just to have some error signal

            // subclass edit control
            SetWindowSubclass( hEdit, EditSubProc, 0, 0 );
        }
        return 0L;

        case WM_DESTROY:
            PostQuitMessage (0);       /* send a WM_QUIT to the message queue */
            break;
        default:                      /* for messages that we don't deal with */
            return DefWindowProc (hWnd, message, wParam, lParam);
    }

    return 0;
}
Pantalets answered 27/5, 2014 at 18:30 Comment(5)
Thank you for helping. Upvoted. I was more interested to adapt your suggestion with your answer to this question. In the light of those answers, I have decided to validate input in EN_UPDATE and to subclass the control to discard invalid characters. In EN_UPDATE handler I also change the color of the edit control to indicate validity. I just thought to pop a tooltip that would list locale valid characters, on invalid character. Incorporating this solution will be little difficult...Sidero
You're welcome. Thanks. Uh-huh, sure I'll have a look a little later when I fire up a 'real' pc (using rasPi just now). I suppose that it shouldn't be too hard to implement a standard message that has the locale-specific characters appended to it. Something like: string msg = "only valid (0-9) and "; string += localeSpecificCharsString;. Probably just an extra field or three to add to the instance-specific data. I'll have a look and a think. Hadn't even considered that question when I wrote this - oops!Pantalets
After with ES_NUMBER edit control I saw it has different shape of balloon tip and balloon tip disappears after some time. This means that MS did not use tracking tooltip to implement it, but maybe bound ordinary tooltip to a rectangle. Aside from that, I ran your example and it has the same performance as mine. This makes me wonder if my approach from second edit is better than going through the "get HDC, DrawText etc" stuff you did since I get formatting rectangle-see the docs I linked to... I will try to find a way to implement the behavior without tracking tooltip. Thanks again!Sidero
Brilliant! Bravo and congratulations. Bookmarked for future reference.Pantalets
Thank you for all your help and efforts. 'Till next time... :)Sidero

© 2022 - 2024 — McMap. All rights reserved.