|
Entering a digit in a CAMSNumericEdit/CAMSCurrencyEdit control that has full text selected places cursor on left side of digit instead of right, forcing following entered digits at left of first.
To reproduce problem open ValidatingEditDemoDlg.cpp of ValidatingEditDemo app, insert following code after _tsetlocale() call:
m_ctlEditCurrencyDataEntry.SetPrefix("");
m_ctlEditCurrencyDataEntry.SetMaxWholeDigits(9);
m_ctlEditCurrencyDataEntry.SetMaxDecimalPlaces(2);
compile and run, enter 1234 into currency edit control, tab out, tab in, enter a digit: the cursor will be placed on left of digit. This behavior appears when you does not have a prefix set, one or more group separators are present and there are some decimals digits.
To fix this behavior go to the AdjustSeparators() function, change the test:
if (nCurrentSeparatorCount != nNewSeparatorCount && . . .
to:
if ((nNewSeparatorCount > 0) && nCurrentSeparatorCount != nNewSeparatorCount && selection.GetStart() > m_strPrefix.GetLength())
Regards,
Carlo Marsura
|
|
|
|
|
Hi Alvaro, I am using CAMSCurrencyEdit in an OCX and during test phase discovered some little bugs, one of
them regarding locale (anew!).
Briefly, I will describe them:
First - environment:
- Windows International Language Settings set to Italian, Decimal Separator is ',', Group Separator is '.'
- Using AMS* classes from a DLL (OCX)
- Cannot force applications to call _tsetlocale()
Second - problems:
a) Applications does not call _tsetlocale() (it's a VB app), NumericBehavior.GetDouble() returns values without decimal part.
b) Changing separators at runtime with NumericBehavior.SetSeparators() when value is set change value afterwards
c) Minor inconsistencies in code using directly '.' and '-' instead of m_cDecimalPoint and m_cNegativeSign
d) Compiler issues an error when not defined both _UNICODE and _MBCS
Third - comments:
I noted that CString.Format(), _tcstod(), _ttoi() functions are using settings retrieved from _tsetlocale().
If you does not call _tsetlocale(), the default is '.' for Decimal Separator; this is independent of values returned from
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, ...
Bugs can be reproduced also changing your demo app accordingly.
Fourth - solutions:
Problem a): Reviewed NumericBehavior class. Added the NumericBehavior::GetLocaleCharacters(), using it in all places
where code works with strings directly involved with locale-affected runtime functions (CString.Format(),
_tcstod(), ...), reviewed last parameter passed to GetNumericText(). This change permits to class work well whether
_tsetlocale() is called or not by the app.
Problem b): Added the CAMSEdit::MultiCharReplace() and using it in NumericBehavior::SetSeparators().
Problem c): Changed use of '.' and '-' in favor of m_cDecimalPoint and m_cNegativeSign.
Problem d): Adding compiler preprocessor directive in amsEdit.h to check if _MBCS is defined inside _UNICODE
Follows here the code changes marked with comments containing "Carlo" (remove them).
The source is relative to Version 3 you posted.
Keep up with this good work.
Regards,
Carlo.
#if !defined(AFX_AMS_EDIT_H__AC5ACB94_4363_11D3_9123_00105A6E5DE4__INCLUDED_)
#define AFX_AMS_EDIT_H__AC5ACB94_4363_11D3_9123_00105A6E5DE4__INCLUDED_
#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// Edit.h : header file
// Created by: Alvaro Mendez - 07/17/2000
//
// Source from http://www.codeproject.com/editctrl/validatingedit.asp
//
#include <afxwin.h>
#include <afxtempl.h>
// To export this code from an MFC Extension DLL uncomment the following line and then define _AMSEDIT_EXPORT inside the DLL's Project Settings.
// #define AMSEDIT_IN_DLL
// Import/Export macro for the classes below
#if defined(AMSEDIT_IN_DLL)
#if defined(_AMSEDIT_EXPORT)
#define AMSEDIT_EXPORT _declspec(dllexport)
#else
#define AMSEDIT_EXPORT _declspec(dllimport)
#endif
#else
#define AMSEDIT_EXPORT
#endif
// The following IDs are assigned for each class below to allow us to set which ones we need compiled
#define AMSEDIT_ALPHANUMERIC_CLASS 0x01
#define AMSEDIT_MASKED_CLASS 0x02
#define AMSEDIT_NUMERIC_CLASS 0x04
#define AMSEDIT_INTEGER_CLASS (AMSEDIT_NUMERIC_CLASS | 0x08)
#define AMSEDIT_CURRENCY_CLASS (AMSEDIT_NUMERIC_CLASS | 0x10)
#define AMSEDIT_DATE_CLASS 0x20
#define AMSEDIT_TIME_CLASS 0x40
#define AMSEDIT_DATETIME_CLASS (AMSEDIT_DATE_CLASS | AMSEDIT_TIME_CLASS)
#define AMSEDIT_ALL_CLASSES (AMSEDIT_ALPHANUMERIC_CLASS | AMSEDIT_MASKED_CLASS | AMSEDIT_INTEGER_CLASS | AMSEDIT_CURRENCY_CLASS | AMSEDIT_DATETIME_CLASS)
// If your program does not need all the CAMSEdit classes below, you can reduce
// the size of your executable by selecting just the classes you want to be compiled
// via the following macro. Use the IDs defined above and "OR" together the classes you need.
#define AMSEDIT_COMPILED_CLASSES AMSEDIT_ALL_CLASSES
#if !defined ELEMENTS_OF // (Added) Carlo
#define ELEMENTS_OF(v) (sizeof(v)/sizeof(v[0]))
#endif
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit window
// Class CAMSEdit is the base class for all the other AMS CEdit classes.
// It provides some base functionality to set and get the text and change
// its text and background color.
//
class AMSEDIT_EXPORT CAMSEdit : public CEdit
{
public:
// Construction/destruction
CAMSEdit();
virtual ~CAMSEdit();
// Operations
void SetText(const CString& strText);
CString GetText() const;
CString GetTrimmedText() const;
void SetBackgroundColor(COLORREF rgb);
COLORREF GetBackgroundColor() const;
void SetTextColor(COLORREF rgb);
COLORREF GetTextColor() const;
bool IsReadOnly() const;
protected:
virtual void Redraw();
virtual CString GetValidText() const;
virtual BOOL OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult);
virtual bool ShouldEnter(TCHAR c) const;
protected:
CBrush m_brushBackground;
COLORREF m_rgbText;
private:
enum InternalFlags
{
None = 0x0000,
TextColorHasBeenSet = 0x0001
};
UINT m_uInternalFlags;
public:
static CString MultiCharReplace(const CString & sText, int nElementsOfChars, const PTCHAR pcOldChars, const PTCHAR pcNewChars); // (Added) Carlo
// Class SelectionSaver is used to save an edit box's current
// selection and then restore it on destruction.
class AMSEDIT_EXPORT SelectionSaver
{
public:
SelectionSaver(CEdit* pEdit);
SelectionSaver(CEdit* pEdit, int nStart, int nEnd);
~SelectionSaver();
void MoveTo(int nStart, int nEnd);
void MoveBy(int nStart, int nEnd);
void MoveBy(int nPos);
void operator+=(int nPos);
int GetStart() const;
int GetEnd() const;
void Update();
void Disable();
protected:
CEdit* m_pEdit;
int m_nStart, m_nEnd;
};
// Class Behavior is an abstract base class used to define how an edit
// box will behave when it is used. Note that its virtual member functions start
// with an underscore; this avoids naming conflicts when multiply inheriting.
class AMSEDIT_EXPORT Behavior
{
protected:
Behavior(CAMSEdit* pEdit);
virtual ~Behavior();
public:
bool ModifyFlags(UINT uAdd, UINT uRemove);
UINT GetFlags() const;
public:
virtual CString _GetValidText() const = 0;
virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags) = 0;
virtual void _OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags);
virtual void _OnKillFocus(CWnd* pNewWnd);
virtual LONG _OnPaste(UINT wParam, LONG lParam);
protected:
// Wrappers to allow access to protected members of CAMSEdit
virtual LRESULT _Default();
virtual void _Redraw();
virtual bool _ShouldEnter(TCHAR c) const;
protected:
CAMSEdit* m_pEdit;
UINT m_uFlags;
};
friend class Behavior;
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)
// The AlphanumericBehavior class is used to allow entry of alphanumeric
// characters. It can be restricted in terms of what characters cannot
// be inputed as well as how many are allowed altogether.
class AMSEDIT_EXPORT AlphanumericBehavior : public Behavior
{
public:
AlphanumericBehavior(CAMSEdit* pEdit, int nMaxChars = 0, const CString& strInvalidChars = _T("%'*\"+?><:\\"));
// Operations
void SetInvalidCharacters(const CString& strInvalidChars);
const CString& GetInvalidCharacters() const;
void SetMaxCharacters(int nMaxChars);
int GetMaxCharacters() const;
protected:
virtual CString _GetValidText() const;
virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags);
protected:
int m_nMaxChars;
CString m_strInvalidChars;
};
#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)
// The MaskedBehavior class is used to allow entry of numeric characters
// based on a given mask containing '#' characters to hold digits.
class AMSEDIT_EXPORT MaskedBehavior : public Behavior
{
public:
// Construction
MaskedBehavior(CAMSEdit* pEdit, const CString& strMask = _T(""));
public:
// Operations
void SetMask(const CString& strMask);
const CString& GetMask() const;
CString GetNumericText() const;
// The Symbol class represents a character which may be added to the mask and then interpreted by the
// MaskedBehavior class to validate the input from the user and possibly convert it to something else.
class AMSEDIT_EXPORT Symbol
{
public:
#ifndef _UNICODE
#ifdef _MBCS // (Added) Carlo
typedef int (*ValidationFunction)(UINT); // designed for functions such as _istdigit, _istalpha
typedef UINT (*ConversionFunction)(UINT); // designed for functions such as _totupper, _totlower
#else
typedef int (*ValidationFunction)(INT); // designed for functions such as _istdigit, _istalpha
typedef UINT (*ConversionFunction)(INT); // designed for functions such as _totupper, _totlower
#endif
#else
typedef int (*ValidationFunction)(WCHAR);
typedef WCHAR (*ConversionFunction)(WCHAR);
#endif
Symbol();
Symbol(TCHAR cSymbol, ValidationFunction fnValidation, ConversionFunction fnConversion = NULL);
virtual ~Symbol();
virtual bool Validate(TCHAR c) const;
virtual TCHAR Convert(TCHAR c) const;
void Set(TCHAR cSymbol);
TCHAR Get() const;
operator TCHAR() const;
protected:
TCHAR m_cSymbol;
ValidationFunction m_fnValidation;
ConversionFunction m_fnConversion;
};
typedef CArray<Symbol, Symbol const&> SymbolArray;
SymbolArray& GetSymbolArray();
protected:
virtual CString _GetValidText() const;
virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags);
virtual void _OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags);
protected:
// Attributes
CString m_strMask;
SymbolArray m_arraySymbols;
};
#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_NUMERIC_CLASS)
// The NumericBehavior class is used to allow the entry of an actual numeric
// value into the edit control. It may be restricted by the number of digits
// before or after the decimal point (if any). If can also be set to use
// commas to separate and group thousands.
class AMSEDIT_EXPORT NumericBehavior : public Behavior
{
public:
// Construction
NumericBehavior(CAMSEdit* pEdit, int nMaxWholeDigits = 9, int nMaxDecimalPlaces = 4);
public:
// Operations
void SetDouble(double dText, bool bTrimTrailingZeros = true);
double GetDouble() const;
void SetInt(int nText);
int GetInt() const;
void SetMaxWholeDigits(int nMaxWholeDigits);
int GetMaxWholeDigits() const;
void SetMaxDecimalPlaces(int nMaxDecimalPlaces);
int GetMaxDecimalPlaces() const;
void AllowNegative(bool bAllowNegative = true);
bool IsNegativeAllowed() const;
void SetDigitsInGroup(int nDigitsInGroup);
int GetDigitsInGroup() const;
void SetSeparators(TCHAR cDecimal, TCHAR cGroup);
void GetSeparators(TCHAR* pcDecimal, TCHAR* pcGroup) const;
void SetPrefix(const CString& strPrefix);
const CString& GetPrefix() const;
void SetMask(const CString& strMask);
CString GetMask() const;
void SetRange(double dMin, double dMax);
void GetRange(double* pdMin, double* pdMax) const;
virtual bool IsValid() const;
bool CheckIfValid(bool bShowErrorIfNotValid = true);
static TCHAR GetLocaleCharacters(PTCHAR pcGroupSeparator = NULL, PTCHAR pcNegativeSign = NULL); // Carlo
enum Flags
{
.
.
.
(follows untouched source code)
// amsEdit.cpp : implementation file for CEdit-derived classes
// Created by: Alvaro Mendez - 07/17/2000
//
// Source from http://www.codeproject.com/editctrl/validatingedit.asp
//
#include "stdafx.h"
#include <locale.h> // (Added) Carlo
#include "amsEdit.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#pragma warning (disable:4355) // disables: 'this': used in base member initializer list
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit
// Constructs the object with the default attributes
CAMSEdit::CAMSEdit() :
m_rgbText(0),
m_uInternalFlags(None)
{
}
// Destroys the object (virtual).
CAMSEdit::~CAMSEdit()
{
}
BEGIN_MESSAGE_MAP(CAMSEdit, CEdit)
//{{AFX_MSG_MAP(CAMSEdit)
ON_WM_KEYDOWN()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_CUT, OnCut)
ON_MESSAGE(WM_PASTE, OnPaste)
ON_MESSAGE(WM_CLEAR, OnClear)
ON_MESSAGE(WM_SETTEXT, OnSetText)
END_MESSAGE_MAP()
// Returns the control's text.
CString CAMSEdit::GetText() const
{
CString strText;
if (::IsWindow(m_hWnd)) GetWindowText(strText); // (Changed) Carlo
return strText;
}
// Returns the control's text without leading or trailing blanks.
CString CAMSEdit::GetTrimmedText() const
{
CString strText = GetText();
strText.TrimLeft();
strText.TrimRight();
return strText;
}
// Sets the control's text to the given string value.
void CAMSEdit::SetText(const CString& strText)
{
if (::IsWindow(m_hWnd)) SetWindowText(strText); // (Changed) Carlo
}
// Sets the background color to the given rgb.
void CAMSEdit::SetBackgroundColor(COLORREF rgb)
{
m_brushBackground.DeleteObject();
m_brushBackground.CreateSolidBrush(rgb);
Invalidate();
}
// Returns the RGB for the background color.
COLORREF CAMSEdit::GetBackgroundColor() const
{
CAMSEdit* pThis = const_cast<CAMSEdit*>(this);
if (!m_brushBackground.GetSafeHandle())
{
COLORREF rgb = pThis->GetDC()->GetBkColor();
pThis->m_brushBackground.CreateSolidBrush(rgb);
return rgb;
}
LOGBRUSH lb;
pThis->m_brushBackground.GetLogBrush(&lb);
return lb.lbColor;
}
// Sets the text color to the given rgb.
void CAMSEdit::SetTextColor(COLORREF rgb)
{
m_rgbText = rgb;
m_uInternalFlags |= TextColorHasBeenSet;
Invalidate();
}
// Returns the RGB for the text color.
COLORREF CAMSEdit::GetTextColor() const
{
if (!(m_uInternalFlags & TextColorHasBeenSet))
{
CAMSEdit* pThis = const_cast<CAMSEdit*>(this);
pThis->m_rgbText = pThis->GetDC()->GetTextColor();
pThis->m_uInternalFlags |= TextColorHasBeenSet;
}
return m_rgbText;
}
// Returns true if the control is read only
bool CAMSEdit::IsReadOnly() const
{
return !!(GetStyle() & ES_READONLY);
}
// Returns the control's value in a valid format.
CString CAMSEdit::GetValidText() const
{
return GetText();
}
// Redraws the window's text.
void CAMSEdit::Redraw()
{
if (!::IsWindow(m_hWnd))
return;
CString strText = GetValidText();
if (strText != GetText())
SetWindowText(strText);
}
// Returns true if the given character should be entered into the control.
bool CAMSEdit::ShouldEnter(TCHAR) const
{
return true;
}
// Cuts the current selection into the clipboard.
LONG CAMSEdit::OnCut(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);
if (nStart < nEnd)
{
SendMessage(WM_COPY); // copy the selection and...
ReplaceSel(_T("")); // delete it
}
return 0;
}
// Clears the current selection.
LONG CAMSEdit::OnClear(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);
if (nStart < nEnd)
SendMessage(WM_KEYDOWN, VK_DELETE); // delete the selection
return 0;
}
// Pastes the text from the clipboard onto the current selection.
LONG CAMSEdit::OnPaste(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);
CEdit::Default();
CString strText = GetValidText();
if (strText != GetText())
{
SetWindowText(strText);
SetSel(nStart, nEnd);
}
return 0;
}
// Handles drawing the text and background using the designated colors
BOOL CAMSEdit::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
{
if ((message == WM_CTLCOLOREDIT || message == WM_CTLCOLORSTATIC) && (m_brushBackground.GetSafeHandle() || m_uInternalFlags & TextColorHasBeenSet))
{
CDC* pDC = CDC::FromHandle((HDC)wParam);
if (m_rgbText)
pDC->SetTextColor(m_rgbText);
// Set the text background to the requested background color
pDC->SetBkColor(GetBackgroundColor());
*pLResult = (LRESULT)m_brushBackground.GetSafeHandle();
return TRUE;
}
return CEdit::OnChildNotify(message, wParam, lParam, pLResult);
}
// Handles the WM_SETTEXT message to ensure that text (set via SetWindowText) is valid.
LONG CAMSEdit::OnSetText(UINT, LONG lParam)
{
LONG nResult = CEdit::Default();
CString strText = GetValidText();
if (strText != (LPCTSTR)lParam)
SetWindowText(strText);
return nResult;
}
// Search and replace in string all chars specified
CString CAMSEdit::MultiCharReplace(const CString & sText, int nElementsOfChars, const PTCHAR pcOldChars, const PTCHAR pcNewChars) // (Added) Carlo
{
CString sTextNew(sText);
// For each char to replace
for (int i = 0; i < nElementsOfChars; i++)
{
// For each occurrence of that char in source string
for (int pos = 0; (pos = sText.Find(pcOldChars[i], pos)) >= 0; pos++)
{
// Change relative char in destination string
sTextNew.SetAt(pos, pcNewChars[i]);
}
}
return sTextNew;
}
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::SelectionSaver
// Constructs the selection saver object for the given edit control.
// It then saves the edit control's current selection.
CAMSEdit::SelectionSaver::SelectionSaver(CEdit* pEdit) :
m_pEdit(pEdit)
{
ASSERT(pEdit);
pEdit->GetSel(m_nStart, m_nEnd);
}
// Constructs the selection saver object for the given edit control.
// It then saves the given nStart and nEnd values.
CAMSEdit::SelectionSaver::SelectionSaver(CEdit* pEdit, int nStart, int nEnd) :
m_pEdit(pEdit),
m_nStart(nStart),
m_nEnd(nEnd)
{
ASSERT(pEdit);
ASSERT(nStart <= nEnd);
}
// Destroys the object and restores the selection to the saved start and end values.
CAMSEdit::SelectionSaver::~SelectionSaver()
{
if (m_pEdit)
m_pEdit->SetSel(m_nStart, m_nEnd, TRUE);
}
// Changes the start and end values to nStart and nEnd respectively.
void CAMSEdit::SelectionSaver::MoveTo(int nStart, int nEnd)
{
ASSERT(nStart <= nEnd);
m_nStart = nStart;
m_nEnd = nEnd;
}
// Changes the start and end values by nStart and nEnd respectively.
void CAMSEdit::SelectionSaver::MoveBy(int nStart, int nEnd)
{
m_nStart += nStart;
m_nEnd += nEnd;
ASSERT(m_nStart <= m_nEnd);
}
// Changes both the start and end values by nPos.
void CAMSEdit::SelectionSaver::MoveBy(int nPos)
{
m_nStart += nPos;
m_nEnd += nPos;
}
// Changes both the start and end values by nPos.
void CAMSEdit::SelectionSaver::operator+=(int nPos)
{
MoveBy(nPos);
}
// Returns the value for the selection's start.
int CAMSEdit::SelectionSaver::GetStart() const
{
return m_nStart;
}
// Returns the value for the selection's end.
int CAMSEdit::SelectionSaver::GetEnd() const
{
return m_nEnd;
}
// Updates the selection's start and end with the current selection.
void CAMSEdit::SelectionSaver::Update()
{
if (m_pEdit)
m_pEdit->GetSel(m_nStart, m_nEnd);
}
// Disables resetting the selection in the destructor
void CAMSEdit::SelectionSaver::Disable()
{
m_pEdit = NULL;
}
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::Behavior
// Constructs the object from the given control.
CAMSEdit::Behavior::Behavior(CAMSEdit* pEdit) :
m_pEdit(pEdit),
m_uFlags(0)
{
ASSERT(m_pEdit);
}
// Destroys the object (virtual).
CAMSEdit::Behavior::~Behavior()
{
}
// Adds and removes flags from the behavior and then redraws the control
bool CAMSEdit::Behavior::ModifyFlags(UINT uAdd, UINT uRemove)
{
UINT uFlags = (m_uFlags & ~uRemove) | uAdd;
if (m_uFlags == uFlags)
return false;
m_uFlags = uFlags;
_Redraw();
return true;
}
// Returns the flags
UINT CAMSEdit::Behavior::GetFlags() const
{
return m_uFlags;
}
// Handles the WM_CHAR message by passing it to the control.
void CAMSEdit::Behavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
m_pEdit->OnChar(uChar, nRepCnt, nFlags);
}
// Handles the WM_KEYDOWN message by passing it to the control.
void CAMSEdit::Behavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
m_pEdit->OnKeyDown(uChar, nRepCnt, nFlags);
}
// Handles the WM_KILLFOCUS message by passing it to the control.
void CAMSEdit::Behavior::_OnKillFocus(CWnd* pNewWnd)
{
m_pEdit->OnKillFocus(pNewWnd);
}
// Handles the WM_PASTE message by passing it to the control.
LONG CAMSEdit::Behavior::_OnPaste(UINT wParam, LONG lParam)
{
return m_pEdit->OnPaste(wParam, lParam);
}
// Calls the default handler for the current message
LRESULT CAMSEdit::Behavior::_Default()
{
return m_pEdit->Default();
}
// Redraws the control so that its value is valid
void CAMSEdit::Behavior::_Redraw()
{
m_pEdit->Redraw();
}
// Returns true if the given character should be entered into the control.
bool CAMSEdit::Behavior::_ShouldEnter(TCHAR c) const
{
return m_pEdit->ShouldEnter(c);
}
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::AlphanumericBehavior
// Constructs the object using the given set of strInvalidChars
CAMSEdit::AlphanumericBehavior::AlphanumericBehavior(CAMSEdit* pEdit, int nMaxChars /*= 0*/, const CString& strInvalidChars /*= _T("%'*\"+?><:\\")*/) :
Behavior(pEdit),
m_nMaxChars(nMaxChars),
m_strInvalidChars(strInvalidChars)
{
ASSERT(m_nMaxChars >= 0);
}
// Sets the characters to be considered invalid for text input.
void CAMSEdit::AlphanumericBehavior::SetInvalidCharacters(const CString& strInvalidChars)
{
if (m_strInvalidChars == strInvalidChars)
return;
m_strInvalidChars = strInvalidChars;
_Redraw();
}
// Returns the characters considered invalid for text input.
const CString& CAMSEdit::AlphanumericBehavior::GetInvalidCharacters() const
{
return m_strInvalidChars;
}
// Sets the maximum number of characters to allow for input.
void CAMSEdit::AlphanumericBehavior::SetMaxCharacters(int nMaxChars)
{
if (m_nMaxChars == nMaxChars)
return;
m_nMaxChars = nMaxChars;
_Redraw();
}
// Returns the characters considered invalid for input.
int CAMSEdit::AlphanumericBehavior::GetMaxCharacters() const
{
return m_nMaxChars;
}
// Returns the control's value in a valid format.
CString CAMSEdit::AlphanumericBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
CString strNewText = strText.Left(m_nMaxChars ? m_nMaxChars : strText.GetLength());
// Remove any invalid characters from the control's text
for (int iPos = strNewText.GetLength() - 1; iPos >= 0; iPos--)
{
if (m_strInvalidChars.Find(strNewText[iPos]) >= 0)
strNewText = strNewText.Left(iPos) + strNewText.Mid(iPos + 1);
}
return strNewText;
}
// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::AlphanumericBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;
if (!m_strInvalidChars.IsEmpty())
{
// Check if the character is invalid
if (m_strInvalidChars.Find((TCHAR)uChar) >= 0)
{
MessageBeep(MB_ICONEXCLAMATION);
return;
}
}
TCHAR c = static_cast<TCHAR>(uChar);
// If the number of characters is already at Max, overwrite
CString strText = m_pEdit->GetText();
if (strText.GetLength() == m_nMaxChars && m_nMaxChars && _istprint(c))
{
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);
if (nStart < m_nMaxChars && _ShouldEnter(c))
{
m_pEdit->SetSel(nStart, nStart + 1);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}
if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
}
#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::MaskedBehavior
// Constructs the object using the given mask.
CAMSEdit::MaskedBehavior::MaskedBehavior(CAMSEdit* pEdit, const CString& strMask /*= _T("")*/) :
Behavior(pEdit),
m_strMask(strMask)
{
m_arraySymbols.Add(Symbol('#', _istdigit)); // default mask symbol
}
// Returns the mask
const CString& CAMSEdit::MaskedBehavior::GetMask() const
{
return m_strMask;
}
// Sets the mask and redraws the control to accomodate it
void CAMSEdit::MaskedBehavior::SetMask(const CString& strMask)
{
if (m_strMask == strMask)
return;
m_strMask = strMask;
_Redraw();
}
// Returns the numeric portion of the control's value as a string
CString CAMSEdit::MaskedBehavior::GetNumericText() const
{
CString strText = m_pEdit->GetText();
CString strResult;
for (int iPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];
if (_istdigit(c))
strResult += c;
}
return strResult;
}
// Returns the control's value in a valid format.
CString CAMSEdit::MaskedBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
int nMaskLen = m_strMask.GetLength();
// If the mask is empty, allow anything
if (!nMaskLen)
return strText;
CString strNewText;
// Accomodate the text to the mask as much as possible
for (int iPos = 0, iMaskPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++, iMaskPos++)
{
TCHAR c = strText[iPos];
TCHAR cMask = static_cast<TCHAR>(iMaskPos < nMaskLen ? m_strMask[iMaskPos] : 0);
// If we've reached the end of the mask, break
if (!cMask)
break;
// Match the character to any of the symbols
for (int iSymbol = 0, nSymbolCount = m_arraySymbols.GetSize(); iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];
// Find the symbol that applies for the given character
if (cMask != symbol || !symbol.Validate(c))
continue;
// Try to add matching characters in the mask until a different symbol is reached
for (; iMaskPos < nMaskLen; iMaskPos++)
{
cMask = m_strMask[iMaskPos];
if (cMask == symbol)
{
strNewText += symbol.Convert(c);
break;
}
else
{
for (int iSymbol2 = 0; iSymbol2 < nSymbolCount; iSymbol2++)
{
if (cMask == m_arraySymbols[iSymbol2])
{
strNewText += symbol.Convert(c);
break;
}
}
if (iSymbol2 < nSymbolCount)
break;
strNewText += cMask;
}
}
break;
}
// If the character was not matched to a symbol, stop
if (iSymbol == nSymbolCount)
{
if (c == cMask)
{
// Match the character to any of the symbols
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
{
if (cMask == m_arraySymbols[iSymbol])
break;
}
if (iSymbol == nSymbolCount)
{
strNewText += c;
continue;
}
}
break;
}
}
return strNewText;
}
// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::MaskedBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;
TCHAR c = static_cast<TCHAR>(uChar);
// If the mask is empty, allow anything
int nMaskLen = m_strMask.GetLength();
if (!nMaskLen)
{
if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
return;
}
// Check that we haven't gone past the mask's length
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);
if (nStart >= nMaskLen && c != VK_BACK)
return;
CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();
// Check for a non-printable character (such as Ctrl+C)
if (!_istprint(c))
{
if (c == VK_BACK && nStart != nLen)
{
m_pEdit->SendMessage(WM_KEYDOWN, VK_LEFT); // move the cursor left
return;
}
// Allow backspace only if the cursor is all the way to the right
if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
return;
}
TCHAR cMask = m_strMask[nStart];
// Check if the mask's character matches with any of the symbols in the array.
for (int iSymbol = 0, nSymbolCount = m_arraySymbols.GetSize(); iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];
if (cMask == symbol)
{
if (symbol.Validate(c) && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(symbol.Convert(c)), TRUE);
}
return;
}
}
// Check if it's the same character as the mask.
if (cMask == c && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
return;
}
// Concatenate all the mask symbols
CString strSymbols;
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
strSymbols += m_arraySymbols[iSymbol];
// If it's a valid character, find the next symbol on the mask and add any non-mask characters in between.
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];
// See if the character is valid for any other symbols
if (!symbol.Validate(c))
continue;
// Find the position of the next symbol
CString strMaskPortion = m_strMask.Mid(nStart);
int nMaskPos = strMaskPortion.FindOneOf(strSymbols);
// Enter the character if there isn't another symbol before it
if (nMaskPos >= 0 && strMaskPortion[nMaskPos] == symbol && _ShouldEnter(c))
{
m_pEdit->SetSel(nStart, nStart + nMaskPos);
m_pEdit->ReplaceSel(strMaskPortion.Left(nMaskPos), TRUE);
_OnChar(uChar, nRepCnt, nFlags);
return;
}
}
}
// Handles the WM_KEYDOWN message, which is called when the user enters a special character such as Delete or the arrow keys.
void CAMSEdit::MaskedBehavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
switch (uChar)
{
case VK_DELETE:
{
// If deleting make sure it's the last character or that
// the selection goes all the way to the end of the text
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);
CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();
if (nEnd != nLen)
{
if (!(nEnd == nStart && nEnd == nLen - 1))
return;
}
break;
}
}
Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);
}
// Returns a reference to the array of symbols that may be found on the mask.
// This allows adding, editing, or deleting symbols for the mask.
CAMSEdit::MaskedBehavior::SymbolArray& CAMSEdit::MaskedBehavior::GetSymbolArray()
{
return m_arraySymbols;
}
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::MaskedBehavior::Symbol
// Constructs the object -- needed for CArray
CAMSEdit::MaskedBehavior::Symbol::Symbol() :
m_cSymbol(0),
m_fnValidation(NULL),
m_fnConversion(NULL)
{
}
// Constructs the object with the given character and set of functions
CAMSEdit::MaskedBehavior::Symbol::Symbol(TCHAR cSymbol, ValidationFunction fnValidation, ConversionFunction fnConversion /*= NULL*/) :
m_cSymbol(cSymbol),
m_fnValidation(fnValidation),
m_fnConversion(fnConversion)
{
}
// Destroys the object (virtual).
CAMSEdit::MaskedBehavior::Symbol::~Symbol()
{
}
// Returns true if the given character (usually just entered by the user) is a match for self's symbol.
// This is tested by passing it to the validation function passed in the constructor (if valid).
bool CAMSEdit::MaskedBehavior::Symbol::Validate(TCHAR c) const
{
if (m_fnValidation)
return (m_fnValidation(c) != 0);
return true;
}
// Returns the given character converted as a result of calling the conversion function was passed in the constructor.
// If no conversion function was passed, the character is returned intact.
TCHAR CAMSEdit::MaskedBehavior::Symbol::Convert(TCHAR c) const
{
if (m_fnConversion)
return (TCHAR)m_fnConversion(c);
return c;
}
// Sets the character for the symbol to be used in the mask.
void CAMSEdit::MaskedBehavior::Symbol::Set(TCHAR cSymbol)
{
m_cSymbol = cSymbol;
}
// Returns the character for the symbol to be used in the mask.
TCHAR CAMSEdit::MaskedBehavior::Symbol::Get() const
{
return m_cSymbol;
}
// Returns the character for the symbol to be used in the mask.
CAMSEdit::MaskedBehavior::Symbol::operator TCHAR() const
{
return m_cSymbol;
}
#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)
#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_NUMERIC_CLASS)
/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::NumericBehavior
// Constructs the object using the given nMaxWholeDigits and nMaxDecimalPlaces.
CAMSEdit::NumericBehavior::NumericBehavior(CAMSEdit* pEdit, int nMaxWholeDigits /*= 9*/, int nMaxDecimalPlaces /*= 4*/) :
Behavior(pEdit),
m_nMaxWholeDigits(nMaxWholeDigits >= 0 ? nMaxWholeDigits : -nMaxWholeDigits),
m_nMaxDecimalPlaces(nMaxDecimalPlaces),
m_cNegativeSign('-'),
m_cDecimalPoint('.'),
m_cGroupSeparator(','),
m_nDigitsInGroup(0),
m_dMin(AMS_MIN_NUMBER),
m_dMax(AMS_MAX_NUMBER),
m_bAdjustingSeparators(false)
{
ASSERT(m_nMaxWholeDigits > 0); // must have at least 1 digit to the left of the decimal
ASSERT(m_nMaxDecimalPlaces >= 0); // decimal places must be positive
if (nMaxWholeDigits < 0)
m_uFlags |= CannotBeNegative;
// Get the system's negative sign
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SNEGATIVESIGN, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SNEGATIVESIGN, &m_cNegativeSign, sizeof(m_cNegativeSign));
// Get the system's decimal point
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, &m_cDecimalPoint, sizeof(m_cDecimalPoint));
// Get the system's group separator
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, &m_cGroupSeparator, sizeof(m_cGroupSeparator));
// Ensure the separators are not the same
if (m_cDecimalPoint == m_cGroupSeparator)
m_cGroupSeparator = (m_cDecimalPoint == ',' ? '.' : ',');
}
// Returns decimal point separator plus group separator and negative sign characters if pointers are passed
TCHAR CAMSEdit::NumericBehavior::GetLocaleCharacters(PTCHAR pcGroupSeparator /*= NULL*/, PTCHAR pcNegativeSign /*= NULL*/) // (Added) Carlo
{
TCHAR cDecimalPoint;
struct lconv *plc = localeconv();
cDecimalPoint = '.'; // Preinitialize
if (*plc->decimal_point ) // If value present
cDecimalPoint = *plc->decimal_point ;
if (pcGroupSeparator)
{ // If pointer passed
*pcGroupSeparator = ','; // Preinitialize
if (*plc->thousands_sep) // If value present
*pcGroupSeparator = *plc->thousands_sep;
}
if (pcNegativeSign)
{ // If pointer passed
*pcNegativeSign = '-'; // Preinitialize
if (*plc->negative_sign) // If value present
*pcNegativeSign = *plc->negative_sign;
}
return cDecimalPoint;
}
// Sets the maximum number of digits before the decimal point.
// If nMaxWholeDigits is negative, then negative numbers will NOT be allowed.
void CAMSEdit::NumericBehavior::SetMaxWholeDigits(int nMaxWholeDigits)
{
ASSERT(nMaxWholeDigits);
// If nMaxWholeDigits is negative, don't allow negatives
bool bAllowNegative = (nMaxWholeDigits >= 0);
if (nMaxWholeDigits < 0)
nMaxWholeDigits = -nMaxWholeDigits;
if (m_nMaxWholeDigits == nMaxWholeDigits && IsNegativeAllowed() == bAllowNegative)
return;
m_nMaxWholeDigits = nMaxWholeDigits;
if (!ModifyFlags(bAllowNegative ? 0 : CannotBeNegative, bAllowNegative ? CannotBeNegative : 0))
_Redraw();
}
// Returns the maximum number of digits before the decimal point.
int CAMSEdit::NumericBehavior::GetMaxWholeDigits() const
{
return m_nMaxWholeDigits;
}
// Sets the maximum number of digits after the decimal point.
void CAMSEdit::NumericBehavior::SetMaxDecimalPlaces(int nMaxDecimalPlaces)
{
ASSERT(nMaxDecimalPlaces >= 0);
if (m_nMaxDecimalPlaces == nMaxDecimalPlaces)
return;
m_nMaxDecimalPlaces = nMaxDecimalPlaces;
_Redraw();
}
// Returns the maximum number of digits after the decimal point.
int CAMSEdit::NumericBehavior::GetMaxDecimalPlaces() const
{
return m_nMaxDecimalPlaces;
}
// Sets whether the negative sign is allowed in the number or not.
void CAMSEdit::NumericBehavior::AllowNegative(bool bAllowNegative /*= true*/)
{
ModifyFlags(bAllowNegative ? 0 : CannotBeNegative, bAllowNegative ? CannotBeNegative : 0);
}
// Returns true if the negative sign is allowed in the number.
bool CAMSEdit::NumericBehavior::IsNegativeAllowed() const
{
return !(m_uFlags & CannotBeNegative);
}
// Sets the number of digits to be grouped together (if any).
void CAMSEdit::NumericBehavior::SetDigitsInGroup(int nDigitsInGroup)
{
ASSERT(nDigitsInGroup >= 0);
if (m_nDigitsInGroup == nDigitsInGroup)
return;
m_nDigitsInGroup = nDigitsInGroup;
_Redraw();
}
// Returns the number of digits to be grouped together (if any).
int CAMSEdit::NumericBehavior::GetDigitsInGroup() const
{
return m_nDigitsInGroup;
}
// Sets the character to use for the decimal point and the group separator (thousands)
void CAMSEdit::NumericBehavior::SetSeparators(TCHAR cDecimal, TCHAR cGroup)
{
ASSERT(cDecimal);
ASSERT(cGroup);
// If nothing's changing, leave
if (m_cDecimalPoint == cDecimal && m_cGroupSeparator == cGroup)
return;
TCHAR cOldChars[2] = {m_cDecimalPoint, m_cGroupSeparator}; // Chars to search for // (Added) Carlo
CString sText = m_pEdit->GetText(); // Save old text
// Set them
m_cDecimalPoint = cDecimal;
m_cGroupSeparator = cGroup;
// Ensure they're not the same
if (m_cDecimalPoint == m_cGroupSeparator)
m_cGroupSeparator = (m_cDecimalPoint == ',' ? '.' : ',');
TCHAR cNewChars[2] = {m_cDecimalPoint, m_cGroupSeparator}; // Chars to replace with // (Added) Carlo
ASSERT(ELEMENTS_OF(cOldChars) == ELEMENTS_OF(cNewChars));
sText = CAMSEdit::MultiCharReplace(sText, ELEMENTS_OF(cOldChars), cOldChars, cNewChars); // Replace characters // (Added) Carlo
m_pEdit->SetText(sText); // Change text with new separators
_Redraw();
}
// Retrieves the character being used for the decimal point and group separator (thousands).
void CAMSEdit::NumericBehavior::GetSeparators(TCHAR* pcDecimal, TCHAR* pcGroup) const
{
if (pcDecimal)
*pcDecimal = m_cDecimalPoint;
if (pcGroup)
*pcGroup = m_cGroupSeparator;
}
// Sets the text to be automatically inserted in front of the number (such as a currency sign).
void CAMSEdit::NumericBehavior::SetPrefix(const CString& strPrefix)
{
if (m_strPrefix == strPrefix)
return;
m_strPrefix = strPrefix;
_Redraw();
}
// Returns the text to be automatically inserted in front of the number (such as a currency sign).
const CString& CAMSEdit::NumericBehavior::GetPrefix() const
{
return m_strPrefix;
}
// Parses the given strMask to set the control's configuration.
void CAMSEdit::NumericBehavior::SetMask(const CString& strMask)
{
int nDecimalPos = -1;
int nLen = strMask.GetLength();
m_nMaxWholeDigits = 0;
m_nMaxDecimalPlaces = 0;
m_nDigitsInGroup = 0;
m_uFlags = (m_uFlags & ~CannotBeNegative); // allow it to be negative
m_strPrefix = _T("");
for (int iPos = nLen - 1; iPos >= 0; iPos--)
{
TCHAR c = strMask[iPos];
if (c == '#')
{
if (nDecimalPos >= 0)
m_nMaxWholeDigits++;
else
m_nMaxDecimalPlaces++;
}
else if ((c == '.' || c == m_cDecimalPoint) && nDecimalPos < 0)
{
nDecimalPos = iPos;
m_cDecimalPoint = c;
}
else if (c == ',' || c == m_cGroupSeparator)
{
if (!m_nDigitsInGroup)
{
m_nDigitsInGroup = (((nDecimalPos >= 0) ? nDecimalPos : nLen) - iPos) - 1;
m_cGroupSeparator = c;
}
}
else
{
m_strPrefix = strMask.Left(iPos + 1);
break;
}
}
if (nDecimalPos < 0)
{
m_nMaxWholeDigits = m_nMaxDecimalPlaces;
m_nMaxDecimalPlaces = 0;
}
ASSERT(m_nMaxWholeDigits > 0); // must have at least one digit on left side of decimal point
_Redraw();
}
// Gets the mask corresponding to the maximum number than can be entered into the control
CString CAMSEdit::NumericBehavior::GetMask() const
{
CString strMask;
for (int iDigit = 0; iDigit < m_nMaxWholeDigits; iDigit++)
strMask += '0';
if (m_nMaxDecimalPlaces)
strMask += m_cDecimalPoint;
for (iDigit = 0; iDigit < m_nMaxDecimalPlaces; iDigit++)
strMask += '0';
strMask = GetSeparatedText(strMask);
for (int iPos = 0, nLen = strMask.GetLength(); iPos < nLen; iPos++)
{
if (strMask[iPos] == '0')
strMask.SetAt(iPos, '#');
}
return strMask;
}
// Sets the range of allowed values to the given minimum and maximum double values.
void CAMSEdit::NumericBehavior::SetRange(double dMin, double dMax)
{
ASSERT(dMin <= dMax);
m_dMin = dMin;
m_dMax = dMax;
_Redraw();
}
// Retrieves the range of allowed values inside the given set of double pointers.
void CAMSEdit::NumericBehavior::GetRange(double* pdMin, double* pdMax) const
{
if (pdMin)
*pdMin = m_dMin;
if (pdMax)
*pdMax = m_dMax;
}
// Returns the number of group separator characters in the given strText.
int CAMSEdit::NumericBehavior::GetGroupSeparatorCount(const CString& strText) const
{
for (int iPos = 0, nSepCount = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
if (strText[iPos] == m_cGroupSeparator)
nSepCount++;
}
return nSepCount;
}
// Returns the given strText as a numeric string.
CString CAMSEdit::NumericBehavior::GetNumericText(const CString& strText, bool bUseMathSymbols /*= false*/) const
{
CString strNewText;
bool bIsNegative = false;
bool bHasDecimalPoint = false;
TCHAR cNegativeSign; // (Added) Carlo
TCHAR cDecimalPoint = GetLocaleCharacters(NULL, &cNegativeSign); // (Added) Carlo
for (int iPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];
if (_istdigit(c))
strNewText += c;
else if (c == m_cNegativeSign)
bIsNegative = true;
else if (c == m_cDecimalPoint && !bHasDecimalPoint)
{
bHasDecimalPoint = true;
strNewText += (bUseMathSymbols ? cDecimalPoint /*'.'*/ : m_cDecimalPoint); // (Changed) Carlo
}
}
// Add the negative sign to the front of the number
if (bIsNegative)
strNewText.Insert(0, bUseMathSymbols ? cNegativeSign /*'-'*/ : m_cNegativeSign); // (Changed) Carlo
return strNewText;
}
// Returns the current double as a text value.
// If bTrimTrailingZeros is true, any insignificant zeros after the decimal point are removed.
CString CAMSEdit::NumericBehavior::GetDoubleText(double dText, bool bTrimTrailingZeros /*= true*/) const
{
CString strText;
TCHAR cDecimalPoint = GetLocaleCharacters(); // (Added) Carlo
strText.Format(_T("%lf"), dText);
strText.Replace(cDecimalPoint /*_T('.')*/, m_cDecimalPoint); // (Changed) Carlo
if (bTrimTrailingZeros)
{
strText.TrimRight('0');
strText.TrimRight(m_cDecimalPoint);
}
return strText;
}
// Sets the control's text to the given double value.
// If bTrimTrailingZeros is true, any insignificant zeros after the decimal point are removed.
void CAMSEdit::NumericBehavior::SetDouble(double dText, bool bTrimTrailingZeros /*= true*/)
{
m_pEdit->SetWindowText(GetDoubleText(dText, bTrimTrailingZeros));
}
// Returns the current text as a double value.
double CAMSEdit::NumericBehavior::GetDouble() const
{
return _tcstod(GetNumericText(m_pEdit->GetText(), true), NULL); // (Changed) Carlo
}
// Sets the control's text to the given integer value.
void CAMSEdit::NumericBehavior::SetInt(int nText)
{
CString strText;
strText.Format(_T("%d"), nText);
m_pEdit->SetWindowText(strText);
}
// Returns the current text as an integer value.
int CAMSEdit::NumericBehavior::GetInt() const
{
return _ttoi(GetNumericText(m_pEdit->GetText(), true)); // (Changed) Carlo
}
// Adjusts the location of separators based on the nCurrentSeparatorCount.
void CAMSEdit::NumericBehavior::AdjustSeparators(int nCurrentSeparatorCount)
{
SelectionSaver selection = m_pEdit;
m_bAdjustingSeparators = true;
CString strText = _GetValidText();
if (strText != m_pEdit->GetText())
m_pEdit->SetWindowText(strText);
// Adjust the current selection if separators were added/removed
int nNewSeparatorCount = GetGroupSeparatorCount(strText);
if (nCurrentSeparatorCount != nNewSeparatorCount && selection.GetStart() > m_strPrefix.GetLength())
selection += (nNewSeparatorCount - nCurrentSeparatorCount);
m_bAdjustingSeparators = false;
}
// Returns the given text with the group separator characters inserted in the proper places.
CString CAMSEdit::NumericBehavior::GetSeparatedText(const CString& strText) const
{
CString strNumericText = GetNumericText(strText);
CString strNewText = strNumericText;
// Retrieve the number without the decimal point
int nDecimalPos = strNumericText.Find(m_cDecimalPoint);
if (nDecimalPos >= 0)
strNewText = strNewText.Left(nDecimalPos);
if (m_nDigitsInGroup > 0)
{
int nLen = strNewText.GetLength();
BOOL bIsNegative = (!strNewText.IsEmpty() && strNewText[0] == m_cNegativeSign);
// Loop in reverse and stick the separator every m_nDigitsInGroup digits.
for (int iPos = nLen - (m_nDigitsInGroup + 1); iPos >= bIsNegative; iPos -= m_nDigitsInGroup)
strNewText = strNewText.Left(iPos + 1) + m_cGroupSeparator + strNewText.Mid(iPos + 1);
}
// Prepend the prefix, if the number is not empty.
if (!strNewText.IsEmpty() || nDecimalPos >= 0)
{
strNewText = m_strPrefix + strNewText;
if (nDecimalPos >= 0)
strNewText += strNumericText.Mid(nDecimalPos);
}
return strNewText;
}
// Inserts nCount zeros into the given string at the given position.
// If nPos is less than 0, the zeros are appended.
void CAMSEdit::NumericBehavior::InsertZeros(CString* pStrText, int nPos, int nCount)
{
ASSERT(pStrText);
if (nPos < 0 && nCount > 0)
nPos = pStrText->GetLength();
for (int iZero = 0; iZero < nCount; iZero++)
pStrText->Insert(nPos, '0');
}
// Returns the control's value in a valid format.
CString CAMSEdit::NumericBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
CString strNewText;
bool bIsNegative = false;
// Remove any invalid characters from the number
for (int iPos = 0, nDecimalPos = -1, nNewLen = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];
// Check for a negative sign
if (c == m_cNegativeSign && IsNegativeAllowed())
bIsNegative = true;
// Check for a digit
else if (_istdigit(c))
{
// Make sure it doesn't go beyond the limits
if (nDecimalPos < 0 && nNewLen == m_nMaxWholeDigits)
continue;
if (nDecimalPos >= 0 && nNewLen > nDecimalPos + m_nMaxDecimalPlaces)
break;
strNewText += c;
nNewLen++;
}
// Check for a decimal point
else if (c == m_cDecimalPoint && nDecimalPos < 0)
{
if (m_nMaxDecimalPlaces == 0)
break;
strNewText += c;
nDecimalPos = nNewLen;
nNewLen++;
}
}
// Check if we need to pad the number with zeros after the decimal point
if (m_nMaxDecimalPlaces > 0 && nNewLen > 0 &&
((m_uFlags & PadWithZerosAfterDecimalWhenTextChanges) ||
(!m_bAdjustingSeparators && (m_uFlags & PadWithZerosAfterDecimalWhenTextIsSet))))
{
if (nDecimalPos < 0)
{
if (nNewLen == 0 || strNewText == /*'-'*/m_cNegativeSign) // (Changed) Carlo
{
strNewText = '0';
nNewLen = 1;
}
strNewText += m_cDecimalPoint;
nDecimalPos = nNewLen++;
}
InsertZeros(&strNewText, -1, m_nMaxDecimalPlaces - (nNewLen - nDecimalPos - 1));
}
// Insert the negative sign if it's there
if (bIsNegative)
strNewText.Insert(0, m_cNegativeSign);
return GetSeparatedText(strNewText);
}
// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::NumericBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;
TCHAR c = static_cast<TCHAR>(uChar);
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);
CString strText = m_pEdit->GetText();
CString strNumericText = GetNumericText(strText);
int nDecimalPos = strText.Find(m_cDecimalPoint);
int nNumericDecimalPos = strNumericText.Find(m_cDecimalPoint);
int nLen = strText.GetLength();
int nNumericLen = strNumericText.GetLength();
int nPrefixLen = m_strPrefix.GetLength();
int nSepCount = GetGroupSeparatorCount(strText);
bool bNeedAdjustment = false;
// Check if we're in the prefix's location
if (nStart < nPrefixLen && _istprint(c))
{
TCHAR cPrefix = m_strPrefix[nStart];
// Check if it's the same character as the prefix.
if (cPrefix == c && _ShouldEnter(c))
{
if (nLen > nStart)
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
else
Behavior::_OnChar(uChar, nRepCnt, nFlags);
}
// If it's a part of the number, enter the prefix
else if ((_istdigit(c) || c == m_cNegativeSign || c == m_cDecimalPoint) && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nPrefixLen));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(m_strPrefix.Mid(nStart), TRUE);
NumericBehavior::_OnChar(uChar, nRepCnt, nFlags);
}
return;
}
// Check if it's a negative sign
if (c == m_cNegativeSign && IsNegativeAllowed())
{
// If it's at the beginning, determine if it should overwritten
if (nStart == nPrefixLen)
{
if (!strNumericText.IsEmpty() && strNumericText[0] == m_cNegativeSign && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(m_cNegativeSign), TRUE);
return;
}
}
// If we're not at the beginning, toggle the sign
else if (_ShouldEnter(c))
{
if (strNumericText[0] == m_cNegativeSign)
{
m_pEdit->SetSel(nPrefixLen, nPrefixLen + 1);
m_pEdit->ReplaceSel(_T(""), TRUE);
m_pEdit->SetSel(nStart - 1, nEnd - 1);
}
else
{
m_pEdit->SetSel(nPrefixLen, nPrefixLen);
m_pEdit->ReplaceSel(CString(m_cNegativeSign), TRUE);
m_pEdit->SetSel(nStart + 1, nEnd + 1);
}
return;
}
}
// Check if it's a decimal point (only one is allowed).
else if (c == m_cDecimalPoint && m_nMaxDecimalPlaces > 0)
{
if (nDecimalPos >= 0)
{
// Check if we're replacing the decimal point
if (nDecimalPos >= nStart && nDecimalPos < nEnd)
bNeedAdjustment = true;
else
{ // Otherwise, put the caret on it
if (_ShouldEnter(c))
m_pEdit->SetSel(nDecimalPos + 1, nDecimalPos + 1);
return;
}
}
else
bNeedAdjustment = true;
}
// Check if it's a digit
else if (_istdigit(c))
{
// Check if we're on the right of the decimal point.
if (nDecimalPos >= 0 && nDecimalPos < nStart)
{
if (strNumericText.Mid(nNumericDecimalPos + 1).GetLength() == m_nMaxDecimalPlaces)
{
if (nStart <= nDecimalPos + m_nMaxDecimalPlaces && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}
}
// We're on the left side of the decimal point
else
{
bool bIsNegative = (!strNumericText.IsEmpty() && strNumericText[0] == m_cNegativeSign);
// Make sure we can still enter digits.
if (nStart == m_nMaxWholeDigits + bIsNegative + nSepCount + nPrefixLen)
{
if (m_uFlags & AddDecimalAfterMaxWholeDigits && m_nMaxDecimalPlaces > 0)
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 2));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(m_cDecimalPoint) + c, TRUE);
}
return;
}
if (strNumericText.Mid(0, nNumericDecimalPos >= 0 ? nNumericDecimalPos : nNumericLen).GetLength() == m_nMaxWholeDigits + bIsNegative)
{
if (_ShouldEnter(c))
{
if (strText[nStart] == m_cGroupSeparator)
nStart++;
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}
bNeedAdjustment = true;
}
}
// Check if it's a non-printable character, such as Backspace or Ctrl+C
else if (!_istprint(c))
bNeedAdjustment = true;
else
return;
// Check if the character should be entered
if (!_ShouldEnter(c))
return;
Behavior::_OnChar(uChar, nRepCnt, nFlags);
// If the decimal point was added/removed or a separator needs adding/removing, adjust the text
if (bNeedAdjustment)
AdjustSeparators(nSepCount);
}
// Handles the WM_KEYDOWN message, which is called when the user enters a special character such as Delete or the arrow keys.
void CAMSEdit::NumericBehavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
switch (uChar)
{
case VK_DELETE:
{
int nStart = 0, nEnd = 0;
m_pEdit->GetSel(nStart, nEnd);
CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();
// If deleting the prefix, don't allow it if there's a number after it.
int nPrefixLen = m_strPrefix.GetLength();
if (nStart < nPrefixLen && nLen > nPrefixLen)
{
if (nEnd == nLen)
break;
return;
}
if (nStart < nLen && strText[nStart] == m_cGroupSeparator && nStart == nEnd)
Behavior::_OnKeyDown(VK_RIGHT, nRepCnt, nFlags);
// Allow the deletion and then adjust the value
int nSepCount = GetGroupSeparatorCount(strText);
Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);
AdjustSeparators(nSepCount);
// If everything on the right was deleted, put the selection on the right
if (nEnd == nLen)
m_pEdit->SetSel(nStart, nStart);
return;
}
}
Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);
}
// Handles the WM_KILLFOCUS message, which is called when the user leaves the control.
// It's used here to check if zeros need to be added to the value.
void CAMSEdit::NumericBehavior::_OnKillFocus(CWnd* pNewWnd)
{
Behavior::_OnKillFocus(pNewWnd);
// Check if the value is empty and we don't want to touch it
CString strOriginalText = GetNumericText(m_pEdit->GetText());
CString strText = strOriginalText;
int nLen = strText.GetLength();
// If desired, remove any extra leading zeros but always leave one in front of the decimal point
if (m_uFlags & OnKillFocus_RemoveExtraLeadingZeros && nLen > 0)
{
bool bIsNegative = (strText[0] == m_cNegativeSign);
if (bIsNegative)
strText.Delete(0);
strText.TrimLeft('0');
if (strText.IsEmpty() || strText[0] == m_cDecimalPoint)
strText.Insert(0, '0');
if (bIsNegative)
strText.Insert(0, m_cNegativeSign);
}
else if (!(m_uFlags & OnKillFocus_Max) || (nLen == 0 && m_uFlags & OnKillFocus_DontPadWithZerosIfEmpty))
return;
int nDecimalPos = strText.Find(m_cDecimalPoint);
// Check if we need to pad the number with zeros after the decimal point
if (m_uFlags & OnKillFocus_PadWithZerosAfterDecimal && m_nMaxDecimalPlaces > 0)
{
if (nDecimalPos < 0)
{
if (nLen == 0 || strText == m_cNegativeSign /*'-'*/) // (Changed) Carlo
{
strText = '0';
nLen = 1;
}
strText += m_cDecimalPoint;
nDecimalPos = nLen++;
}
InsertZeros(&strText, -1, m_nMaxDecimalPlaces - (nLen - nDecimalPos - 1));
}
// Check if we need to pad the number with zeros before the decimal point
if (m_uFlags & OnKillFocus_PadWithZerosBeforeDecimal && m_nMaxWholeDigits > 0)
{
if (nDecimalPos < 0)
nDecimalPos = nLen;
if (nLen && strText[0] == m_cNegativeSign /*'-'*/) // (Changed) Carlo
nDecimalPos--;
InsertZeros(&strText, (nLen ? strText[0] == m_cNegativeSign /*'-'*/ : -1), m_nMaxWholeDigits - nDecimalPos); // (Changed) Carlo
}
if (strText != strOriginalText)
{
.
.
.
(follows untouched source code)
|
|
|
|
|
I wanted a DateTime ActiveX control?
How can I use your CEdit-derived classes?
|
|
|
|
|
You must create an ActiveX control using MFC Wizard, optionally with ATL (I prefer) and put a wrapper around the CEdit-derived class. Surely, this is not a speedy work, but you can do with patience and knowledge.
Regards,
Carlo
|
|
|
|
|
Thanks Carlo. I started to reply to this message yesterday but couldn't find the words to say it as well as you did.
It can be done, it won't be necessarily simple (depending on your expertise), but it will work!
Regards,
Alvaro
Give a man a fish, he owes you one fish. Teach a man to fish, you give up your monopoly on fisheries.
|
|
|
|
|
I have not done this before.
Can you give me an example or a code fragment about this?
Thank you!
|
|
|
|
|
Hello,
If I put this controls in a MFC extension DLL I don't receive anymore OnChange event in the main program and the data isn't updated anymore. For example, when I use a CAMSNumericEdit derived control in a dialog (and CAMSNumericEdit is in a DLL), the program never gets into OnChange method of the main program and if the OnChar event gets a comma char (,) the control will not show this char.
How can I solve this?
|
|
|
|
|
Adrian,
I just tried doing what you said: I put the files inside an MFC extension DLL and exported by first adding this to the top of amsEdit.h:
#ifdef _AMS_EXPORT
#define AMS_EXPORT _declspec(dllexport)
#else
#define AMS_EXPORT _declspec(dllimport)
#endif
Then I went to every class declaration and added AMS_EXPORT in front of the name:
class AMS_EXPORT CAMSEdit : public CEdit
{
...
}
Of course, I changed the project settings and added _AMS_EXPORT to the defines.
I then created a small dialog based app and used my controls in there importing them from the DLL. It all worked just fine!
I don't understand what could be occurring in your case. Could you provide more information? I used VC6.0 BTW.
Regards,
Alvaro
Give a man a fish, he owes you one fish. Teach a man to fish, you give up your monopoly on fisheries.
|
|
|
|
|
Great tool!
But I am getting a compiler error with compile set to Warning Level 4 (Visual C++ version 6.0). I don't want to change my warning level - how do I cast this to eliminate the warning?
------------------
CAMSEdit::MaskedBehavior::MaskedBehavior(CAMSEdit* pEdit, const CString& strMask /*= _T("")*/) :
Behavior(pEdit),
m_strMask(strMask)
{
error-> m_arraySymbols.Add(Symbol('#', _istdigit)); // default mask symbol
}
------------------
amsEdit.cpp(488) : error C2665: 'Symbol::Symbol' : none of the 3 overloads can convert parameter 2 from type 'int (int)'
|
|
|
|
|
With Visual C++ version 6.0 SP5 the warning is different:
amsEdit.cpp(488) : warning C4239: nonstandard extension used : 'argument' : conversion from 'class CAMSEdit::MaskedBehavior::Symbol' to 'class CAMSEdit::MaskedBehavior::Symbol &'
A reference that is not to 'const' cannot be bound to a non-lvalue
To eliminate warning edit MaskedBehavior class in amsEdit.h and change:
typedef CArray<Symbol, Symbol&> SymbolArray;
to
typedef CArray<Symbol, Symbol const&> SymbolArray;
The warning will go away.
|
|
|
|
|
Thanks but that change wasn't enough.
Because in the statement
m_arraySymbols.Add(Symbol('#', _istdigit));
_istdigit is defined as
#define _istdigit isdigit
and isdigit is defined as
_CRTIMP int __cdecl isdigit(int);
so I had to change the statement
typedef int (*ValidationFunction)(UINT);
to
typedef int (*ValidationFunction)(INT);
My question now is, will everything still function correctly? I assume that it will but I would still like to check.
Also, I got another 4 or 5 other compiler warning messages that were trivial to fix.
Thanks,
Jim
|
|
|
|
|
That error depends on whether the MBCS version of istdigit is used or not.
If I compile the non-unicode version of the sample, the compiler use the:
_ismbcdigit(unsigned int);
version; may be that you have a setting that forces compiler to use the SBCS version:
isdigit(int);
As you, I assume that all still function correctly or not more worse than before, since internally the code use extensively TCHARs.
Regards,
Carlo
|
|
|
|
|
Before all, thanks for this product! Is is very good.
I got a bug using Currency edit in Europe where currency symbols are exchanged with respect to Americans settings. Final result gets multiplied by number of decimal places after decimal point, because 'NumericBehavior::_OnKillFocus()' internally uses hardcoded characters to make his work. I resolved the problem with following changes:
change:
CString strOriginalText = GetNumericText(m_pEdit->GetText(), true);
to:
CString strOriginalText = GetNumericText(m_pEdit->GetText());
change:
int nDecimalPos = strText.Find('.');
to:
int nDecimalPos = strText.Find(m_cDecimalPoint);
change:
strText += '.';
to:
strText += m_cDecimalPoint;
Regards.
Carlo Marsura
|
|
|
|
|
Thanks Carlo, your fixes will appear on the next version, which I'm about to submit to Code Project.
Regards,
Alvaro
Give a man a fish, he owes you one fish. Teach a man to fish, you give up your monopoly on fisheries.
|
|
|
|
|
First of all: Great Work!!!
I did however locate a bug that appears when using CAMSNumericEdit and Doubles on a system when the GetLocaleInfo(...) initializes m_cDecimalPoint with a _T(','). It seems like CString::Format(...) does not do the same initialization (at least on my system, and potentially on somebody elses) as CAMSNumericEdit, and adds a _T('.').
This could potentially be a problem if the SW is built and tested in the US/UK, and subsequently used in continental Europe.
It can however, easily be fixed by:
CString CAMSEdit::NumericBehavior::GetDoubleText(...) const
{
CString strText;
strText.Format(_T("%lf"), dText);
if (m_cDecimalPoint != _T('.'))
strText.Replace(_T('.'), m_cDecimalPoint);
if (bTrimTrailingZeros)
{
strText.TrimRight('0');
strText.TrimRight(m_cDecimalPoint);
}
return strText;
}
//JNKS
|
|
|
|
|
Thanks for the suggestion JNKS.
I'm going to include your changes in the next version, which I'm about to release. But I wanted to let you know that this problem occurs because the locale is not properly set when CString::Format gets called.
If you look at the help for setlocale , you'll see that at the beginning of the program Microsoft calls it and passes it "C", the traditional locale for C programs. I don't understand their decision (probably for backward compatibility), but it's the cause of the problem. It basically ignores the user's regional settings!
The solution is to call setlocale yourself (at the beginning of the program) and pass it an empty string:
_tsetlocale(LC_ALL, _T(""));
This does the trick and makes your changes unnecessary. I'm still going to include them though, since it's harmless and it will work for those people who forget to call setlocale.
Regards,
Alvaro
Give a man a fish, he owes you one fish. Teach a man to fish, you give up your monopoly on fisheries.
|
|
|
|
|
I don't understand well, how currency edit working was planning. For what comma ',' is used? It's a thousand divider only? And how about fraction part of value? And the edit doesn't work if I put '$' into it
Ken Keray
|
|
|
|
|
Ken,
I quite don't understand what it is you don't understand.
The comma may be used as a thousand's separator or as a decimal separator. It's based on your regional settings, but you can change it to anything you want.
If there's a bug or problem you're facing, please let me know the steps you took to reproduce it so I can look into it.
Regards,
Alvaro
Can I ask you a question?
|
|
|
|
|
OK.
In 1-st I've try to enter dot as decimal separator. The edit has ignored this trying.
After that I've try to enter '$' and the edit has ignored it again!
I can't reproduce input in the currency edit like you did it on the article's screenshot.
Ken Keray
|
|
|
|
|
Where are you located? If you're in Russia (like your profile says), your Regional Settings will be different from mine (USA). I believe the Russian monetary format looks like this:
123 456 789,00р.
So no $ sign, and a comma for the decimal separator. You see, by default, these controls work with your Regional Settings. Change your settings to United States (or something similar) and run the demo again. It should work as in the screenshot.
If you want it to always look like in the screenshot, without worrying about the Regional Settings, you can use SetSeparator('.', ',') and SetPrefix("$") methods, as shown in the article.
Regards,
Alvaro
Can I ask you a question?
|
|
|
|
|
First of all, thank for your work Alvaro, this control help me and my work a lot!
I've found a bug in all the various CAMSxxx classes: if you write some text in the edit, select it (or part of it) and press Shift+Delete, the application crash, reporting (in some case) a Stack Overflow in MFC. I've tried with my applications and with your demo, same results.
Regards,
Matteo
|
|
|
|
|
I found the problem.
If i press Shift+Delete, this message is at the end processed by CAMSEdit::Behavior::_OnKeyDown, which call CWnd::OnKeyDown, which process the default behaviour of Shift+Delete, the Cut Operation calling CAMSEdit::OnCut, that copy the selection in the clipboard and send a WM_KEYDOWN message, with WPARAM = VK_DELETE, processed by CAMSEdit::Behavior::_OnKeyDown, so there's a loop.
I solved the problem replacing
SendMessage(WM_KEYDOWN, VK_DELETE);
with
ReplaceSel(_T(""));
in LONG CAMSEdit::OnCut(UINT, LONG).
I hope this help.
Regards,
Matteo
|
|
|
|
|
Thanks Matteo. That's a nasty one.
I've fixed it like you suggested and tomorrow I'll submit an update that will include it.
Thanks again,
Alvaro
Can I ask you a question?
|
|
|
|
|
|
Thanks Jeremy.
It feels great to benefit fellow developers, especially when doing so is also fun.
Regards,
Alvaro
Hey! It compiles! Ship it.
|
|
|
|
|