Permalink
Browse files

Speed ups to ms word object model (#9217)

* winword support in nvdaHelper: speed things up a bit by optimizing for only one layout column, remove some debugging code, and turn off screenUpdating while collecting text formatting.

* Winword support in nvdaHelper: fetch editor revision information much more efficiently, similarly to how we fetch comments and spelling errors.

* MS Word object model support: rather than disabling screen updating specifically from within each in-process call, disable it from Python the first time it is required in a core cycle, re-enabling it at the end of the core cycle.

* speech.speakTextInfo: if the unit is paragraph, cell or story, don't fetch costly formatting info such as spelling errors and editor revisions, even if the user has these turned on.

* NVDAHelper MS Word support: add more debug warning log statements.

* Revert "speech.speakTextInfo: if the unit is paragraph, cell or story, don't fetch costly formatting info such as spelling errors and editor revisions, even if the user has these turned on."

This reverts commit 6ea9d0f.

* When navigating by table cell in MS Word, only speak the first line of the next cell, otherwise it may take a very long time to collect all content for the cell.

* When navigating by paragraph, spelling errors and editor revisions will not be announced. It is important that navigating by paragraph is performant as it is used for quick skimming.

* MS Word: Optimize collapse so that end is only fetched if needed.

* Navigating by table cell in MS Word again speaks the entire cell, but editor revisions and spelling errors (like when navigating by paragraph) are no longer spoken for navigating by table cell.

* Only disable spelling errors when speaking paragraphs and table cells. Editor revisions are okay.

* MS Word nvdaHelper support: remove unneeded fetching of application object.

* MS Word support: remove old comment.

* MS Word support: go back to disabling screen updating in-process for now, but do it from a RAAI class so that it can be re-enabled automatically at the end of scope.

* Address review comments.

* Update what's new.
  • Loading branch information...
michaelDCurran committed Feb 11, 2019
1 parent 1a45aeb commit 960983e41a4321411e54bd5a7b2f205a70ae7600
@@ -121,8 +121,10 @@ constexpr int wdDISPID_RANGE_STORYTYPE = 7;
constexpr int wdDISPID_RANGE_STYLE = 151;
constexpr int wdDISPID_RANGE_TABLES = 50;
constexpr int wdDISPID_RANGE_TEXT = 0;
constexpr int wdDISPID_REVISION_RANGE = 3;
constexpr int wdDISPID_REVISION_TYPE = 4;
constexpr int wdDISPID_REVISIONS_ITEM = 0;
constexpr int wdDISPID_REVISIONS_COUNT = 5;
constexpr int wdDISPID_ROWS_COUNT = 2;
constexpr int wdDISPID_SECTIONS_COUNT = 2;
constexpr int wdDISPID_SECTIONS_ITEM = 0;
@@ -58,6 +58,35 @@ constexpr int formatConfig_initialFormatFlags =(formatConfig_reportPage|formatCo
constexpr wchar_t PAGE_BREAK_VALUE = L'\x0c';
constexpr wchar_t COLUMN_BREAK_VALUE = L'\x0e';

// A class that disables MS Word screen updating while it is alive.
class ScreenUpdatingDisabler {
private:
IDispatchPtr pDispatchApplication {nullptr};

public:
ScreenUpdatingDisabler(IDispatch* arg_pDispatchApplication) {
if(!arg_pDispatchApplication) {
LOG_ERROR(L"Null application given");
return;
}
HRESULT res=_com_dispatch_raw_propput(arg_pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,false);
if(res!=S_OK) {
LOG_ERROR(L"application.screenUpdating false failed with code "<<res);
return;
}
pDispatchApplication=arg_pDispatchApplication;
}

~ScreenUpdatingDisabler() {
if(!pDispatchApplication) return;
HRESULT res=_com_dispatch_raw_propput(pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,true);
if(res!=S_OK) {
LOG_ERROR(L"application.screenUpdating true failed with code "<<res);
}
}

};

UINT wm_winword_expandToLine=0;
typedef struct {
int offset;
@@ -71,12 +100,14 @@ void winword_expandToLine_helper(HWND hwnd, winword_expandToLine_args* args) {
LOG_DEBUGWARNING(L"AccessibleObjectFromWindow failed");
return;
}
IDispatchPtr pDispatchApplication=NULL;
IDispatchPtr pDispatchApplication=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_APPLICATION,VT_DISPATCH,&pDispatchApplication)!=S_OK||!pDispatchApplication) {
LOG_DEBUGWARNING(L"window.application failed");
return;
}
IDispatchPtr pDispatchSelection=NULL;
// Disable screen updating until the end of this scope
ScreenUpdatingDisabler sud{pDispatchApplication};
IDispatchPtr pDispatchSelection=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_SELECTION,VT_DISPATCH,&pDispatchSelection)!=S_OK||!pDispatchSelection) {
LOG_DEBUGWARNING(L"application.selection failed");
return;
@@ -90,8 +121,6 @@ void winword_expandToLine_helper(HWND hwnd, winword_expandToLine_args* args) {
LOG_DEBUGWARNING(L"selection.range failed");
return;
}
//Disable screen updating as we will be moving the selection temporarily
_com_dispatch_raw_propput(pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,false);
//Move the selection to the given range
_com_dispatch_raw_method(pDispatchSelection,wdDISPID_SELECTION_SETRANGE,DISPATCH_METHOD,VT_EMPTY,NULL,L"\x0003\x0003",args->offset,args->offset);
//Expand the selection to the line
@@ -126,8 +155,6 @@ void winword_expandToLine_helper(HWND hwnd, winword_expandToLine_args* args) {
_com_dispatch_raw_method(pDispatchOldSelRange,wdDISPID_RANGE_SELECT,DISPATCH_METHOD,VT_EMPTY,NULL,NULL);
//Restore the old selection direction
_com_dispatch_raw_propput(pDispatchSelection,wdDISPID_SELECTION_STARTISACTIVE,VT_BOOL,startWasActive);
//Reenable screen updating
_com_dispatch_raw_propput(pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,true);
}

BOOL generateFormFieldXML(IDispatch* pDispatchRange, IDispatchPtr pDispatchRangeExpandedToParagraph, wostringstream& XMLStream, int& chunkEnd) {
@@ -288,25 +315,6 @@ int generateHeadingXML(IDispatch* pDispatchParagraph, IDispatch* pDispatchParagr
return 1;
}

int getRevisionType(IDispatch* pDispatchOrigRange) {
IDispatchPtr pDispatchRange=NULL;
//If range is not duplicated here, revisions collection represents revisions at the start of the range when it was first created
if(_com_dispatch_raw_propget(pDispatchOrigRange,wdDISPID_RANGE_DUPLICATE,VT_DISPATCH,&pDispatchRange)!=S_OK||!pDispatchRange) {
return 0;
}
IDispatchPtr pDispatchRevisions=NULL;
if(_com_dispatch_raw_propget(pDispatchRange,wdDISPID_RANGE_REVISIONS,VT_DISPATCH,&pDispatchRevisions)!=S_OK||!pDispatchRevisions) {
return 0;
}
IDispatchPtr pDispatchRevision=NULL;
if(_com_dispatch_raw_method(pDispatchRevisions,wdDISPID_REVISIONS_ITEM,DISPATCH_METHOD,VT_DISPATCH,&pDispatchRevision,L"\x0003",1)!=S_OK||!pDispatchRevision) {
return 0;
}
long revisionType=0;
_com_dispatch_raw_propget(pDispatchRevision,wdDISPID_REVISION_TYPE,VT_I4,&revisionType);
return revisionType;
}

IDispatchPtr CreateExpandedDuplicate(IDispatch* pDispatchRange, const int expandTo) {
IDispatchPtr pDispatchRangeDup = nullptr;
auto res = _com_dispatch_raw_propget( pDispatchRange, wdDISPID_RANGE_DUPLICATE, VT_DISPATCH, &pDispatchRangeDup);
@@ -351,6 +359,51 @@ bool collectCommentOffsets(IDispatchPtr pDispatchRange, vector<pair<long,long>>&
return !commentVector.empty();
}

bool collectRevisionOffsets(IDispatchPtr pDispatchRange, vector<tuple<long,long,long>>& revisionsVector) {
IDispatchPtr pDispatchRevisions=nullptr;
HRESULT res=_com_dispatch_raw_propget(pDispatchRange,wdDISPID_RANGE_REVISIONS,VT_DISPATCH,&pDispatchRevisions);
if(res!=S_OK||!pDispatchRevisions) {
LOG_DEBUGWARNING(L"range.revisions failed");
return false;
}
long iVal=0;
_com_dispatch_raw_propget(pDispatchRevisions,wdDISPID_REVISIONS_COUNT,VT_I4,&iVal);
for(int i=1;i<=iVal;++i) {
IDispatchPtr pDispatchRevision=nullptr;
res=_com_dispatch_raw_method(pDispatchRevisions,wdDISPID_REVISIONS_ITEM,DISPATCH_METHOD,VT_DISPATCH,&pDispatchRevision,L"\x0003",i);
if(res!=S_OK||!pDispatchRevision) {
LOG_DEBUGWARNING(L"revisions.item "<<i<<L" failed");
continue;
}
long revisionType=0;
res=_com_dispatch_raw_propget(pDispatchRevision,wdDISPID_REVISION_TYPE,VT_I4,&revisionType);
if(res!=S_OK) {
LOG_DEBUGWARNING(L"revision.type failed");
continue;
}
IDispatchPtr pDispatchRevisionScope=nullptr;
_com_dispatch_raw_propget(pDispatchRevision,wdDISPID_REVISION_RANGE,VT_DISPATCH,&pDispatchRevisionScope);
if(res!=S_OK||!pDispatchRevisionScope) {
LOG_DEBUGWARNING(L"revision.range failed");
continue;
}
long start=0;
res=_com_dispatch_raw_propget(pDispatchRevisionScope,wdDISPID_RANGE_START,VT_I4,&start);
if(res!=S_OK) {
LOG_DEBUGWARNING(L"range.start failed");
continue;
}
long end=0;
res=_com_dispatch_raw_propget(pDispatchRevisionScope,wdDISPID_RANGE_END,VT_I4,&end);
if(res!=S_OK) {
LOG_DEBUGWARNING(L"range.end failed");
continue;
}
revisionsVector.push_back({start,end,revisionType});
}
return !revisionsVector.empty();
}

bool fetchTableInfo(IDispatch* pDispatchTable, bool includeLayoutTables, int* rowCount, int* columnCount, int* nestingLevel) {
IDispatchPtr pDispatchRows=NULL;
IDispatchPtr pDispatchColumns=NULL;
@@ -546,10 +599,6 @@ void generateXMLAttribsForFormatting(IDispatch* pDispatchRange, int startOffset,
}
}
}
if(formatConfig&formatConfig_reportRevisions) {
long revisionType=getRevisionType(pDispatchRange);
formatAttribsStream<<L"wdRevisionType=\""<<revisionType<<L"\" ";
}
if(formatConfig&formatConfig_reportStyle) {
IDispatchPtr pDispatchStyle=NULL;
if(_com_dispatch_raw_propget(pDispatchRange,wdDISPID_RANGE_STYLE,VT_DISPATCH,&pDispatchStyle)==S_OK&&pDispatchStyle) {
@@ -877,6 +926,14 @@ void detectAndGenerateColumnFormatXML(IDispatchPtr pDispatchRange, wostringstrea
}
xmlStream <<L"text-column-count=\"" << count << "\" ";

// Optimization: If there is only one column, then we can only be in that column.
if(count<=1) {
if(count==1) {
xmlStream <<L"text-column-number=\"" << 1 << "\" ";
}
return;
}

// 2. Get the start position (IN POINTS) of the range. This is relative to the start
// of the document.
auto rangeStart = getStartOfRangeDistanceFromEdgeOfDocument(pDispatchRange);
@@ -945,7 +1002,8 @@ void detectAndGenerateColumnFormatXML(IDispatchPtr pDispatchRange, wostringstrea
}
xmlStream <<L"text-column-number=\"" << columnNumber << "\" ";

// Finally, double check that we calculated the full width of the document
/*
The following commented out code to the end of this function can be used to double check that we calculated the full width of the document
float pageWidth = -1.0f;
res = _com_dispatch_raw_propget( pDispatchPageSetup, wdDISPID_PAGESETUP_PAGEWIDTH,
VT_R4, &pageWidth);
@@ -966,6 +1024,7 @@ void detectAndGenerateColumnFormatXML(IDispatchPtr pDispatchRange, wostringstrea
<< " that some column numbers reported were incorrect."
<< " pageWidth: " << pageWidth << " colStartPos: " << colStartPos);
}
*/
}

UINT wm_winword_getTextInRange=0;
@@ -983,8 +1042,15 @@ void winword_getTextInRange_helper(HWND hwnd, winword_getTextInRange_args* args)
LOG_DEBUGWARNING(L"AccessibleObjectFromWindow failed");
return;
}
//Get the current selection
IDispatchPtr pDispatchSelection=NULL;
IDispatchPtr pDispatchApplication=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_APPLICATION,VT_DISPATCH,&pDispatchApplication)!=S_OK||!pDispatchApplication) {
LOG_DEBUGWARNING(L"window.application failed");
return;
}
// Disable screen updating until the end of this scope
ScreenUpdatingDisabler sud{pDispatchApplication};
//Get the current selection
IDispatchPtr pDispatchSelection=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_SELECTION,VT_DISPATCH,&pDispatchSelection)!=S_OK||!pDispatchSelection) {
LOG_DEBUGWARNING(L"application.selection failed");
return;
@@ -1048,6 +1114,10 @@ void winword_getTextInRange_helper(HWND hwnd, winword_getTextInRange_args* args)
if(formatConfig&formatConfig_reportComments) {
collectCommentOffsets(pDispatchParagraphRange,commentVector);
}
vector<tuple<long,long,long>> revisionsVector;
if(formatConfig&formatConfig_reportRevisions) {
collectRevisionOffsets(pDispatchParagraphRange,revisionsVector);
}
if(initialFormatConfig&formatConfig_reportHeadings) {
neededClosingControlTagCount+=generateHeadingXML(pDispatchParagraph,pDispatchParagraphRange,args->startOffset,args->endOffset,XMLStream);
}
@@ -1190,6 +1260,15 @@ void winword_getTextInRange_helper(HWND hwnd, winword_getTextInRange_args* args)
break;
}
}
for(auto& i: revisionsVector) {
long revStart=get<0>(i);
long revEnd=get<1>(i);
long revType=get<2>(i);
if(!(chunkStartOffset>=revEnd||chunkEndOffset<=revStart)) {
XMLStream<<L"wdRevisionType=\""<<revType<<L"\" ";
break;
}
}
XMLStream<<L">";
if(firstLoop) {
formatConfig&=(~formatConfig_reportLists);
@@ -1231,12 +1310,14 @@ void winword_moveByLine_helper(HWND hwnd, winword_moveByLine_args* args) {
LOG_DEBUGWARNING(L"AccessibleObjectFromWindow failed");
return;
}
IDispatchPtr pDispatchApplication=NULL;
IDispatchPtr pDispatchApplication=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_APPLICATION,VT_DISPATCH,&pDispatchApplication)!=S_OK||!pDispatchApplication) {
LOG_DEBUGWARNING(L"window.application failed");
return;
}
IDispatchPtr pDispatchSelection=NULL;
// Disable screen updating until the end of this scope
ScreenUpdatingDisabler sud{pDispatchApplication};
IDispatchPtr pDispatchSelection=nullptr;
if(_com_dispatch_raw_propget(pDispatchWindow,wdDISPID_WINDOW_SELECTION,VT_DISPATCH,&pDispatchSelection)!=S_OK||!pDispatchSelection) {
LOG_DEBUGWARNING(L"application.selection failed");
return;
@@ -1250,8 +1331,6 @@ void winword_moveByLine_helper(HWND hwnd, winword_moveByLine_args* args) {
LOG_DEBUGWARNING(L"selection.range failed");
return;
}
//Disable screen updating as we will be moving the selection temporarily
_com_dispatch_raw_propput(pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,false);
//Move the selection to the given range
_com_dispatch_raw_method(pDispatchSelection,wdDISPID_SELECTION_SETRANGE,DISPATCH_METHOD,VT_EMPTY,NULL,L"\x0003\x0003",args->offset,args->offset);
// Move the selection by 1 line
@@ -1263,7 +1342,6 @@ void winword_moveByLine_helper(HWND hwnd, winword_moveByLine_args* args) {
//Restore the old selection direction
_com_dispatch_raw_propput(pDispatchSelection,wdDISPID_SELECTION_STARTISACTIVE,VT_BOOL,startWasActive);
//Reenable screen updating
_com_dispatch_raw_propput(pDispatchApplication,wdDISPID_APPLICATION_SCREENUPDATING,VT_BOOL,true);
}

LRESULT CALLBACK winword_callWndProcHook(int code, WPARAM wParam, LPARAM lParam) {
@@ -339,7 +339,7 @@ def _moveInTable(self,row=True,forward=True):
ui.message(_("Edge of table"))
return False
newInfo=WordDocumentTextInfo(self,textInfos.POSITION_CARET,_rangeObj=foundCell)
speech.speakTextInfo(newInfo,reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_PARAGRAPH)
speech.speakTextInfo(newInfo,reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_CELL)
newInfo.collapse()
newInfo.updateCaret()
return True
@@ -927,12 +927,13 @@ def collapse(self,end=False):
if end:
oldEndOffset=self._rangeObj.end
self._rangeObj.collapse(wdCollapseEnd if end else wdCollapseStart)
newEndOffset = self._rangeObj.end
# the new endOffset should not have become smaller than the old endOffset, this could cause an infinite loop in
# a case where you called move end then collapse until the size of the range is no longer being reduced.
# For an example of this see sayAll (specifically readTextHelper_generator in sayAllHandler.py)
if end and newEndOffset < oldEndOffset :
raise RuntimeError
if end:
newEndOffset = self._rangeObj.end
# the new endOffset should not have become smaller than the old endOffset, this could cause an infinite loop in
# a case where you called move end then collapse until the size of the range is no longer being reduced.
# For an example of this see sayAll (specifically readTextHelper_generator in sayAllHandler.py)
if newEndOffset < oldEndOffset :
raise RuntimeError

def copy(self):
return WordDocumentTextInfo(self.obj,None,_rangeObj=self._rangeObj)
@@ -766,10 +766,13 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
extraDetail=unit in (textInfos.UNIT_CHARACTER,textInfos.UNIT_WORD)
if not formatConfig:
formatConfig=config.conf["documentFormatting"]
formatConfig=formatConfig.copy()
if extraDetail:
formatConfig=formatConfig.copy()
formatConfig['extraDetail']=True
reportIndentation=unit==textInfos.UNIT_LINE and ( formatConfig["reportLineIndentation"] or formatConfig["reportLineIndentationWithTones"])
# For performance reasons, when navigating by paragraph or table cell, spelling errors will not be announced.
if unit in (textInfos.UNIT_PARAGRAPH,textInfos.UNIT_CELL) and reason is controlTypes.REASON_CARET:
formatConfig['reportSpellingErrors']=False

speechSequence=[]
#Fetch the last controlFieldStack, or make a blank one
@@ -30,8 +30,9 @@ What's New in NVDA
- Reporting of text under the mouse has been improved within Microsoft Edge and other UIA applications. (#8370)
- When NVDA is started with the `--portable-path` command line parameter, the provided path is automatically filled in when trying to create a portable copy of NVDA using the NVDA menu. (#8623)
- Updated the path to the Norwegian braille table to reflect the standard from the year 2015. (#9170)


- When navigating by paragraph (control+arrows) or navigating by table cell (control+alt+arrows), the existence of spelling errors will no longer be announced, even if NVDA is configured to announce these automatically. This is because paragraphs and table cells can be quite large, and calculating spelling errors in some applications can be very costly. (#9217)


== Bug Fixes ==
- OneCore speech synthesizer: On Windows 10 April 2018 and above, large chunks of silence are no longer inserted between speech utterances. (#8985)
- When moving by character in plain text controls (such as Notepad) or browse mode, 32 bit emoji characters consisting of two UTF-16 code points (such as 🤦) will now read properly. (#8782)
@@ -54,6 +55,7 @@ What's New in NVDA
- In Windows 10 October 2018 Update and later, when opening cloud clipboard history with clipboard empty, NVDA will announce clipboard status. (#9112)
- In Windows 10 October 2018 Update and later, when searching for emojis in emoji panel, NVDA will announce top search result. (#9112)
- NVDA no longer freezes in the mainwindow of Virtualbox 5.2 and above. (#9202)
- Responsiveness in Microsoft Word when navigating by line, paragraph or table cell may be significantly improved in some documents. A reminder that for best performance, set Microsoft Word to Draft view with alt+w,e after opening a document. (#9217)


== Changes for Developers ==

0 comments on commit 960983e

Please sign in to comment.