2074 lines
72 KiB
C++
2074 lines
72 KiB
C++
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
|
|
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
|
|
// PARTICULAR PURPOSE.
|
|
//
|
|
// Copyright © Microsoft Corporation. All rights reserved
|
|
|
|
/******************************************************************************
|
|
* DictationRun.cpp
|
|
* This module contains the implementation details of the CDictationRun
|
|
* class which handles the dictation-specfic issues for a dictation
|
|
* text run.
|
|
******************************************************************************/
|
|
|
|
#include "stdafx.h"
|
|
#include "DictationRun.h"
|
|
|
|
/******************************************************************************
|
|
* CDictationRun::CDictationRun *
|
|
*------------------------------*
|
|
* Description:
|
|
* Constructor.
|
|
*
|
|
* Return:
|
|
* <none>
|
|
******************************************************************************/
|
|
CDictationRun::CDictationRun() : m_ulStartElement( 0 ),
|
|
m_cElements( 0 ),
|
|
m_fAltsGotten( false ),
|
|
m_pPhraseReplacement( NULL ),
|
|
m_pulElementOffsets( NULL ),
|
|
m_pResultContainer( NULL )
|
|
{} /* CDictationRun::CDictationRun */
|
|
|
|
/******************************************************************************
|
|
* CDictationRun::~CDictationRun *
|
|
*-------------------------------*
|
|
* Description:
|
|
* Destructor.
|
|
*
|
|
* Return:
|
|
* <none>
|
|
******************************************************************************/
|
|
CDictationRun::~CDictationRun( void )
|
|
{
|
|
// See CDictationRun::SetTextRange(), where this is allocated
|
|
delete[] m_pulElementOffsets;
|
|
|
|
// Tell the result object (which might be shared with other DictationRuns)
|
|
// that we no longer need it.
|
|
if ( m_pResultContainer )
|
|
{
|
|
m_pResultContainer->DeleteOwner( *this );
|
|
}
|
|
|
|
// The CPhraseReplacement object is going to need to stay around
|
|
// for as long as the CResultContainer does; thus when all
|
|
// CDictationRuns depending on this result object are deleted,
|
|
// the associated CPhraseReplacement object will also be deleted.
|
|
|
|
} /* CDictationRun::~CDictationRun */
|
|
|
|
/******************************************************************************
|
|
* CDictationRun::Initialize *
|
|
*---------------------------*
|
|
* Description:
|
|
* Constructor.
|
|
* This is the initialization routine that is called when a DictationRun
|
|
* is being created from scratch, either from a serialized DictationRun
|
|
* (if the optional param pDictHdr is NULL) or from newly-dictated text.
|
|
* Initializes the phrase element information and sets up the
|
|
* phrase data.
|
|
*
|
|
* Return:
|
|
* S_OK
|
|
* E_OUTOFMEMORY
|
|
* Return value of CPhraseReplacement::Initialize()
|
|
******************************************************************************/
|
|
HRESULT CDictationRun::Initialize( ISpRecoResult &rRecoResult, DICTHEADER *pDictHdr )
|
|
{
|
|
// Get the phrase element information from the serialized header, if available
|
|
if ( pDictHdr )
|
|
{
|
|
m_ulStartElement = pDictHdr->ulStartElement;
|
|
m_cElements = pDictHdr->cElements;
|
|
}
|
|
|
|
// If this is a new DictationRun (not recreated from a serialized DictationRun),
|
|
// the element count will get properly initialized
|
|
// when the text range is set.
|
|
|
|
// Create and initialize the phrase replacement info
|
|
m_pPhraseReplacement = new CPhraseReplacement();
|
|
if ( !m_pPhraseReplacement )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
HRESULT hr = m_pPhraseReplacement->Initialize( rRecoResult );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Create a CResultContainer to hold the result object
|
|
m_pResultContainer = new CResultContainer( rRecoResult, *this,
|
|
*m_pPhraseReplacement );
|
|
if ( !m_pResultContainer )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
return S_OK;
|
|
|
|
} /* CDictationRun::Initialize */
|
|
|
|
/******************************************************************************
|
|
* CDictationRun::Initialize *
|
|
*---------------------------*
|
|
* Description:
|
|
* Constructor.
|
|
* This is the initialization routine that is called when a DictationRun
|
|
* is being created from the contents of another DictationRun
|
|
* (i.e. a DictationRun is being split).
|
|
* All of the phrase information is already contained in the
|
|
* CResultContainer.
|
|
*
|
|
* Return:
|
|
* S_OK
|
|
******************************************************************************/
|
|
HRESULT CDictationRun::Initialize( CResultContainer &rResultContainer )
|
|
{
|
|
// Get the phrase element and result object information from the
|
|
// CResultContainer
|
|
m_pResultContainer = &rResultContainer;
|
|
m_pPhraseReplacement = rResultContainer.GetPhraseReplacement();
|
|
|
|
// The caller needs to call rResultContainer.AddOwner().
|
|
|
|
return S_OK;
|
|
} /* CDictationRun::Initialize */
|
|
|
|
/******************************************************************************
|
|
* CDictationRun::SetTextRange *
|
|
*-----------------------------*
|
|
* Description:
|
|
* Stores the ITextRange * for this run.
|
|
* Creates the array of element offsets and initializes
|
|
* the entries.
|
|
* The two cases in which this function can be called are
|
|
* * New DictationRun, in which presumably all of
|
|
* the elements will be found
|
|
* * A DictationRun formed by being split off an old
|
|
* DictationRun, in which case not all of the elements
|
|
* will be present
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_OUTOFMEMORY
|
|
* Return value of ITextRange::FindTextStart()
|
|
******************************************************************************/
|
|
HRESULT CDictationRun::SetTextRange( ITextRange *pTextRange )
|
|
{
|
|
if ( !pTextRange )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
m_cpTextRange = pTextRange;
|
|
|
|
if ( !m_pPhraseReplacement || !m_pResultContainer )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// If we do not already know how many phrase elements we have, then
|
|
// this is a new run, so get it from the phrase replacement object
|
|
if ( !m_cElements )
|
|
{
|
|
m_cElements = m_pPhraseReplacement->GetNumReplacementElements();
|
|
}
|
|
|
|
// Allocate enough space in our element offsets to hold information
|
|
// for all the elements.
|
|
// Each entry in this array will indicate where the given element
|
|
// starts (given as an offset from the start of the range)
|
|
m_pulElementOffsets =
|
|
new ULONG[ m_pPhraseReplacement->GetNumReplacementElements() ];
|
|
if ( !m_pulElementOffsets )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Fill the array with an impossible value, right now the length of
|
|
// this range (since nothing can have an offset tha big)
|
|
const ULONG ulBogusVal = GetEnd() - GetStart();
|
|
ULONG ulElement;
|
|
for ( ulElement = 0;
|
|
ulElement < m_pPhraseReplacement->GetNumReplacementElements();
|
|
ulElement++ )
|
|
{
|
|
m_pulElementOffsets[ ulElement ] = ulBogusVal;
|
|
}
|
|
|
|
// Loop through the elements finding the character offset in the range
|
|
// for each one, if it is there.
|
|
// We will use a duplicate ITextRange and ITextRange::FindTextStart()
|
|
BSTR bstrElement;
|
|
long lStart = GetStart();
|
|
long lElementStart;
|
|
CComPtr<ITextRange> cpRangeDup;
|
|
HRESULT hr = pTextRange->GetDuplicate( &cpRangeDup );
|
|
if ( !cpRangeDup )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
for ( ulElement = 0;
|
|
SUCCEEDED( hr ) &&
|
|
( ulElement < m_pPhraseReplacement->GetNumReplacementElements() );
|
|
ulElement++ )
|
|
{
|
|
// Get the element text
|
|
BYTE bDisplayAttributes;
|
|
bstrElement = ::SysAllocString(
|
|
m_pPhraseReplacement->GetDisplayText( ulElement, &bDisplayAttributes ) );
|
|
if ( !bstrElement )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Look for the element.
|
|
long lLength;
|
|
hr = cpRangeDup->FindTextStart( bstrElement, 0, 0, &lLength );
|
|
|
|
::SysFreeString( bstrElement );
|
|
|
|
if ( lLength )
|
|
{
|
|
// The element was found in cpRangeDup
|
|
|
|
// The start of the cpRangeDup is now the start of the element
|
|
cpRangeDup->GetStart( &lElementStart );
|
|
|
|
// Give the offset relative to the start of the range
|
|
m_pulElementOffsets[ulElement] = lElementStart - lStart;
|
|
|
|
// Advance the start past this word
|
|
cpRangeDup->MoveStart( tomCharacter, lLength, NULL );
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
} /* CDictationRun::SetTextRange */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::Split *
|
|
*----------------------*
|
|
* Description:
|
|
* Splits up a DictationRun so that this text run now ends at
|
|
* lFirstEnd and a second dictation run (*ppTextRun) begins at
|
|
* lSecondBegin.
|
|
* "This" will now be a shorter range (it will end sooner),
|
|
* and *ppTextRun will point to the new text run for which
|
|
* space will be allocated here.
|
|
* In general, the phrase element information for the two
|
|
* DictationRuns will NOT be correct after exiting this
|
|
* function. The caller must call CorrectPhraseElementsAndRange
|
|
* to ensure that the phrase element information correctly reflects
|
|
* the new range, which it in general will not after this function.
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_INVALIDARG: *plFirstEnd, *plSecondBegin out of bounds or
|
|
* in the wrong order
|
|
* E_OUTOFMEMORY
|
|
* Return value of CDictationRun::Initialize()
|
|
* Return value of ITextDocument::Range()
|
|
* Return value of CResultContainer::AddOwner()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::Split( long *plFirstEnd,
|
|
long *plSecondBegin,
|
|
ITextDocument *cpTextDoc,
|
|
CTextRun **ppTextRun )
|
|
{
|
|
if ( !plFirstEnd || !plSecondBegin || !cpTextDoc || !ppTextRun )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
if ( !m_cpTextRange || !m_pResultContainer )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
long lFirstEnd = *plFirstEnd;
|
|
long lSecondBegin = *plSecondBegin;
|
|
|
|
if ( !WithinRange( lFirstEnd ) || (lFirstEnd > lSecondBegin) )
|
|
{
|
|
// Cannot split somewhere that is not in the range
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
if ( (GetStart() == lSecondBegin) || (GetEnd() == lFirstEnd) ||
|
|
(lSecondBegin > GetEnd()) )
|
|
{
|
|
// Don't need to do anything, since we are trying to split on a
|
|
// run boundary
|
|
*ppTextRun = NULL;
|
|
return S_OK;
|
|
}
|
|
|
|
// Create a new dictation run to hold the text from lSecondBegin until
|
|
// the end of the original range.
|
|
// We create this new DictationRun off the same result object
|
|
// in m_pResultContainer
|
|
*ppTextRun = new CDictationRun();
|
|
CDictationRun *pDictRun = (CDictationRun *) *ppTextRun;
|
|
if ( !pDictRun )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
HRESULT hr = pDictRun->Initialize( *m_pResultContainer );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// This DictationRun should be on the owner list for this result container
|
|
hr = m_pResultContainer->AddOwner( *pDictRun );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Adjust the latter range so that it starts at lSecondBegin
|
|
// and ends where "this" used to end
|
|
long lEnd = GetEnd();
|
|
CComPtr<ITextRange> cpLatterRange;
|
|
hr = cpTextDoc->Range( lSecondBegin, lEnd, &cpLatterRange );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
hr = pDictRun->SetTextRange( cpLatterRange );
|
|
|
|
// Adjust the range of "this" so that it ends at lFirstEnd
|
|
m_cpTextRange->SetEnd( lFirstEnd );
|
|
|
|
// Just copy over the phrase element information, which will
|
|
// in general be incorrect until we call CorrectPhraseEltsAndRange()
|
|
pDictRun->m_ulStartElement = m_ulStartElement;
|
|
pDictRun->m_cElements = m_cElements;
|
|
|
|
return hr;
|
|
|
|
} /* CDictationRun::Split */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::Concatenate *
|
|
*----------------------------*
|
|
* Description:
|
|
* If bConcatenateAfter is true, pTextRun's range is appended
|
|
* to "this"'s range; else it is prepended.
|
|
* Concatenation happens according to the following rules:
|
|
* * If pTextRun is a DictationRun that refers to the same
|
|
* result object as "this" and its phrase elements adjoin
|
|
* ours, then concatenation is possible.
|
|
* * If pTextRun is a plain TextRun, then we try
|
|
* concatenating the whole thing on and seeing if
|
|
* some of that text can be appropriated as phrase elements
|
|
* Return:
|
|
* E_NOMERGE if no merging was possible
|
|
* E_PARTIALMERGE if some but not all of pTextRun's range
|
|
* could be appropriated by this
|
|
* E_FULLMERGE if all of pTextRun's range was appropriated
|
|
* E_LESSTEXT if by joining pText onto "this", we reduce
|
|
* the number of phrase elements that "this" has
|
|
* (e.g. pTextRun's text runs onto the end of "this"'s
|
|
* last phrase element, thus making that last word no
|
|
* longer a true phrase element)
|
|
***********************************************************************/
|
|
MERGERESULT CDictationRun::Concatenate( CTextRun *pTextRun,
|
|
bool fConcatAfter )
|
|
{
|
|
// Validate params
|
|
if ( !pTextRun || !m_cpTextRange || !m_pResultContainer )
|
|
{
|
|
_ASSERTE( false );
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
// Check that the runs are indeed adjacent in the text
|
|
if (( fConcatAfter && ( GetEnd() != pTextRun->GetStart() ) ) ||
|
|
( !fConcatAfter && ( GetStart() != pTextRun->GetEnd() ) ))
|
|
{
|
|
// Non-consecutive runs can't be concatenated
|
|
_ASSERTE( false );
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
if ( pTextRun->IsDict() )
|
|
{
|
|
// pTextRun is a dictation run.
|
|
// This merge is easy to do: Check that the two DictationRuns
|
|
// share the same result object, then check if e.g. the
|
|
// first one has elements 2 through 4 and the second one
|
|
// has elements 5 through 8.
|
|
|
|
// We know this is a DictationRun so do the cast
|
|
CDictationRun *pDictRun = (CDictationRun *) pTextRun;
|
|
|
|
// Check to see if the result objects are the same (if they
|
|
// are then they'll point to the same CResultContainer
|
|
if ( pDictRun->m_pResultContainer != m_pResultContainer )
|
|
{
|
|
// Dictation runs with different result objects
|
|
// cannot be concatenated
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
// See if phrase elements are consecutive
|
|
if ( fConcatAfter )
|
|
{
|
|
// pDictRun's elements need to follow "this"'s
|
|
|
|
if ( (m_ulStartElement + m_cElements) ==
|
|
pDictRun->m_ulStartElement )
|
|
{
|
|
// These can be merged, since pDictRun picks
|
|
// up where we leave off
|
|
|
|
// Store the start indices so that we can update
|
|
// the element offsets
|
|
long lThisStart = GetStart();
|
|
long lDictRunStart = pDictRun->GetStart();
|
|
|
|
// Adjust the ranges so that "this" contains
|
|
// all of pDictRun's text and so that
|
|
// pDictRun will be degenerate
|
|
SetEnd( pDictRun->GetEnd() );
|
|
pDictRun->SetStart( pDictRun->GetEnd() );
|
|
|
|
// Update the element offsets for the new elements introduced by
|
|
// pDictRun.
|
|
// We will need to update every element accounted for
|
|
// by pDictRun to be relative to the start of this run rather
|
|
// than the start of pDictRun
|
|
for ( ULONG ulElement = pDictRun->m_ulStartElement;
|
|
ulElement < (pDictRun->m_ulStartElement + pDictRun->m_cElements);
|
|
ulElement++ )
|
|
{
|
|
// Shift the offset of the new element.
|
|
// Add lDictRunStart to get an absolute offset (within the document),
|
|
// then subtract lThisStart for an offset relative to "this"
|
|
m_pulElementOffsets[ulElement] =
|
|
pDictRun->m_pulElementOffsets[ulElement]
|
|
+ lDictRunStart - lThisStart;
|
|
}
|
|
|
|
// Adjust the phrase element info so that "this" contains all of
|
|
// pDictRun's phrase elements
|
|
m_cElements += pDictRun->m_cElements;
|
|
|
|
// pDictRun should now contain no elements
|
|
pDictRun->m_ulStartElement = m_ulStartElement + m_cElements;
|
|
pDictRun->m_cElements = 0;
|
|
|
|
return E_FULLMERGE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Same as above, just in the opposite direction.
|
|
// Here pDictRun is being prepended to this run.
|
|
|
|
if ( m_ulStartElement ==
|
|
(pDictRun->m_ulStartElement + pDictRun->m_cElements) )
|
|
{
|
|
// Store the start indices so that we can update
|
|
// the element offsets
|
|
long lThisStart = GetStart();
|
|
long lDictRunStart = pDictRun->GetStart();
|
|
|
|
// Adjust the ranges so that "this" contains
|
|
// all of pDictRun's text and so that
|
|
// pDictRun will be degenerate
|
|
SetStart( pDictRun->GetStart() );
|
|
pDictRun->SetEnd( pDictRun->GetStart() );
|
|
|
|
// Update the element offsets for the new elements introduced by
|
|
// pDictRun.
|
|
// The offsets in pDictRun are correct, since they are relative
|
|
// to the earlier start.
|
|
// The offsets for "this" need to be updated
|
|
ULONG ulElement;
|
|
for ( ulElement = pDictRun->m_ulStartElement;
|
|
ulElement <
|
|
(pDictRun->m_ulStartElement + pDictRun->m_cElements);
|
|
ulElement++ )
|
|
{
|
|
// Copy over element offsets from pDictRun
|
|
m_pulElementOffsets[ulElement] =
|
|
pDictRun->m_pulElementOffsets[ulElement];
|
|
}
|
|
|
|
for ( ulElement = m_ulStartElement;
|
|
ulElement < (m_ulStartElement + m_cElements);
|
|
ulElement++ )
|
|
{
|
|
// Shift the offset of the element.
|
|
// Add lThisStart to get an absolute offset (within the document),
|
|
// then subtract lDictRunStart (the new start of the text range)
|
|
// for an offset relative to the new start
|
|
m_pulElementOffsets[ulElement] +=
|
|
lThisStart - lDictRunStart;
|
|
}
|
|
|
|
// Adjust the phrase element info so that "this" contains all of
|
|
// pDictRun's phrase elements
|
|
m_cElements += pDictRun->m_cElements;
|
|
m_ulStartElement = pDictRun->m_ulStartElement;
|
|
|
|
// pDictRun should now contain no elements
|
|
pDictRun->m_cElements = 0;
|
|
|
|
return E_FULLMERGE;
|
|
}
|
|
}
|
|
|
|
// If we got here, then both runs are CDictationRuns pointing to the same
|
|
// CResultContainer but they did not contain adjacent elements
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
|
|
// Merging a text run onto "this"
|
|
|
|
// To do this, we first "guess" that the entire text run should go
|
|
// with "this". CorrectPhraseEltsAndRange() corrects this assumption
|
|
// if the entire phrase cannot in fact be used.
|
|
|
|
if ( fConcatAfter )
|
|
{
|
|
// Add on all of the text in pTextRun to the end of "this"
|
|
|
|
// For determining how much of a merge will have taken place
|
|
long lOldBorder = pTextRun->GetStart();
|
|
|
|
// Temporarily set the range of "this" to the end of pTextRun,
|
|
// thus incorporating all of pTextRun's text into "this"
|
|
SetEnd( pTextRun->GetEnd() );
|
|
|
|
// Cut out whatever is not a phrase element.
|
|
// This may push back the end of "this"'s range
|
|
bool fCorrectionResult;
|
|
HRESULT hr = CorrectPhraseEltsAndRange( true, &fCorrectionResult );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
// Now have pTextRun pick up where "this" leaves off,
|
|
// so that it includes everything that could not be
|
|
// merged into "this"
|
|
pTextRun->SetStart( GetEnd() );
|
|
|
|
if ( fCorrectionResult )
|
|
{
|
|
// The entire range of pTextRun was appropriated by this
|
|
_ASSERTE( pTextRun->IsDegenerate() );
|
|
return E_FULLMERGE;
|
|
}
|
|
else
|
|
{
|
|
if ( pTextRun->GetStart() == lOldBorder )
|
|
{
|
|
// None of pTextRun's range was appropriated
|
|
return E_NOMERGE;
|
|
}
|
|
else
|
|
{
|
|
// If pTextRun starts earlier than it did before,
|
|
// then concatenating pTextRun onto "this"
|
|
// reduced the number of phrase elements it
|
|
// contained.
|
|
// If pTextRun starts later than it did before,
|
|
// then "this" appropriated some part of it as
|
|
// phrase elements.
|
|
return (pTextRun->GetStart() < lOldBorder) ?
|
|
E_LESSTEXT : E_PARTIALMERGE;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Same as above, only in the reverse direction (try to merge
|
|
// pTextRun onto the beginning of "this"
|
|
|
|
// For determining how much of a merge will have taken place
|
|
long lOldBorder = pTextRun->GetEnd();
|
|
|
|
// Temporarily set the range of "this" to include pTextRun,
|
|
// thus incorporating all of pTextRun's text into "this"
|
|
SetStart( pTextRun->GetStart() );
|
|
|
|
// Cut out whatever is not a phrase element.
|
|
// This may push forward the start of "this"'s range
|
|
bool fCorrectionResult;
|
|
HRESULT hr = CorrectPhraseEltsAndRange( false, &fCorrectionResult );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return E_NOMERGE;
|
|
}
|
|
|
|
// Now have pTextRun end where "this" starts,
|
|
// so that it includes everything that could not be
|
|
// merged into "this"
|
|
pTextRun->SetEnd( GetStart() );
|
|
|
|
if ( fCorrectionResult )
|
|
{
|
|
// The entire range of pTextRun was appropriated by this
|
|
_ASSERTE( pTextRun->IsDegenerate() );
|
|
return E_FULLMERGE;
|
|
}
|
|
else
|
|
{
|
|
if ( pTextRun->GetEnd() == lOldBorder )
|
|
{
|
|
// None of pTextRun's range was appropriated
|
|
return E_NOMERGE;
|
|
}
|
|
else
|
|
{
|
|
// If pTextRun ends later than it did before,
|
|
// then concatenating pTextRun onto "this"
|
|
// reduced the number of phrase elements it
|
|
// contained.
|
|
// If pTextRun ends earlier than it did before,
|
|
// then "this" appropriated some part of it as
|
|
// phrase elements.
|
|
return (pTextRun->GetEnd() > lOldBorder) ?
|
|
E_LESSTEXT : E_PARTIALMERGE;
|
|
}
|
|
}
|
|
}
|
|
} /* CDictationRun::Concatenate */
|
|
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::CorrectPhraseEltsAndRange *
|
|
*------------------------------------------*
|
|
* Description:
|
|
* Ensures that the m_ulStartElement and m_cElements correctly
|
|
* reflect the text range. If not, adjusts the phrase element
|
|
* info and the range so that they agree.
|
|
*
|
|
* Depending on whether fForward is true, either goes forwards
|
|
* from m_ulStartElement or backwards from
|
|
* m_ulStartElement + m_cElements in matching up the phrases.
|
|
* If fForward is true, then includes any between-word space
|
|
* after the last element it matches, if that space was
|
|
* included in the original range.
|
|
*
|
|
* If fForward is true, the end of "this"'s range may be
|
|
* pushed back if there is some text at the end of the
|
|
* range that does not match phrase elements.
|
|
* If fForward is false, the start of "this"'s range may be
|
|
* pushed forward.
|
|
*
|
|
* Note that the range will never be expanded here, only
|
|
* possibly contracted.
|
|
*
|
|
* *pfCorrectionResult will be true iff the phrase element info
|
|
* and range did not have to be changed
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* Return value of ITextRange::GetDuplicate()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::CorrectPhraseEltsAndRange( bool fForward,
|
|
bool *pfCorrectionResult )
|
|
{
|
|
// Make sure current state is what we expect
|
|
_ASSERTE( m_pPhraseReplacement && m_pResultContainer );
|
|
|
|
// Store range and phrase element info.
|
|
// We will use these values later on to determine what has changed
|
|
const long lOldStart = GetStart();
|
|
const long lOldEnd = GetEnd();
|
|
const ULONG ulOldStartElement = m_ulStartElement;
|
|
const ULONG cOldElements = m_cElements;
|
|
|
|
#ifdef _DEBUG
|
|
BSTR bstrRangeText;
|
|
bstrRangeText = NULL; // RichEdit Win64 bug returns garbage instead of NULL
|
|
m_cpTextRange->GetText( &bstrRangeText );
|
|
#endif
|
|
|
|
// Degenerate range case: Number of elements should be 0.
|
|
if ( IsDegenerate() )
|
|
{
|
|
if ( m_cElements )
|
|
{
|
|
// Phrase information is incorrect since it says
|
|
// that we contain some phrase elements
|
|
m_cElements = 0;
|
|
|
|
if ( pfCorrectionResult )
|
|
{
|
|
*pfCorrectionResult = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( pfCorrectionResult )
|
|
{
|
|
*pfCorrectionResult = true;
|
|
}
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
// Nondegenerate case
|
|
|
|
// Create a duplicate range.
|
|
// This range will correspond to text we skip during
|
|
// our search for phrase elements
|
|
CComPtr<ITextRange> pSkippedRange;
|
|
HRESULT hr = m_cpTextRange->GetDuplicate( &pSkippedRange );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Depending on the direction, we will either want to start
|
|
// at the earliest element supposedly contained in this range
|
|
// (if fForward == true) or at the last element.
|
|
const ULONG ulFirstElement = fForward ? m_ulStartElement :
|
|
(m_ulStartElement + m_cElements - 1);
|
|
ULONG ulElement = ulFirstElement;
|
|
|
|
long cch, lOldBound;
|
|
BSTR bstrElement, bstrSkippedText;
|
|
|
|
// For each phrase element, find it in the range.
|
|
// We will use the start (resp. end) of "this"'s range
|
|
// in the calls to ITextRange::FindTextStart()
|
|
// which will move the start (or end) of our range just past
|
|
// each element that is found
|
|
while (ulElement < m_pPhraseReplacement->GetNumReplacementElements())
|
|
{
|
|
// Get the next phrase element that we are expecting
|
|
BYTE bDisplayAttributes = 0;
|
|
bstrElement = ::SysAllocString(
|
|
m_pPhraseReplacement->GetDisplayText( ulElement, &bDisplayAttributes ) );
|
|
|
|
// Remember where "this"'s range started (resp. ended)
|
|
lOldBound = fForward ? GetStart() : GetEnd();
|
|
|
|
// Either move the start to right before this word
|
|
// or move the end to right after this word.
|
|
// Note that we want to match entire words only if the display attributes
|
|
// don't tell us to consume leading spaces (like a comma or something)
|
|
if ( GetStart() == GetEnd() )
|
|
{
|
|
cch = 0;
|
|
}
|
|
else
|
|
{
|
|
fForward ?
|
|
m_cpTextRange->FindTextStart(
|
|
bstrElement,
|
|
0,
|
|
(bDisplayAttributes & SPAF_CONSUME_LEADING_SPACES) ? 0 : tomMatchWord,
|
|
&cch )
|
|
: m_cpTextRange->FindTextEnd(
|
|
bstrElement,
|
|
0,
|
|
(bDisplayAttributes & SPAF_CONSUME_LEADING_SPACES) ? 0 : tomMatchWord,
|
|
&cch );
|
|
}
|
|
|
|
|
|
::SysFreeString( bstrElement );
|
|
|
|
#ifdef _DEBUG
|
|
::SysFreeString( bstrRangeText );
|
|
bstrRangeText = NULL; // RichEdit Win64 bug returns garbage instead of NULL
|
|
m_cpTextRange->GetText( &bstrRangeText );
|
|
#endif
|
|
|
|
// Set pSkippedRange to include whatever text is between
|
|
// the old start (resp end) of m_cpTextRange (which is at the
|
|
// end (resp start) of the last phrase element we found)
|
|
// and the new start (resp end) of m_cpTextRange (which is at the start
|
|
// (resp end) of the phrase element we just found, if we did
|
|
// indeed find one)
|
|
pSkippedRange->SetStart( fForward ? lOldBound : GetEnd() );
|
|
pSkippedRange->SetEnd( fForward ? GetStart() : lOldBound );
|
|
|
|
// If this skipped range contains any text that is not whitespace,
|
|
// then there was something between the last phrase element and
|
|
// this phrase element that we just found.
|
|
bstrSkippedText = NULL; // RichEdit Win64 bug returns garbage instead of NULL
|
|
pSkippedRange->GetText( &bstrSkippedText );
|
|
if ( ContainsNonWhitespace( bstrSkippedText ) )
|
|
{
|
|
// Contains some non-spaces, so this element should not
|
|
// count as found, since there is some text between
|
|
// where we are and the phrase element
|
|
cch = 0;
|
|
|
|
// Set the start (resp. end) to the old bound, since we
|
|
// don't want to include this text (it does not consist
|
|
// solely of phrase elements
|
|
fForward ? SetStart( lOldBound ) : SetEnd( lOldBound );
|
|
}
|
|
::SysFreeString( bstrSkippedText );
|
|
|
|
if ( !cch )
|
|
{
|
|
// Phrase element not found:
|
|
// Break out of the loop
|
|
break;
|
|
}
|
|
|
|
// The phrase element was found; update the element
|
|
// offset array.
|
|
// For the time being, these are absolute indices
|
|
// (since if fForward is FALSE, then we don't yet know
|
|
// what the start of the range is.
|
|
// Later in the function they will be adjusted
|
|
// to be relative indices.
|
|
if ( fForward )
|
|
{
|
|
// Because of ITextRange::FindTextStart(),
|
|
// the start of this range is the start of this element
|
|
m_pulElementOffsets[ ulElement ] = GetStart();
|
|
}
|
|
else
|
|
{
|
|
// Because of ITextRange::FindTextEnd(),
|
|
// the end of this range is the end of this element.
|
|
// Subtract cch since cch is the length of the element
|
|
m_pulElementOffsets[ ulElement ] = GetEnd() - cch;
|
|
}
|
|
|
|
// Move the start (resp. the end) past the phrase element
|
|
// (whose length is cch)
|
|
fForward ? m_cpTextRange->MoveStart( tomCharacter, cch, NULL ) :
|
|
m_cpTextRange->MoveEnd( tomCharacter, -cch, NULL );
|
|
|
|
|
|
#ifdef _DEBUG
|
|
::SysFreeString( bstrRangeText );
|
|
bstrRangeText = NULL; // RichEdit Win64 bug returns garbage instead of NULL
|
|
m_cpTextRange->GetText( &bstrRangeText );
|
|
#endif
|
|
|
|
// Increment (decrement) ulElement to look for the next element
|
|
fForward ? ulElement++ : ulElement--;
|
|
} // (while loop)
|
|
|
|
#ifdef _DEBUG
|
|
::SysFreeString( bstrRangeText );
|
|
#endif
|
|
|
|
if ( fForward )
|
|
{
|
|
// If all of the elements were there, ulElement is equal
|
|
// to the old start element plus the old number of elements.
|
|
// Otherwise, ulElement is equal to the first phrase element
|
|
// after ulFirstElement that was not found.
|
|
|
|
// Adjust the phrase element information to reflect
|
|
// what we found
|
|
m_cElements = ulElement - ulFirstElement;
|
|
|
|
// Adjust the end so that only phrase elements are in this range.
|
|
// This is where the start of the range is right now (since we
|
|
// just moved the start past the last phrase element
|
|
// that was found.
|
|
SetEnd( GetStart() );
|
|
|
|
// Reset the start to its original location (it was moved around
|
|
// by ITextRange::FindTextStart())
|
|
SetStart( lOldStart );
|
|
|
|
// Now try to include any between-word space that can be included.
|
|
// up until the old end limit.
|
|
// We do not want to do this if there are no phrase elements found
|
|
// at all (in this case, the range should be degenerate).
|
|
if ( m_cElements )
|
|
{
|
|
m_cpTextRange->MoveEnd( tomWord, 1, NULL );
|
|
if ( GetEnd() > lOldEnd )
|
|
{
|
|
// We overshot the old end, so we should just end at the old end
|
|
SetEnd( lOldEnd );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If all of the elements were there, ulElement is equal
|
|
// to the old start element - 1.
|
|
// Otherwise, ulElement is equal to the first phrase element
|
|
// going backwards from ulFirstElement that was not found.
|
|
|
|
// Must adjust both the start element and the number of elements
|
|
m_ulStartElement = ulElement + 1;
|
|
m_cElements = ulFirstElement - ulElement;
|
|
|
|
// Adjust the start so that only phrase elements are in this range.
|
|
// This is where the end of the range is right now (since we
|
|
// just moved the end before the last phrase element
|
|
// that was found.
|
|
SetStart( GetEnd() );
|
|
|
|
// Reset the end to its original location (it was moved around
|
|
// by ITextRange::FindTextEnd())
|
|
SetEnd( lOldEnd );
|
|
|
|
// Do not include spaces leading up to this phrase
|
|
}
|
|
|
|
// If no elements were found in the range, then set the range to be degenerate
|
|
if ( !m_cElements )
|
|
{
|
|
fForward ? SetEnd( lOldStart ) : SetStart( lOldEnd );
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
// Fill the array with the bogus phrase element offset value wherever
|
|
// there is an element that did not fall into the "found" range
|
|
// of elements
|
|
const ULONG ulBogusVal = GetEnd() - GetStart();
|
|
for ( ulElement = 0;
|
|
ulElement < m_pPhraseReplacement->GetNumReplacementElements();
|
|
ulElement++ )
|
|
{
|
|
if ( (ulElement < m_ulStartElement) ||
|
|
(ulElement >= (m_ulStartElement + m_cElements)) )
|
|
{
|
|
m_pulElementOffsets[ ulElement ] = ulBogusVal;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// All elements in the range should have had their offsets updated above
|
|
// to be absolute locations within the document.
|
|
// Since we now know the start of the range, we can update the offsets
|
|
// of elements we have found relative to that.
|
|
long lStart = GetStart();
|
|
for ( ulElement = m_ulStartElement;
|
|
ulElement < (m_ulStartElement + m_cElements);
|
|
ulElement++ )
|
|
{
|
|
// Element is in the range; make its index
|
|
// relative to the start index of the range
|
|
m_pulElementOffsets[ ulElement ] -= lStart;
|
|
}
|
|
|
|
// Should return true iff nothing has changed (i.e. everything was already
|
|
// correct)
|
|
if ( pfCorrectionResult )
|
|
{
|
|
*pfCorrectionResult = (( lOldStart == GetStart() ) &&
|
|
( lOldEnd == GetEnd() ) &&
|
|
( ulOldStartElement == m_ulStartElement ) &&
|
|
( cOldElements == m_cElements ));
|
|
}
|
|
return S_OK;
|
|
|
|
} /* CDictationRun::CorrectPhraseEltsAndRange */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::GetAlternatesText *
|
|
*----------------------------------*
|
|
* Description:
|
|
* Called to get the alternates for the given range.
|
|
* Since pRangeForAlts does not necessarily line up
|
|
* with element boundaries, this method will get the
|
|
* alternates for the closest range it can get that
|
|
* contains whole elements.
|
|
* plAltStart and plAltEnd will point to the start
|
|
* and end of the range for which alternates are
|
|
* actually being returned.
|
|
* The text for the alternates is given in an array
|
|
* of CoTaskMemAlloced WCHAR *'s, and pcPhrasesReturned
|
|
* points to a count of how many of them there are.
|
|
* apfFitsInRun will be an array of bools indicating
|
|
* which alternates actually do fit in "this"'s range
|
|
* (since some may cover more phrase elements than
|
|
* are actually in our range right now)
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_INVALIDARG
|
|
* Return value of CDictationRun::FindNearestWholeElementRange()
|
|
* Return value of CResultContainer::GetAlternatesText()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::GetAlternatesText( ITextRange *pRangeForAlts,
|
|
ULONG ulRequestCount,
|
|
long *plAltStart,
|
|
long *plAltEnd,
|
|
__out_ecount_part(ulRequestCount, *pcPhrasesReturned) WCHAR **ppszCoMemText,
|
|
bool *apfFitsInRun,
|
|
__out ULONG *pcPhrasesReturned )
|
|
{
|
|
if ( !pRangeForAlts || !plAltStart || !plAltEnd ||
|
|
!ppszCoMemText || !pcPhrasesReturned || !apfFitsInRun )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
|
|
if ( !m_pResultContainer )
|
|
{
|
|
_ASSERTE( false );
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Validate the pRangeForAlts param
|
|
long lRangeStart;
|
|
long lRangeEnd;
|
|
pRangeForAlts->GetStart( &lRangeStart );
|
|
pRangeForAlts->GetEnd( &lRangeEnd );
|
|
if ( !( WithinRange( lRangeStart ) && WithinRange( lRangeEnd ) ) )
|
|
{
|
|
// Asking for alternates from an invalid range
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
// Expand the range to the smallest range containing it that
|
|
// contains whole elements
|
|
ULONG ulStartElement;
|
|
ULONG cElements;
|
|
HRESULT hr = FindNearestWholeElementRange( lRangeStart, lRangeEnd,
|
|
plAltStart, plAltEnd, &ulStartElement, &cElements );
|
|
|
|
*pcPhrasesReturned = 0;
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// Get the alternates text from the result object
|
|
hr = m_pResultContainer->GetAlternatesText(
|
|
ulStartElement, cElements, ulRequestCount, ppszCoMemText,
|
|
pcPhrasesReturned );
|
|
}
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// Remember what these are alternates for
|
|
m_fAltsGotten = true;
|
|
m_ulStartAltElt = ulStartElement;
|
|
m_cAltElt = cElements;
|
|
|
|
// For each alternate, mark whether or not it fits in the run
|
|
ULONG ulStartInParent;
|
|
ULONG cEltsInParent;
|
|
HRESULT hrAltInfo;
|
|
for ( ULONG ulAlt = 0; ulAlt < *pcPhrasesReturned; ulAlt++ )
|
|
{
|
|
// Find out which elements this alternate covers
|
|
hrAltInfo = m_pResultContainer->GetAltInfo( ulStartElement, cElements, ulAlt,
|
|
&ulStartInParent, &cEltsInParent );
|
|
|
|
// Check if it falls within our m_ulStartElement and m_cElements
|
|
apfFitsInRun[ ulAlt ] = SUCCEEDED(hrAltInfo) &&
|
|
( ulStartInParent >= m_ulStartElement ) &&
|
|
( (ulStartInParent + cEltsInParent) <= (m_ulStartElement + m_cElements) );
|
|
|
|
// See if we got a NULL alternate back, if we did then mark it so it gets ignored.
|
|
if ( ppszCoMemText[ ulAlt ] == NULL )
|
|
{
|
|
apfFitsInRun[ ulAlt ] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
} /* CDictationRun::GetAlternatesText */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::GetAltEndpoints *
|
|
*--------------------------------*
|
|
* Description:
|
|
* Returns the start and end points of the text in the document
|
|
* that the alternate (from the last call to GetAlternatesText())
|
|
* would replace
|
|
* Return:
|
|
* S_OK
|
|
* return value of CResultContainer::GetAltInfo()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::GetAltEndpoints( ULONG ulAlt,
|
|
long *plReplaceStart,
|
|
long *plReplaceEnd )
|
|
{
|
|
_ASSERTE( m_fAltsGotten );
|
|
_ASSERTE( m_pResultContainer );
|
|
if ( !m_fAltsGotten || !m_pResultContainer )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Find out which elements this alternate covers
|
|
ULONG ulReplaceStartElt;
|
|
ULONG cReplacedElts;
|
|
HRESULT hr = m_pResultContainer->GetAltInfo(
|
|
m_ulStartAltElt, m_cAltElt, ulAlt,
|
|
&ulReplaceStartElt, &cReplacedElts );
|
|
if ( FAILED(hr) )
|
|
{
|
|
return hr;
|
|
}
|
|
_ASSERTE( (ulReplaceStartElt + cReplacedElts) <=
|
|
m_pPhraseReplacement->GetNumReplacementElements() );
|
|
|
|
if ( plReplaceStart )
|
|
{
|
|
// Get the start position of the first element that would be replaced
|
|
*plReplaceStart = GetStart() + m_pulElementOffsets[ ulReplaceStartElt ];
|
|
}
|
|
|
|
if ( plReplaceEnd )
|
|
{
|
|
// The range that would be replaced ends where the first element that
|
|
// won't be replaced starts.
|
|
// Get the start position of the first element that would not be
|
|
// replaced (the endpoint), or the end of the run if the last element
|
|
// would be replaced.
|
|
if ( (ulReplaceStartElt + cReplacedElts) == (m_ulStartElement + m_cElements) )
|
|
{
|
|
// The last element in the run would be replaced
|
|
*plReplaceEnd = GetEnd();
|
|
}
|
|
else
|
|
{
|
|
// The last element in the run would not be replaced
|
|
*plReplaceEnd = GetStart() +
|
|
m_pulElementOffsets[ ulReplaceStartElt + cReplacedElts ];
|
|
}
|
|
}
|
|
return S_OK;
|
|
} /* CDictationRun::GetAltEndpoints */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::ChooseAlternate *
|
|
*--------------------------------*
|
|
* Description:
|
|
* Called when the user wishes to commit an alternate
|
|
* from the last group obtained via GetAlternatesText()
|
|
* Return:
|
|
* S_OK
|
|
* return value of CResultContainer::ChooseAlternate
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::ChooseAlternate( ULONG ulAlt )
|
|
{
|
|
_ASSERTE( m_fAltsGotten && m_pResultContainer );
|
|
if ( !m_fAltsGotten || !m_pResultContainer )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Find out which elements will be replaced
|
|
ULONG ulActualAltStart;
|
|
ULONG cActualAltElts;
|
|
HRESULT hr = m_pResultContainer->GetAltInfo(
|
|
m_ulStartAltElt, m_cAltElt, ulAlt, &ulActualAltStart, &cActualAltElts );
|
|
|
|
// Get the text for the alternate that will replace these elements
|
|
CSpDynamicString dstrText;
|
|
BYTE bDisplayAttributes = 0;
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = m_pResultContainer->GetAltText( m_ulStartAltElt, m_cAltElt,
|
|
ulAlt, &dstrText, &bDisplayAttributes );
|
|
}
|
|
|
|
// cpRangeToReplace will cover the text of the elements to replace
|
|
CComPtr<ITextRange> cpRangeToReplace;
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = m_cpTextRange->GetDuplicate( &cpRangeToReplace );
|
|
}
|
|
|
|
// Set the range of cpRangeToReplace to contain the correct text
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
// The start is the start of the first element covered by the alternate
|
|
cpRangeToReplace->SetStart( GetStart() + m_pulElementOffsets[ulActualAltStart] );
|
|
|
|
// The end is the start of the first element not covered by the alternate,
|
|
// or the end of this DictationRun's range, if the last element in the run
|
|
// is covered by the alternate
|
|
if ( (ulActualAltStart + cActualAltElts) == (m_ulStartElement + m_cElements) )
|
|
{
|
|
// The last element in the run is covered
|
|
cpRangeToReplace->SetEnd( GetEnd() );
|
|
}
|
|
else
|
|
{
|
|
// The last element in the run is not covered
|
|
cpRangeToReplace->SetEnd( GetStart() +
|
|
m_pulElementOffsets[ulActualAltStart + cActualAltElts] );
|
|
}
|
|
}
|
|
|
|
// Deal with display attributes:
|
|
|
|
// pwszTextToInsert will contain the actual text to insert, with the
|
|
// correct spaces
|
|
// At most, it will need space for the recognized text, a terminating nul,
|
|
// two spaces before, and two spaces after
|
|
size_t cTextToInsert = wcslen( dstrText ) + 5;
|
|
WCHAR *pwszTextToInsert = new WCHAR[ cTextToInsert ];
|
|
if ( !pwszTextToInsert )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
pwszTextToInsert[0] = 0;
|
|
|
|
// Consume spaces before the alternate if necessary,
|
|
// or add spaces before the alternate.
|
|
// To be safe, we will not consume leading spaces outside of
|
|
// this run
|
|
if ( (ulActualAltStart > m_ulStartElement) &&
|
|
(SPAF_CONSUME_LEADING_SPACES & bDisplayAttributes) )
|
|
{
|
|
// Move the start of the range back until is is
|
|
// right after a non-space character
|
|
HRESULT hrMove;
|
|
long lFirstChar;
|
|
do
|
|
{
|
|
hrMove = cpRangeToReplace->MoveStart( -1, tomCharacter, NULL );
|
|
|
|
// Look at the first character that is now in the range
|
|
lFirstChar = 0;
|
|
cpRangeToReplace->GetChar( &lFirstChar );
|
|
|
|
} while (( S_OK == hrMove ) && ( L' ' == ((WCHAR) lFirstChar) ));
|
|
|
|
if ( S_OK == hrMove )
|
|
{
|
|
// We have found a non-space character to the left of the start,
|
|
// move just past it
|
|
cpRangeToReplace->MoveStart( 1, tomCharacter, NULL );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If the previous element calls for trailing spaces and there aren't
|
|
// any, we need to add them
|
|
UINT uiNumLeadingSpacesNeeded = 1;
|
|
BYTE bPreviousDisplayAttributes = 0;
|
|
if (( ulActualAltStart > 0 ) && ( ulActualAltStart > m_ulStartElement ))
|
|
{
|
|
m_pPhraseReplacement->GetDisplayText( ulActualAltStart - 1,
|
|
&bPreviousDisplayAttributes );
|
|
if ( bPreviousDisplayAttributes & SPAF_ONE_TRAILING_SPACE )
|
|
{
|
|
uiNumLeadingSpacesNeeded = 1;
|
|
}
|
|
else if ( bPreviousDisplayAttributes & SPAF_TWO_TRAILING_SPACES )
|
|
{
|
|
uiNumLeadingSpacesNeeded = 2;
|
|
}
|
|
else
|
|
{
|
|
uiNumLeadingSpacesNeeded = 0;
|
|
}
|
|
}
|
|
|
|
if ( uiNumLeadingSpacesNeeded > 0 )
|
|
{
|
|
// Now look back to previous characters
|
|
CComPtr<ITextRange> cpDupRange;
|
|
if ( FAILED( cpRangeToReplace->GetDuplicate( &cpDupRange ) ) )
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
}
|
|
long lDupRangeStart = 0;
|
|
cpDupRange->GetStart( &lDupRangeStart );
|
|
|
|
if ( lDupRangeStart > 0 )
|
|
{
|
|
cpDupRange->MoveStart( tomCharacter, -1, NULL );
|
|
lDupRangeStart--;
|
|
|
|
long lFirstChar = 0;
|
|
cpDupRange->GetChar( &lFirstChar );
|
|
if ( !iswspace( (WCHAR) lFirstChar ) )
|
|
{
|
|
wcscat_s( pwszTextToInsert, cTextToInsert,
|
|
(1 == uiNumLeadingSpacesNeeded) ? L" " : L" " );
|
|
}
|
|
else if ( 2 == uiNumLeadingSpacesNeeded )
|
|
{
|
|
// If two spaces are called for but only one is there,
|
|
// we need an additional space
|
|
if ( lDupRangeStart > 0 )
|
|
{
|
|
cpDupRange->GetChar( &lFirstChar );
|
|
if ( !iswspace( (WCHAR) lFirstChar ) )
|
|
{
|
|
wcscat_s( pwszTextToInsert, cTextToInsert, L" " );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the display attributes for the element to follow.
|
|
// (If the alt covers the last element, CPhraseReplacement::GetDisplayText()
|
|
// will return NULL)
|
|
BYTE bFollowingDisplayAttributes = 0;
|
|
const WCHAR *pwszText = m_pPhraseReplacement->GetDisplayText(
|
|
ulActualAltStart + cActualAltElts,
|
|
&bFollowingDisplayAttributes );
|
|
bool fConsumeLeadingSpacesAfterAlt =
|
|
pwszText && (bFollowingDisplayAttributes & SPAF_CONSUME_LEADING_SPACES);
|
|
|
|
// Append spaces if necessary: That is, if the attribute itself
|
|
// calls for trailing spaces and the next element is not
|
|
// CONSUME_LEADING_SPACES
|
|
if ( !fConsumeLeadingSpacesAfterAlt )
|
|
{
|
|
if ( SPAF_TWO_TRAILING_SPACES & bDisplayAttributes )
|
|
{
|
|
dstrText.Append( L" " );
|
|
}
|
|
else if ( SPAF_ONE_TRAILING_SPACE & bDisplayAttributes )
|
|
{
|
|
dstrText.Append( L" " );
|
|
}
|
|
}
|
|
|
|
// Get the recognized text and any trailing spaces
|
|
wcscat_s( pwszTextToInsert, cTextToInsert, dstrText );
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
BSTR bstrText = ::SysAllocString( pwszTextToInsert );
|
|
|
|
// Change the text
|
|
hr = cpRangeToReplace->SetText( bstrText );
|
|
|
|
::SysFreeString( bstrText );
|
|
}
|
|
|
|
delete[] pwszTextToInsert;
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// Notify the common reco result so that all runs sharing
|
|
// this result can adjust their offset maps if necessary
|
|
// (as the indices of elements may have changed)
|
|
BYTE b;
|
|
hr = m_pResultContainer->ChooseAlternate(
|
|
m_ulStartAltElt, m_cAltElt, ulAlt, &b );
|
|
}
|
|
|
|
// At this point the alternate has been committed to the phrase,
|
|
// and our PhraseReplacement object knows about the committed alternate
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// This will get us the new offsets for the phrase elements we
|
|
// now have
|
|
hr = CorrectPhraseEltsAndRange( true );
|
|
}
|
|
|
|
return hr;
|
|
} /* CDictationRun::ChooseAlternate */
|
|
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::OnAlternateCommit *
|
|
*----------------------------------*
|
|
* Description:
|
|
* Called right before an alternate is committed on this
|
|
* DictationRun's RecoResult.
|
|
* If the commit has resulted in an increase in the
|
|
* number of elements, the offset map will need to be freed
|
|
* and allocated again.
|
|
* Move the element offset information so that it is correct.
|
|
* Return:
|
|
* S_OK
|
|
* E_OUTOFMEMORY
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::OnAlternateCommit( ULONG ulStartEltToBeReplaced,
|
|
ULONG cEltsToBeReplaced,
|
|
ULONG cElementsInPhraseAfterCommit )
|
|
{
|
|
ULONG cReplacementElements = m_pPhraseReplacement->GetNumReplacementElements();
|
|
|
|
if ( cElementsInPhraseAfterCommit == cReplacementElements )
|
|
{
|
|
// If there is no change in the element count, nothing needs to be done
|
|
return S_OK;
|
|
}
|
|
|
|
if ( ulStartEltToBeReplaced > ULONG_MAX - cEltsToBeReplaced ||
|
|
cElementsInPhraseAfterCommit > (ULONG)LONG_MAX ||
|
|
cReplacementElements > (ULONG)LONG_MAX ||
|
|
ulStartEltToBeReplaced + cEltsToBeReplaced > cReplacementElements )
|
|
{
|
|
// Out-of-bounds arguments
|
|
_ASSERTE( false );
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Determine the change in element count (i.e. whether there will now be
|
|
// more or fewer elements)
|
|
long lElementShift = (long)cElementsInPhraseAfterCommit - (long)cReplacementElements;
|
|
|
|
if ( lElementShift > 0 )
|
|
{
|
|
// The number of elements has increased: need to reallocate
|
|
ULONG *pulNewOffsets = new ULONG[ cElementsInPhraseAfterCommit ];
|
|
if ( !pulNewOffsets )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
ULONG ulElement;
|
|
|
|
#ifdef _DEBUG
|
|
// Fill the element offsets with a bogus value
|
|
const ULONG ulBogusVal = GetEnd() - GetStart();
|
|
for( ulElement = 0;
|
|
ulElement < cElementsInPhraseAfterCommit;
|
|
ulElement++ )
|
|
{
|
|
pulNewOffsets[ulElement] = ulBogusVal;
|
|
}
|
|
#endif
|
|
|
|
// Copy over the relevant elements.
|
|
if ( (m_ulStartElement + m_cElements) <= ulStartEltToBeReplaced )
|
|
{
|
|
// The replacement occurs after this run
|
|
// so we can just copy over the entries.
|
|
for ( ulElement = m_ulStartElement;
|
|
ulElement < (m_ulStartElement + m_cElements) && ulElement < cElementsInPhraseAfterCommit;
|
|
ulElement++ )
|
|
{
|
|
pulNewOffsets[ulElement] = m_pulElementOffsets[ulElement];
|
|
}
|
|
}
|
|
else if ( m_ulStartElement >= (ulStartEltToBeReplaced + cEltsToBeReplaced) )
|
|
{
|
|
// The replacement occurs before this run, so shifting is necessary
|
|
// Shift the entries to the right (recall that lElementShift > 0)
|
|
for ( ulElement = m_ulStartElement;
|
|
ulElement < (m_ulStartElement + m_cElements) && ulElement < cReplacementElements;
|
|
ulElement++ )
|
|
{
|
|
pulNewOffsets[ulElement + lElementShift] = m_pulElementOffsets[ulElement];
|
|
}
|
|
|
|
// The start element also needs to be shifted
|
|
m_ulStartElement += lElementShift;
|
|
}
|
|
|
|
// If this run is the run in which the replacement will take place,
|
|
// do not do anything here, as this run will later do its own thing to
|
|
// recalculate its element offsets
|
|
|
|
// Delete the old map, and make the element offset map point to the new map
|
|
delete[] m_pulElementOffsets;
|
|
m_pulElementOffsets = pulNewOffsets;
|
|
}
|
|
else
|
|
{
|
|
// The number of phrase elements has decreased, so we already have
|
|
// enough space allocated for the map
|
|
|
|
// Sanity check: We had better not be shifting back past zero
|
|
if ( (lElementShift < 0) && ((ulStartEltToBeReplaced + cEltsToBeReplaced) < (ULONG)(-lElementShift)) )
|
|
{
|
|
_ASSERTE( false );
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Copy over the offset info if necessary
|
|
ULONG ulElement;
|
|
if ( m_ulStartElement >= (ulStartEltToBeReplaced + cEltsToBeReplaced) )
|
|
{
|
|
// The replacement occurs before this run.
|
|
// Shift the entries to the left (recall that lElementShift < 0,
|
|
// and that is why we are adding lElementShift, not subtracting)
|
|
for ( ulElement = m_ulStartElement;
|
|
ulElement < (m_ulStartElement + m_cElements);
|
|
ulElement++ )
|
|
{
|
|
m_pulElementOffsets[ulElement + lElementShift] =
|
|
m_pulElementOffsets[ulElement];
|
|
}
|
|
|
|
// The start element also needs to be shifted
|
|
m_ulStartElement += lElementShift;
|
|
}
|
|
|
|
// If the replacement occurs after this run, all of the phrase
|
|
// element information is already correct.
|
|
|
|
// If this is the run in which the alternate was committed, do not
|
|
// do anything here, as this run will later do its own thing to
|
|
// recalculate its element offsets
|
|
}
|
|
|
|
return S_OK;
|
|
} /* CDictationRun::OnAlternateCommit */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::Speak *
|
|
*----------------------*
|
|
* Description:
|
|
* Speaks all of the audio associated with this text run. Uses
|
|
* TTS if there's a problem speaking the audio data.
|
|
*
|
|
* Return:
|
|
* S_OK
|
|
* S_FALSE for a degenerate run
|
|
* Return value of CResultContainer::SpeakAudio()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::Speak( ISpVoice &rVoice )
|
|
{
|
|
_ASSERTE( m_pResultContainer && m_pPhraseReplacement && m_cpTextRange );
|
|
if ( !m_pResultContainer || !m_pPhraseReplacement || !m_cpTextRange )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
if ( !m_cElements )
|
|
{
|
|
// This run is just a place-holder, no need to speak it
|
|
return S_FALSE;
|
|
}
|
|
|
|
// Try to speak it as audio
|
|
HRESULT hr = m_pResultContainer->SpeakAudio(
|
|
m_ulStartElement, m_cElements );
|
|
|
|
if( FAILED( hr ) )
|
|
{
|
|
// Speak it as text
|
|
hr = CTextRun::Speak( rVoice );
|
|
}
|
|
return hr;
|
|
|
|
} /* CDictationRun::Speak */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::Speak *
|
|
*----------------------*
|
|
* Description:
|
|
* Finds the smallest range that contains *plStart -> *plEnd
|
|
* and still contains whole elements. Returns the start
|
|
* and end of the text it actually spoke in *plStart and *plEnd.
|
|
* Speaks the audio associated with this range within this text run.
|
|
* Uses TTS if there's a problem speaking the audio data.
|
|
*
|
|
* Out-of-bounds *plStart causes the range to be spoken from
|
|
* the start.
|
|
* Out-of-bounds *plEnd causes the range to be spoken all the
|
|
* way to the end.
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_OUTOFMEMORY
|
|
* Return value of CDictationRun::FindNearestWholeElementRange()
|
|
* Return value of ITextRange::{Get,Set}{Start,End}()
|
|
* Return value of CDictationRun::Speak()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::Speak( ISpVoice &rVoice,
|
|
long *plStart,
|
|
long *plEnd )
|
|
{
|
|
if ( !plStart || !plEnd )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
|
|
// This function actually temporarily shrinks this DictationRun's
|
|
// range, so we need to store all of its information before
|
|
// proceeding.
|
|
|
|
// Save the old range
|
|
const long lOldStart = GetStart();
|
|
const long lOldEnd = GetEnd();
|
|
|
|
// Save the old phrase element information
|
|
const ULONG ulOldStartElement = m_ulStartElement;
|
|
const ULONG cOldElements = m_cElements;
|
|
|
|
// Save the element offsets
|
|
const ULONG ulOffsetArraySize =
|
|
sizeof(ULONG) * m_pPhraseReplacement->GetNumReplacementElements();
|
|
ULONG *pulOldElementOffsets = new ULONG[ ulOffsetArraySize ];
|
|
if ( !pulOldElementOffsets )
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
memcpy( pulOldElementOffsets, m_pulElementOffsets, ulOffsetArraySize );
|
|
|
|
// Fix out-of-bounds limits, since these mean to speak the entire run
|
|
if ( !WithinRange( *plStart ) )
|
|
{
|
|
*plStart = lOldStart;
|
|
}
|
|
if ( !WithinRange( *plEnd ) )
|
|
{
|
|
*plEnd = lOldEnd;
|
|
}
|
|
|
|
// Find the smallest range containing this range that contains only
|
|
// whole elements
|
|
long lWholeEltStart;
|
|
long lWholeEltEnd;
|
|
HRESULT hr = FindNearestWholeElementRange(
|
|
*plStart, *plEnd, &lWholeEltStart, &lWholeEltEnd, NULL, NULL );
|
|
|
|
// Set the end to the end of the range to speak
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// The limits of this whole-element range are what we are going to speak
|
|
*plStart = lWholeEltStart;
|
|
*plEnd = lWholeEltEnd;
|
|
|
|
hr = m_cpTextRange->SetEnd( *plEnd );
|
|
}
|
|
|
|
// Find out what phrase elements are now contained
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
hr = CorrectPhraseEltsAndRange( true );
|
|
}
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// CDictationRun::CorrectPhraseEltsAndRange() might have moved the end
|
|
hr = m_cpTextRange->GetEnd( plEnd );
|
|
}
|
|
|
|
// Set the start to the start of the range to speak
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
hr = m_cpTextRange->SetStart( *plStart );
|
|
}
|
|
|
|
// Find out what phrase elements are now contained
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
hr = CorrectPhraseEltsAndRange( false );
|
|
}
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// CDictationRun::CorrectPhraseEltsAndRange() might have moved the start
|
|
hr = m_cpTextRange->GetStart( plStart );
|
|
}
|
|
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
// Pass to CDictationRun::Speak() in order to speak the entire range,
|
|
// which now consists solely of what we want to speak
|
|
hr = Speak( rVoice );
|
|
}
|
|
|
|
// Restore old range and phrase element info
|
|
SetStart( lOldStart );
|
|
SetEnd( lOldEnd );
|
|
m_ulStartElement = ulOldStartElement;
|
|
m_cElements = cOldElements;
|
|
memcpy( m_pulElementOffsets, pulOldElementOffsets, ulOffsetArraySize );
|
|
delete[] pulOldElementOffsets;
|
|
|
|
return hr;
|
|
} /* CDictationRun::Speak */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::Serialize *
|
|
*--------------------------*
|
|
* Description:
|
|
* Serializes the text and audio data for this run.
|
|
* Writes the data to the pStream.
|
|
* There are two headers to precede the seralized run;
|
|
* one containing information about the run as a generic
|
|
* text run, and one containing information about the
|
|
* run and its dictation-specific information.
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* Return value of CResultContainer::Serialize()
|
|
* Return value of ISequentialStream::Write()
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::Serialize( IStream *pStream, ISpRecoContext *pRecoCtxt )
|
|
{
|
|
if ( !pStream || !pRecoCtxt )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
_ASSERTE( m_cpTextRange && m_pResultContainer );
|
|
if ( !m_cpTextRange || !m_pResultContainer )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
// Make the relevant headers
|
|
|
|
// This header deals with the run as a generic text run
|
|
RUNHEADER runHdr;
|
|
runHdr.lStart = GetStart();
|
|
runHdr.lEnd = GetEnd();
|
|
runHdr.bResultFollows = true;
|
|
|
|
// This header deals with the run as a CDictationRun
|
|
DICTHEADER dictHdr;
|
|
dictHdr.ulStartElement = m_ulStartElement;
|
|
dictHdr.cElements = m_cElements;
|
|
|
|
// Get the phrase blob from the recoresult
|
|
SPSERIALIZEDRESULT * pResultBlock;
|
|
HRESULT hr = m_pResultContainer->Serialize( &pResultBlock );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Get the size of the result block
|
|
dictHdr.cbSize = pResultBlock->ulSerializedSize;
|
|
|
|
// Write the headers and the phrase blob to the stream
|
|
ULONG cbWritten;
|
|
hr = pStream->Write( &runHdr, sizeof( RUNHEADER ), &cbWritten );
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
hr = pStream->Write( &dictHdr, sizeof( DICTHEADER ), &cbWritten );
|
|
}
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
hr = pStream->Write( pResultBlock, dictHdr.cbSize, &cbWritten );
|
|
}
|
|
|
|
return hr;
|
|
} /* CDictationRun::Serialize */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::IsConsumeLeadingSpaces *
|
|
*---------------------------------------*
|
|
* Description:
|
|
* Sets *pfConsumeLeadingSpaces to true iff any text inserted
|
|
* at lPos would have to consume a space that follows it.
|
|
* This value will be false UNLESS lPos is the first
|
|
* position of a phrase element that has the attribute
|
|
* SPAF_CONSUME_LEADING_SPACES
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_INVALIDARG if lPos is out of range
|
|
* return value of CDictationRun::FindNearestWholeElementRange()
|
|
* E_FAIL if the CPhraseReplacement::GetDisplayText() fails
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::IsConsumeLeadingSpaces( const long lPos,
|
|
bool *pfConsumeLeadingSpaces )
|
|
{
|
|
if ( !pfConsumeLeadingSpaces )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
if ( !WithinRange( lPos ) )
|
|
{
|
|
return E_INVALIDARG;
|
|
}
|
|
*pfConsumeLeadingSpaces = false; // by default
|
|
|
|
// Expand lPos to the nearest element
|
|
long lEltStartPos;
|
|
long lEltEndPos;
|
|
ULONG ulStartElt;
|
|
ULONG cElts;
|
|
HRESULT hr = FindNearestWholeElementRange( lPos, lPos,
|
|
&lEltStartPos, &lEltEndPos, &ulStartElt, &cElts );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Should have found only one element
|
|
_ASSERTE( 1 == cElts );
|
|
|
|
// We definitely don't want to consume leading spaces unless
|
|
// this is the beginning of the element
|
|
if ( lPos != lEltStartPos )
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
// Get the text and attributes for that element
|
|
BYTE bDisplayAttributes;
|
|
const WCHAR *pwszEltText = m_pPhraseReplacement->GetDisplayText(
|
|
ulStartElt, &bDisplayAttributes );
|
|
if ( !pwszEltText )
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Check for the CONSUME_LEADING_SPACES attribute
|
|
if ( SPAF_CONSUME_LEADING_SPACES & bDisplayAttributes )
|
|
{
|
|
*pfConsumeLeadingSpaces = true;
|
|
}
|
|
|
|
return S_OK;
|
|
} /* CDictationRun::IsConsumeLeadingSpaces */
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::HowManySpacesAfter *
|
|
*-----------------------------------*
|
|
* Description:
|
|
* Returns the number of spaces that would need to precede text
|
|
* if text were to be inserted at position lPos.
|
|
* This number goes in the out param puiSpaces.
|
|
* The value returned will be zero UNLESS lPos immediately
|
|
* follows the text of a phrase element that has
|
|
* SPAF_ONE_TRAILING_SPACE or SPAF_TWO_TRAILINGSPACES attributes
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_INVALIDARG if lPos is out of range
|
|
* return value of CDictationRun::FindNearestWholeElementRange()
|
|
* E_FAIL if the CPhraseReplacement::GetDisplayText() fails
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::HowManySpacesAfter( const long lPos,
|
|
UINT *puiSpaces )
|
|
{
|
|
if ( !puiSpaces )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
if ( !WithinRange( lPos ) )
|
|
{
|
|
return E_INVALIDARG;
|
|
}
|
|
*puiSpaces = 0; // by default
|
|
|
|
// Expand lPos to the nearest element
|
|
long lEltStartPos;
|
|
long lEltEndPos;
|
|
ULONG ulStartElt;
|
|
ULONG cElts;
|
|
HRESULT hr = FindNearestWholeElementRange( lPos, lPos,
|
|
&lEltStartPos, &lEltEndPos, &ulStartElt, &cElts );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Should have found only one element
|
|
_ASSERTE( 1 == cElts );
|
|
|
|
// Get the text and attributes for that element
|
|
BYTE bDisplayAttributes;
|
|
const WCHAR *pwszEltText = m_pPhraseReplacement->GetDisplayText(
|
|
ulStartElt, &bDisplayAttributes );
|
|
if ( !pwszEltText )
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
// If that element asks for leading spaces to be consumed and
|
|
// lPos is at the beginning of that element, we actually
|
|
// want to be looking at the element for the previous space,
|
|
// since those two elements may run together.
|
|
// (For instance, if lPos fell between the 'd' and the '.' in
|
|
// "This sentence ends with a period." We want attributes for
|
|
// "period", not ".")
|
|
if (( bDisplayAttributes & SPAF_CONSUME_LEADING_SPACES )
|
|
&& ( lPos == lEltStartPos )
|
|
&& ( lPos > GetStart() ))
|
|
{
|
|
// Get the element that (lPos - 1) is on
|
|
hr = FindNearestWholeElementRange( lPos - 1, lPos - 1,
|
|
&lEltStartPos, &lEltEndPos, &ulStartElt, &cElts );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
// Should have found only one element
|
|
_ASSERTE( 1 == cElts );
|
|
|
|
// Get the text and attributes for that element
|
|
pwszEltText = m_pPhraseReplacement->GetDisplayText(
|
|
ulStartElt, &bDisplayAttributes );
|
|
if ( !pwszEltText )
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
}
|
|
|
|
// Now look for attributes if lPos falls at the end of this element
|
|
if ( (lEltStartPos + (long) wcslen( pwszEltText )) == lPos )
|
|
{
|
|
// lPos immediately follows the element text,
|
|
// so pay attention to the display attributes
|
|
if ( SPAF_ONE_TRAILING_SPACE & bDisplayAttributes )
|
|
{
|
|
*puiSpaces = 1;
|
|
}
|
|
else if ( SPAF_TWO_TRAILING_SPACES & bDisplayAttributes )
|
|
{
|
|
*puiSpaces = 2;
|
|
}
|
|
}
|
|
else if ( (lEltStartPos + (long) wcslen( pwszEltText ) + 1) == lPos )
|
|
{
|
|
// lPos is the position after the position immediately following
|
|
// the element text. We care about this situation only if
|
|
// we want two trailing spaces, in which case we need only one more
|
|
if ( SPAF_TWO_TRAILING_SPACES & bDisplayAttributes )
|
|
{
|
|
*puiSpaces = 1;
|
|
}
|
|
}
|
|
|
|
// Otherwise text can be inserted right at lPos
|
|
|
|
return S_OK;
|
|
} /* CDictationRun::HowManySpacesAfter */
|
|
|
|
|
|
/**********************************************************************
|
|
* CDictationRun::FindNearestWholeElementRange *
|
|
*---------------------------------------------*
|
|
* Description:
|
|
* Finds the start and end of the smallest range containing
|
|
* the range with lStart and lEnd that contains whole elements.
|
|
* Upon return, pulStartElement and pcElements will point
|
|
* to information about the elements to which the range
|
|
* best corresponds.
|
|
* Upon return, plResultStart and plResultEnd will point
|
|
* to the start and end indices (in the document) of this
|
|
* element range.
|
|
* Return:
|
|
* S_OK
|
|
* E_POINTER
|
|
* E_FAIL if there are no elements in this range
|
|
* E_INVALIDARG if the range lStart...lEnd is not contained
|
|
* within our range
|
|
**********************************************************************/
|
|
HRESULT CDictationRun::FindNearestWholeElementRange( long lStart,
|
|
long lEnd,
|
|
long *plResultStart,
|
|
long *plResultEnd,
|
|
ULONG *pulStartElement,
|
|
ULONG *pcElements )
|
|
{
|
|
if ( !plResultStart || !plResultEnd )
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
_ASSERTE( m_cpTextRange && m_pulElementOffsets );
|
|
if ( !m_cpTextRange || !m_pulElementOffsets )
|
|
{
|
|
return E_UNEXPECTED;
|
|
}
|
|
|
|
if ( !m_cElements )
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
if ( !( WithinRange( lStart ) && WithinRange( lEnd ) ) )
|
|
{
|
|
// Asking for out-of-range text
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
ULONG ulThisRunStart = (ULONG) GetStart();
|
|
ULONG ulStart = (ULONG)lStart;
|
|
|
|
// Find the last element offset that is
|
|
// less than or equal to lStart.
|
|
// This is accomplished by going backwards through the
|
|
// elements until we find one with an offset that is <= lStart,
|
|
// or until we hit the start element
|
|
ULONG ulElement;
|
|
for ( ulElement = m_ulStartElement + m_cElements - 1;
|
|
(ulElement > m_ulStartElement) && ((ulThisRunStart + m_pulElementOffsets[ulElement]) > ulStart);
|
|
ulElement-- )
|
|
;
|
|
ULONG ulStartElement = ulElement;
|
|
_ASSERTE( m_pulElementOffsets[ulStartElement] <= ulStart );
|
|
|
|
// Get the result into the out params
|
|
*plResultStart = ulThisRunStart + m_pulElementOffsets[ulStartElement];
|
|
if ( pulStartElement )
|
|
{
|
|
*pulStartElement = ulStartElement;
|
|
}
|
|
|
|
// Find the first element offset that is
|
|
// greater than or equal to lEnd.
|
|
// This is the first element that we will not include.
|
|
ULONG ulEnd = (ULONG) lEnd;
|
|
for ( ulElement = ulStartElement;
|
|
(ulElement < m_ulStartElement + m_cElements) &&
|
|
((ulThisRunStart + m_pulElementOffsets[ulElement]) < ulEnd);
|
|
ulElement++ )
|
|
;
|
|
_ASSERTE( ulElement >= ulStartElement );
|
|
if ( ulElement == ulStartElement )
|
|
{
|
|
// The range was degenerate and at the beginning of a phrase element;
|
|
// we should include one element
|
|
ulElement++;
|
|
}
|
|
|
|
// Get the result into the out params
|
|
if ( (m_ulStartElement + m_cElements) == ulElement )
|
|
{
|
|
// Our range contains part of the final element,
|
|
// so get the end of the range from the end of
|
|
// the document
|
|
*plResultEnd = GetEnd();
|
|
}
|
|
else
|
|
{
|
|
// Get the end of the range from the offset of the
|
|
// first element not included.
|
|
*plResultEnd = ulThisRunStart + m_pulElementOffsets[ulElement];
|
|
}
|
|
if ( pcElements )
|
|
{
|
|
// ulElement is the index of the first element not included.
|
|
// If the last element in the run was included, then ulElement will
|
|
// be one more than that.
|
|
*pcElements = ulElement - ulStartElement;
|
|
}
|
|
|
|
return S_OK;
|
|
} /* CDictationRun::FindNearestWholeElementRange */
|
|
|
|
// Helper function
|
|
|
|
/*********************************************************************
|
|
* ContainsNonWhitespace() *
|
|
*-------------------------*
|
|
* Description:
|
|
* Returns true iff some character in the string is
|
|
* not whitespace.
|
|
**********************************************************************/
|
|
bool ContainsNonWhitespace( const WCHAR *pwsz )
|
|
{
|
|
if ( !pwsz )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const WCHAR *pwc;
|
|
for ( pwc = pwsz; *pwc; pwc++ )
|
|
{
|
|
if ( !iswspace( *pwc ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Did not hit a whitespace character
|
|
return false;
|
|
} /* ContainsNonWhitespace */
|