Calculate ellipsized width exactly.
Width calculation using partial extents is just an inaccurate estimate: partial extents have sub-pixel precision and are rounded by GetPartialTextExtents(). Use partial extents to estimate string width and only verify it with GetTextExtent() when it looks good. git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@66873 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
parent
508fc76d0c
commit
38ddd614f2
@ -236,6 +236,135 @@ wxControlBase::GetCompositeControlsDefaultAttributes(wxWindowVariant WXUNUSED(va
|
||||
|
||||
#define wxELLIPSE_REPLACEMENT wxS("...")
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct EllipsizeCalculator
|
||||
{
|
||||
EllipsizeCalculator(const wxString& s, const wxDC& dc,
|
||||
int maxFinalWidthPx, int replacementWidthPx)
|
||||
:
|
||||
m_initialCharToRemove(0),
|
||||
m_nCharsToRemove(0),
|
||||
m_outputNeedsUpdate(true),
|
||||
m_str(s),
|
||||
m_dc(dc),
|
||||
m_maxFinalWidthPx(maxFinalWidthPx),
|
||||
m_replacementWidthPx(replacementWidthPx)
|
||||
{
|
||||
m_isOk = dc.GetPartialTextExtents(s, m_charOffsetsPx);
|
||||
wxASSERT( m_charOffsetsPx.GetCount() == s.length() );
|
||||
}
|
||||
|
||||
bool IsOk() const { return m_isOk; }
|
||||
|
||||
bool EllipsizationNotNeeded() const
|
||||
{
|
||||
// NOTE: charOffsetsPx[n] is the width in pixels of the first n characters (with the last one INCLUDED)
|
||||
// thus charOffsetsPx[len-1] is the total width of the string
|
||||
return m_charOffsetsPx.Last() <= m_maxFinalWidthPx;
|
||||
}
|
||||
|
||||
void Init(size_t initialCharToRemove, size_t nCharsToRemove)
|
||||
{
|
||||
m_initialCharToRemove = initialCharToRemove;
|
||||
m_nCharsToRemove = nCharsToRemove;
|
||||
}
|
||||
|
||||
void RemoveFromEnd()
|
||||
{
|
||||
m_nCharsToRemove++;
|
||||
}
|
||||
|
||||
void RemoveFromStart()
|
||||
{
|
||||
m_initialCharToRemove--;
|
||||
m_nCharsToRemove++;
|
||||
}
|
||||
|
||||
size_t GetFirstRemoved() const { return m_initialCharToRemove; }
|
||||
size_t GetLastRemoved() const { return m_initialCharToRemove + m_nCharsToRemove - 1; }
|
||||
|
||||
const wxString& GetEllipsizedText()
|
||||
{
|
||||
if ( m_outputNeedsUpdate )
|
||||
{
|
||||
wxASSERT(m_initialCharToRemove <= m_str.length() - 1); // see valid range for initialCharToRemove above
|
||||
wxASSERT(m_nCharsToRemove >= 1 && m_nCharsToRemove <= m_str.length() - m_initialCharToRemove); // see valid range for nCharsToRemove above
|
||||
|
||||
// erase m_nCharsToRemove characters after m_initialCharToRemove (included);
|
||||
// e.g. if we have the string "foobar" (len = 6)
|
||||
// ^
|
||||
// \--- m_initialCharToRemove = 2
|
||||
// and m_nCharsToRemove = 2, then we get "foar"
|
||||
m_output = m_str;
|
||||
m_output.replace(m_initialCharToRemove, m_nCharsToRemove, wxELLIPSE_REPLACEMENT);
|
||||
}
|
||||
|
||||
return m_output;
|
||||
}
|
||||
|
||||
bool IsShortEnough()
|
||||
{
|
||||
if ( m_nCharsToRemove == m_str.length() )
|
||||
return true; // that's the best we could do
|
||||
|
||||
// Width calculation using partial extents is just an inaccurate
|
||||
// estimate: partial extents have sub-pixel precision and are rounded
|
||||
// by GetPartialTextExtents(); replacing part of the string with "..."
|
||||
// may change them too thanks to changes in ligatures, kerning etc.
|
||||
//
|
||||
// The correct algorithm would be to call GetTextExtent() in every step
|
||||
// of ellipsization, but that would be too expensive, especially when
|
||||
// the difference is just a few pixels. So we use partial extents to
|
||||
// estimate string width and only verify it with GetTextExtent() when
|
||||
// it looks good.
|
||||
|
||||
int estimatedWidth = m_replacementWidthPx; // length of "..."
|
||||
|
||||
// length of text before the removed part:
|
||||
if ( m_initialCharToRemove > 0 )
|
||||
estimatedWidth += m_charOffsetsPx[m_initialCharToRemove - 1];
|
||||
|
||||
// length of text after the removed part:
|
||||
|
||||
if ( GetLastRemoved() < m_str.length() )
|
||||
estimatedWidth += m_charOffsetsPx.Last() - m_charOffsetsPx[GetLastRemoved()];
|
||||
|
||||
if ( estimatedWidth > m_maxFinalWidthPx )
|
||||
return false;
|
||||
|
||||
return m_dc.GetTextExtent(GetEllipsizedText()).GetWidth() <= m_maxFinalWidthPx;
|
||||
}
|
||||
|
||||
// calculation state:
|
||||
|
||||
// REMEMBER: indexes inside the string have a valid range of [0;len-1] if not otherwise constrained
|
||||
// lengths/counts of characters (e.g. nCharsToRemove) have a
|
||||
// valid range of [0;len] if not otherwise constrained
|
||||
// NOTE: since this point we know we have for sure a non-empty string from which we need
|
||||
// to remove _at least_ one character (thus nCharsToRemove below is constrained to be >= 1)
|
||||
|
||||
// index of first character to erase, valid range is [0;len-1]:
|
||||
size_t m_initialCharToRemove;
|
||||
// how many chars do we need to erase? valid range is [0;len-m_initialCharToRemove]
|
||||
size_t m_nCharsToRemove;
|
||||
|
||||
wxString m_output;
|
||||
bool m_outputNeedsUpdate;
|
||||
|
||||
// inputs:
|
||||
wxString m_str;
|
||||
const wxDC& m_dc;
|
||||
int m_maxFinalWidthPx;
|
||||
int m_replacementWidthPx;
|
||||
wxArrayInt m_charOffsetsPx;
|
||||
|
||||
bool m_isOk;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
/* static and protected */
|
||||
wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxDC& dc,
|
||||
wxEllipsizeMode mode, int maxFinalWidthPx,
|
||||
@ -253,43 +382,28 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
|
||||
if (maxFinalWidthPx <= 0)
|
||||
return wxEmptyString;
|
||||
|
||||
wxArrayInt charOffsetsPx;
|
||||
size_t len = curLine.length();
|
||||
if (len <= 1 ||
|
||||
!dc.GetPartialTextExtents(curLine, charOffsetsPx))
|
||||
if (len <= 1 )
|
||||
return curLine;
|
||||
|
||||
wxASSERT(charOffsetsPx.GetCount() == len);
|
||||
EllipsizeCalculator calc(curLine, dc, maxFinalWidthPx, replacementWidthPx);
|
||||
|
||||
// NOTE: charOffsetsPx[n] is the width in pixels of the first n characters (with the last one INCLUDED)
|
||||
// thus charOffsetsPx[len-1] is the total width of the string
|
||||
size_t totalWidthPx = charOffsetsPx.Last();
|
||||
if ( totalWidthPx <= (size_t)maxFinalWidthPx )
|
||||
return curLine; // we don't need to do any ellipsization!
|
||||
if ( !calc.IsOk() )
|
||||
return curLine;
|
||||
|
||||
int excessPx = wxMin(totalWidthPx - maxFinalWidthPx +
|
||||
replacementWidthPx,
|
||||
totalWidthPx);
|
||||
wxASSERT(excessPx>0); // excessPx should be in the [1;totalWidthPx] range
|
||||
|
||||
// REMEMBER: indexes inside the string have a valid range of [0;len-1] if not otherwise constrained
|
||||
// lengths/counts of characters (e.g. nCharsToRemove) have a valid range of [0;len] if not otherwise constrained
|
||||
// NOTE: since this point we know we have for sure a non-empty string from which we need
|
||||
// to remove _at least_ one character (thus nCharsToRemove below is constrained to be >= 1)
|
||||
|
||||
size_t initialCharToRemove, // index of first character to erase, valid range is [0;len-1]
|
||||
nCharsToRemove; // how many chars do we need to erase? valid range is [1;len-initialCharToRemove]
|
||||
if ( calc.EllipsizationNotNeeded() )
|
||||
return curLine;
|
||||
|
||||
// let's compute the range of characters to remove depending on the ellipsization mode:
|
||||
switch (mode)
|
||||
{
|
||||
case wxELLIPSIZE_START:
|
||||
initialCharToRemove = 0;
|
||||
for ( nCharsToRemove = 1;
|
||||
nCharsToRemove < len && charOffsetsPx[nCharsToRemove-1] < excessPx;
|
||||
nCharsToRemove++ )
|
||||
;
|
||||
break;
|
||||
{
|
||||
calc.Init(0, 1);
|
||||
while ( !calc.IsShortEnough() )
|
||||
calc.RemoveFromEnd();
|
||||
break;
|
||||
}
|
||||
|
||||
case wxELLIPSIZE_MIDDLE:
|
||||
{
|
||||
@ -301,15 +415,15 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
|
||||
// - the second one to remove, valid range [initialCharToRemove;endCharToRemove]
|
||||
// - the third one to preserve, valid range [endCharToRemove+1;len-1] or the empty range if endCharToRemove==len-1
|
||||
// NOTE: empty range != range [0;0] since the range [0;0] contains 1 character (the zero-th one)!
|
||||
initialCharToRemove = len/2; // index of the last character to remove; valid range is [0;len-1]
|
||||
size_t endCharToRemove = initialCharToRemove - 1; // initial removal range is empty
|
||||
|
||||
int removedPx = 0;
|
||||
calc.Init(len/2, 0);
|
||||
|
||||
bool removeFromStart = true;
|
||||
for ( ; removedPx < excessPx; )
|
||||
|
||||
while ( !calc.IsShortEnough() )
|
||||
{
|
||||
const bool canRemoveFromStart = initialCharToRemove > 0;
|
||||
const bool canRemoveFromEnd = endCharToRemove < len - 1;
|
||||
const bool canRemoveFromStart = calc.GetFirstRemoved() > 0;
|
||||
const bool canRemoveFromEnd = calc.GetLastRemoved() < len - 1;
|
||||
|
||||
if ( !canRemoveFromStart && !canRemoveFromEnd )
|
||||
{
|
||||
@ -326,59 +440,20 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
|
||||
removeFromStart = true;
|
||||
|
||||
if ( removeFromStart )
|
||||
{
|
||||
// try to remove the last character of the first part of the string
|
||||
|
||||
// width of the (initialCharToRemove-1)-th character
|
||||
int widthPx;
|
||||
if (initialCharToRemove >= 2)
|
||||
widthPx = charOffsetsPx[initialCharToRemove-1] - charOffsetsPx[initialCharToRemove-2];
|
||||
else
|
||||
widthPx = charOffsetsPx[initialCharToRemove-1];
|
||||
// the (initialCharToRemove-1)-th character is the first char of the string
|
||||
|
||||
wxASSERT(widthPx >= 0); // widthPx is zero for e.g. tab characters
|
||||
|
||||
// mark the (initialCharToRemove-1)-th character as removable
|
||||
initialCharToRemove--;
|
||||
removedPx += widthPx;
|
||||
|
||||
continue; // don't remove anything else
|
||||
}
|
||||
else // !removeFromStart
|
||||
{
|
||||
// try to remove the first character of the last part of the string
|
||||
|
||||
// width of the (endCharToRemove+1)-th character
|
||||
int widthPx = charOffsetsPx[endCharToRemove+1] -
|
||||
charOffsetsPx[endCharToRemove];
|
||||
|
||||
wxASSERT(widthPx >= 0); // widthPx is zero for e.g. tab characters
|
||||
|
||||
// mark the (endCharToRemove+1)-th character as removable
|
||||
endCharToRemove++;
|
||||
removedPx += widthPx;
|
||||
|
||||
continue; // don't remove anything else
|
||||
}
|
||||
calc.RemoveFromStart();
|
||||
else
|
||||
calc.RemoveFromEnd();
|
||||
}
|
||||
|
||||
nCharsToRemove = endCharToRemove - initialCharToRemove + 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case wxELLIPSIZE_END:
|
||||
{
|
||||
int maxWidthPx = totalWidthPx - excessPx;
|
||||
|
||||
// go backward from the end of the string toward the start
|
||||
for ( initialCharToRemove = len-1;
|
||||
initialCharToRemove > 0 && charOffsetsPx[initialCharToRemove-1] > maxWidthPx;
|
||||
initialCharToRemove-- )
|
||||
;
|
||||
nCharsToRemove = len - initialCharToRemove;
|
||||
calc.Init(len - 1, 1);
|
||||
while ( !calc.IsShortEnough() )
|
||||
calc.RemoveFromStart();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case wxELLIPSIZE_NONE:
|
||||
default:
|
||||
@ -386,20 +461,7 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
|
||||
return curLine;
|
||||
}
|
||||
|
||||
wxASSERT(initialCharToRemove <= len-1); // see valid range for initialCharToRemove above
|
||||
wxASSERT(nCharsToRemove >= 1 && nCharsToRemove <= len-initialCharToRemove); // see valid range for nCharsToRemove above
|
||||
|
||||
// erase nCharsToRemove characters after initialCharToRemove (included);
|
||||
// e.g. if we have the string "foobar" (len = 6)
|
||||
// ^
|
||||
// \--- initialCharToRemove = 2
|
||||
// and nCharsToRemove = 2, then we get "foar"
|
||||
wxString ret(curLine);
|
||||
ret.erase(initialCharToRemove, nCharsToRemove);
|
||||
|
||||
ret.insert(initialCharToRemove, wxELLIPSE_REPLACEMENT);
|
||||
|
||||
return ret;
|
||||
return calc.GetEllipsizedText();
|
||||
}
|
||||
|
||||
/* static */
|
||||
|
Loading…
Reference in New Issue
Block a user