Skip to content

Commit 2673e5f

Browse files
authored
Merge 98b0d8f into 3c6bac1
2 parents 3c6bac1 + 98b0d8f commit 2673e5f

2 files changed

Lines changed: 112 additions & 20 deletions

File tree

source/displayModel.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
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.
65

76
import ctypes
87
from ctypes import *
@@ -25,6 +24,9 @@
2524
import textUtils
2625
from typing import Union, List, Tuple
2726

27+
COLOR_HIGHLIGHT = 13
28+
COLOR_HIGHLIGHTTEXT = 14
29+
2830
#: A text info unit constant for a single chunk in a display model
2931
UNIT_DISPLAYCHUNK = "displayChunk"
3032

@@ -252,33 +254,74 @@ class DisplayModelTextInfo(OffsetsTextInfo):
252254
includeDescendantWindows=True
253255

254256
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))
256258
return self.backgroundSelectionColor
257259

258260
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))
260262
return self.foregroundSelectionColor
261263

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()
263278
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
264287
fields=self._storyFieldsAndRects[0]
265288
startOffset=None
266289
endOffset=None
267290
curOffset=0
268-
inHighlightChunk=False
291+
inHighlightChunk = False
269292
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):
275318
curOffset += textUtils.WideStringOffsetConverter(item).wideStringLength
276319
if inHighlightChunk:
277-
endOffset=curOffset
320+
endOffset = curOffset
278321
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)
282325
raise LookupError
283326

284327
def __init__(self, obj, position,limitRect=None):

source/textInfos/__init__.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,62 @@
2727
from controlTypes import OutputReason
2828
import locationHelper
2929
from logHandler import log
30+
from typing import Union, List, Dict
3031

31-
32-
SpeechSequence = List[Union[Any, str]]
32+
FieldSubQuery = Dict[str, Union[bool, List]]
33+
FieldQuery = List[FieldSubQuery]
3334

3435
class Field(dict):
3536
"""Provides information about a piece of text."""
3637

38+
def evaluateQuery(self, query: FieldQuery) -> FieldSubQuery:
39+
"""Executes a query against self.
40+
41+
This function evaluates whether the provided query is met for this L{Field}.
42+
The argument to this function is a list of dicts whose keys are field attributes,
43+
and whose values are either:
44+
* A list of possible values for the attribute.
45+
* A boolean value, indicating that the condition for the key matches,
46+
i.e. whether or not the key is in the field.
47+
The dicts are joined with 'or', the keys in each dict are joined with 'and',
48+
and the values for each key are joined with 'or'.
49+
For example, to create a query that matches on a format field with a white or black foreground color,
50+
you would provide the following query argument:
51+
52+
[{'color': [colors.RGB(255, 255, 255), colors.RGB(0, 0, 0)]}]
53+
54+
To create a query that matches on a format field with whatever foreground color,
55+
you would provide the following query argument:
56+
57+
[{'color': True}]
58+
59+
To create a query that matches on a format field without a foreground color,
60+
you would provide the following query argument:
61+
62+
[{'color': False}]
63+
"""
64+
for sub in query:
65+
# Dicts are joined with or, therefore return early if a dict matches.
66+
for key, values in sub.items():
67+
if key not in self:
68+
if values is False:
69+
continue
70+
else:
71+
# Go to the next dict
72+
break
73+
elif values is True:
74+
continue
75+
if not isinstance(values, (list, set, tuple)):
76+
values = [values]
77+
if not self[key] in values:
78+
# Key does not match.
79+
break
80+
else:
81+
# This dict matches.
82+
return sub
83+
return None
84+
85+
3786
class FormatField(Field):
3887
"""Provides information about the formatting of text; e.g. font information and hyperlinks."""
3988

0 commit comments

Comments
 (0)