|
1 | | -#displayModel.py |
2 | | -#A part of NonVisual Desktop Access (NVDA) |
3 | | -#This file is covered by the GNU General Public License. |
4 | | -#See the file COPYING for more details. |
5 | | -#Copyright (C) 2006-2017 NV Access Limited, Babbage B.V. |
| 1 | +# A part of NonVisual Desktop Access (NVDA) |
| 2 | +# This file is covered by the GNU General Public License. |
| 3 | +# See the file COPYING for more details. |
| 4 | +# Copyright (C) 2006-2019 NV Access Limited, Babbage B.V. |
6 | 5 |
|
7 | 6 | import ctypes |
8 | 7 | from ctypes import * |
|
25 | 24 | import textUtils |
26 | 25 | from typing import Union, List, Tuple |
27 | 26 |
|
| 27 | +COLOR_HIGHLIGHT = 13 |
| 28 | +COLOR_HIGHLIGHTTEXT = 14 |
| 29 | + |
28 | 30 | #: A text info unit constant for a single chunk in a display model |
29 | 31 | UNIT_DISPLAYCHUNK = "displayChunk" |
30 | 32 |
|
@@ -252,33 +254,74 @@ class DisplayModelTextInfo(OffsetsTextInfo): |
252 | 254 | includeDescendantWindows=True |
253 | 255 |
|
254 | 256 | def _get_backgroundSelectionColor(self): |
255 | | - self.backgroundSelectionColor=colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(13)) |
| 257 | + self.backgroundSelectionColor = colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(COLOR_HIGHLIGHT)) |
256 | 258 | return self.backgroundSelectionColor |
257 | 259 |
|
258 | 260 | def _get_foregroundSelectionColor(self): |
259 | | - self.foregroundSelectionColor=colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(14)) |
| 261 | + self.foregroundSelectionColor = colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(COLOR_HIGHLIGHTTEXT)) |
260 | 262 | return self.foregroundSelectionColor |
261 | 263 |
|
262 | | - def _getSelectionOffsets(self): |
| 264 | + def _get_selectionQuery(self) -> textInfos.FieldQuery: |
| 265 | + """The search query for selections. |
| 266 | +
|
| 267 | + It is applied to a L{textInfos.FieldCommand} when searching for selected or highlighted text. |
| 268 | + A query is a list with dicts whose keys are control field attributes, |
| 269 | + and whose values are either: |
| 270 | + * A list of possible values for the attribute. |
| 271 | + * A boolean value, indicating that the condition for the key matches, |
| 272 | + i.e. whether or not the key is in the field. |
| 273 | + The dicts are joined with 'or', the keys in each dict are joined with 'and', |
| 274 | + and the values for each key are joined with 'or'. |
| 275 | + It is evaluated using L{textInfos.Field.evaluateQuery}. |
| 276 | + """ |
| 277 | + defaultSubQuery: textInfos.FieldSUbQuery = dict() |
263 | 278 | if self.backgroundSelectionColor is not None and self.foregroundSelectionColor is not None: |
| 279 | + defaultSubQuery['color'] = [self.foregroundSelectionColor] |
| 280 | + defaultSubQuery['background-color'] = [self.backgroundSelectionColor] |
| 281 | + return [defaultSubQuery] |
| 282 | + |
| 283 | + def _getSelectionOffsets(self): |
| 284 | + query = self.selectionQuery |
| 285 | + if query: |
| 286 | + highlightDict = None |
264 | 287 | fields=self._storyFieldsAndRects[0] |
265 | 288 | startOffset=None |
266 | 289 | endOffset=None |
267 | 290 | curOffset=0 |
268 | | - inHighlightChunk=False |
| 291 | + inHighlightChunk = False |
269 | 292 | for item in fields: |
270 | | - if isinstance(item,textInfos.FieldCommand) and item.command=="formatChange" and item.field.get('color',None)==self.foregroundSelectionColor and item.field.get('background-color',None)==self.backgroundSelectionColor: |
271 | | - inHighlightChunk=True |
272 | | - if startOffset is None: |
273 | | - startOffset=curOffset |
274 | | - elif isinstance(item,str): |
| 293 | + if isinstance(item, textInfos.FieldCommand) and item.command == "formatChange": |
| 294 | + # If we are able to evaluate text against a field query, |
| 295 | + # We can limit our future evaluations to the sub query that matches. |
| 296 | + # This makes sure that we only apply the first matching sub query |
| 297 | + # to the highlight searching strategy. |
| 298 | + if not highlightDict: |
| 299 | + evaluation = item.field.evaluateQuery(self.selectionQuery) |
| 300 | + if evaluation: |
| 301 | + highlightDict = evaluation |
| 302 | + else: |
| 303 | + evaluation = item.field.evaluateQuery([highlightDict]) |
| 304 | + if not evaluation: |
| 305 | + # The highlight dict does not match, but we're dealing with format changes |
| 306 | + # The highlight chunk ends if we encounter another format change that contains the keys. |
| 307 | + # Execute a negative evaluation. |
| 308 | + evaluation = item.field.evaluateQuery([ |
| 309 | + {key: False for key in highlightDict.keys()} |
| 310 | + ]) |
| 311 | + if evaluation: |
| 312 | + inHighlightChunk = True |
| 313 | + if startOffset is None: |
| 314 | + startOffset = curOffset |
| 315 | + else: |
| 316 | + inHighlightChunk = False |
| 317 | + elif isinstance(item, str): |
275 | 318 | curOffset += textUtils.WideStringOffsetConverter(item).wideStringLength |
276 | 319 | if inHighlightChunk: |
277 | | - endOffset=curOffset |
| 320 | + endOffset = curOffset |
278 | 321 | else: |
279 | | - inHighlightChunk=False |
280 | | - if startOffset is not None and endOffset is not None: |
281 | | - return (startOffset,endOffset) |
| 322 | + inHighlightChunk = False |
| 323 | + if not inHighlightChunk and startOffset is not None and endOffset is not None: |
| 324 | + return (startOffset, endOffset) |
282 | 325 | raise LookupError |
283 | 326 |
|
284 | 327 | def __init__(self, obj, position,limitRect=None): |
|
0 commit comments