Skip to content

Commit

Permalink
implement a displayStringEnumMixin (#12510)
Browse files Browse the repository at this point in the history
- add displayStringEnumMixin
- deprecate roleLabels, stateLabels, negativeStateLabels

Co-authored-by: Leonard de Ruijter <leonardder@users.noreply.github.com>
  • Loading branch information
seanbudd and LeonarddeR committed Jun 18, 2021
1 parent 87828a7 commit 54f930b
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 57 deletions.
15 changes: 10 additions & 5 deletions source/controlTypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,19 @@
from .isCurrent import IsCurrent
from .outputReason import OutputReason
from .processAndLabelStates import processAndLabelStates, _processNegativeStates, _processPositiveStates
from .role import Role, roleLabels, silentRolesOnFocus, silentValuesForRoles
from .state import State, STATES_SORTED, negativeStateLabels, stateLabels
from .role import Role, silentRolesOnFocus, silentValuesForRoles, _roleLabels
from .state import State, STATES_SORTED, _negativeStateLabels, _stateLabels


__all__ = [
"IsCurrent",
"OutputReason",
"processAndLabelStates",
"Role",
"roleLabels",
"silentRolesOnFocus",
"silentValuesForRoles",
"State",
"STATES_SORTED",
"negativeStateLabels",
"stateLabels",
]


Expand All @@ -35,6 +32,14 @@
processPositiveStates = _processPositiveStates


# Added to maintain backwards compatibility, marked for deprecation to be removed in 2022.1
# usages to be replaced by Role.*.displayString and State.*.displayString
if version_year < 2022:
roleLabels = _roleLabels
stateLabels = _stateLabels
negativeStateLabels = _negativeStateLabels


# Added to maintain backwards compatibility, marked for deprecation to be removed in 2022.1
if version_year < 2022:
ROLE_UNKNOWN = Role.UNKNOWN
Expand Down
21 changes: 8 additions & 13 deletions source/controlTypes/isCurrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from enum import Enum
from typing import Dict

from logHandler import log
from utils.displayString import DisplayStringEnumMixin, DisplayStringEnumMixinMeta


class IsCurrent(Enum):
class IsCurrent(DisplayStringEnumMixin, str, Enum, metaclass=DisplayStringEnumMixinMeta):
"""Values to use within NVDA to denote 'current' values.
These describe if an item is the current item within a particular kind of selection.
EG aria-current
Expand All @@ -23,17 +23,12 @@ class IsCurrent(Enum):
TIME = "time"

@property
def displayString(self):
"""
@return: The translated UI display string that should be used for this value of the IsCurrent enum
"""
try:
return _isCurrentLabels[self]
except KeyError:
log.debugWarning(f"No translation mapping for: {self}")
# there is a value for 'current' but NVDA hasn't learned about it yet,
# at least describe in the general sense that this item is 'current'
return _isCurrentLabels[IsCurrent.YES]
def _displayStringLabels(self):
return _isCurrentLabels

@property
def defaultValue(self):
return self.YES


#: Text to use for 'current' values. These describe if an item is the current item
Expand Down
38 changes: 4 additions & 34 deletions source/controlTypes/processAndLabelStates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,11 @@
# See the file COPYING for more details.
# Copyright (C) 2007-2021 NV Access Limited, Babbage B.V.

from enum import Enum, auto
from typing import Dict, List, Optional, Set

from .role import Role, clickableRoles
from .state import State, STATES_SORTED, negativeStateLabels, stateLabels


class OutputReason(Enum):
"""Specify the reason that a given piece of output was generated.
"""
#: An object to be reported due to a focus change or similar.
FOCUS = auto()
#: An ancestor of the focus object to be reported due to a focus change or similar.
FOCUSENTERED = auto()
#: An item under the mouse.
MOUSE = auto()
#: A response to a user query.
QUERY = auto()
#: Reporting a change to an object.
CHANGE = auto()
#: A generic, screen reader specific message.
MESSAGE = auto()
#: Text reported as part of a say all.
SAYALL = auto()
#: Content reported due to caret movement or similar.
CARET = auto()
#: No output, but any state should be cached as if output had occurred.
ONLYCACHE = auto()

QUICKNAV = auto()
from .state import State, STATES_SORTED
from .outputReason import OutputReason


def _processPositiveStates(
Expand Down Expand Up @@ -205,12 +180,7 @@ def processAndLabelStates(
negativeStates = _processNegativeStates(role, states, reason, negativeStates)
for state in sorted(positiveStates | negativeStates):
if state in positiveStates:
mergedStateLabels.append(positiveStateLabelDict.get(state, stateLabels[state]))
mergedStateLabels.append(positiveStateLabelDict.get(state, state.displayString))
elif state in negativeStates:
# Translators: Indicates that a particular state of an object is negated.
# Separate strings have now been defined for commonly negated states (e.g. not selected and not
# checked), but this still might be used in some other cases.
# %s will be replaced with the full identifier of the negated state (e.g. selected).
negativeStateLabel = negativeStateLabels.get(state, _("not %s") % stateLabels[state])
mergedStateLabels.append(negativeStateLabelDict.get(state, negativeStateLabel))
mergedStateLabels.append(negativeStateLabelDict.get(state, state.negativeDisplayString))
return mergedStateLabels
14 changes: 12 additions & 2 deletions source/controlTypes/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@
from enum import IntEnum
from typing import Dict, Set

from utils.displayString import DisplayStringEnumMixin, DisplayStringEnumMixinMeta


class Role(DisplayStringEnumMixin, IntEnum, metaclass=DisplayStringEnumMixinMeta):
@property
def _displayStringLabels(self):
return _roleLabels

@property
def defaultValue(self):
return self.UNKNOWN

class Role(IntEnum):
UNKNOWN = 0
WINDOW = 1
TITLEBAR = 2
Expand Down Expand Up @@ -161,7 +171,7 @@ class Role(IntEnum):
MARKED_CONTENT = 153


roleLabels: Dict[Role, str] = {
_roleLabels: Dict[Role, str] = {
# Translators: The word for an unknown control type.
Role.UNKNOWN: _("unknown"),
# Translators: The word for window of a program such as document window.
Expand Down
31 changes: 28 additions & 3 deletions source/controlTypes/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,33 @@
from enum import IntEnum
from typing import Dict

from utils.displayString import DisplayStringEnumMixin, DisplayStringEnumMixinMeta


class State(DisplayStringEnumMixin, IntEnum, metaclass=DisplayStringEnumMixinMeta):
@property
def _displayStringLabels(self):
return _stateLabels

@property
def defaultValue(self):
return self.DEFUNCT

@property
def negativeDisplayString(self) -> str:
"""
@return: The translated UI display string that should be used for when referring to this value of
the enum in the negative.
"""
try:
return _negativeStateLabels[self]
except KeyError:
# Translators: Indicates that a particular state of an object is negated.
# Separate strings have now been defined for commonly negated states (e.g. not selected and not
# checked), but this still might be used in some other cases.
# %s will be replaced with the full identifier of the negated state (e.g. selected).
return _("not %s") % self.displayString

class State(IntEnum):
UNAVAILABLE = 0x1
FOCUSED = 0x2
SELECTED = 0x4
Expand Down Expand Up @@ -56,7 +81,7 @@ class State(IntEnum):
STATES_SORTED = frozenset([State.SORTED, State.SORTED_ASCENDING, State.SORTED_DESCENDING])


stateLabels: Dict[State, str] = {
_stateLabels: Dict[State, str] = {
# Translators: This is presented when a control or document is unavailable.
State.UNAVAILABLE: _("unavailable"),
# Translators: This is presented when a control has focus.
Expand Down Expand Up @@ -143,7 +168,7 @@ class State(IntEnum):
}


negativeStateLabels: Dict[State, str] = {
_negativeStateLabels: Dict[State, str] = {
# Translators: This is presented when a selectable object (e.g. a list item) is not selected.
State.SELECTED: _("not selected"),
# Translators: This is presented when a button is not pressed.
Expand Down
Empty file added source/utils/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions source/utils/displayString.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2021 NV Access Limited.

from abc import ABC, ABCMeta, abstractproperty
from enum import Enum, EnumMeta
from typing import Dict

from logHandler import log


class DisplayStringEnumMixinMeta(ABCMeta, EnumMeta):
"""
This helps correct the Method Resolution Order (MRO) when using the `DisplayStringEnumMixin`.
When creating an Enum with a Mixin, Python suggest an ordering of
`class EnumWithMixin(Mixin, type, EnumClass):`.
This creates a metaclass conflict as both DisplayStringEnumMixin and Enum both have metaclasses,
ABCMeta and EnumMeta. This requires a new MetaClass which subclasses both of these. This follows the
same ordering of the EnumWithMixin usage.
See `DisplayStringEnumMixin`.
"""
pass


class DisplayStringEnumMixin(ABC):
"""
This mixin can be used with a class which subclasses Enum to provided translated display strings for
members of the enum. The abstract properties must be overridden.
To be used with `DisplayStringEnumMixinMeta`.
Usage for python 3.7 is as follows:
```
class ExampleEnum(DisplayStringEnumMixin, str, Enum, metaclass=DisplayStringEnumMixinMeta):
pass
class ExampleIntEnum(DisplayStringEnumMixin, IntEnum, metaclass=DisplayStringEnumMixinMeta):
pass
```
"""
@abstractproperty
def _displayStringLabels(self) -> Dict[Enum, str]:
"""
Specify a dictionary which takes members of the Enum and returns the translated display string.
"""
pass

@abstractproperty
def defaultValue(self) -> Enum:
"""
Specify an Enum member with a known translated display string to use as a default if there is no known
display string for a given member in _displayStringLabels.
"""
pass

@property
def displayString(self) -> str:
"""
@return: The translated UI display string that should be used for this value of the enum.
"""
try:
return self._displayStringLabels[self]
except KeyError:
log.error(f"No translation mapping for: {self}")
return self._displayStringLabels[self.defaultValue]

0 comments on commit 54f930b

Please sign in to comment.