Skip to content

Commit 44ac474

Browse files
authored
Merge 210c9b3 into 3c6bac1
2 parents 3c6bac1 + 210c9b3 commit 44ac474

2 files changed

Lines changed: 117 additions & 18 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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,69 @@
2727
from controlTypes import OutputReason
2828
import locationHelper
2929
from logHandler import log
30+
from typing import Union, List, Dict
31+
from enum import Enum, auto
3032

3133

34+
class ConditionMatch(Enum):
35+
KEY_IN_FIELD = auto()
36+
KEY_NOT_IN_FIELD = auto()
37+
38+
39+
FieldSubQuery = Dict[str, Union[ConditionMatch, List]]
40+
FieldQuery = List[FieldSubQuery]
3241
SpeechSequence = List[Union[Any, str]]
3342

3443
class Field(dict):
3544
"""Provides information about a piece of text."""
3645

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

0 commit comments

Comments
 (0)