Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 85 additions & 7 deletions addon/globalPlugins/text_information/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

addonHandler.initTranslation()
import treeInterceptorHandler
import gui
import gui.settingsDialogs
import scriptHandler
import tones
import threading
Expand All @@ -26,10 +28,24 @@
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
import isbn
import definitions
import providers
import settings
from bs4 import BeautifulSoup

sys.path.remove(sys.path[-1])

settings.initialize()


def resolve_audio_fallback(word):
"""Pronunciation urls to try when the source's own recording cannot be played."""
if not settings.should_use_fallback():
return []
return [url for label, url in providers.get_commons_audio(word)]


definitions.audio_fallback_resolver = resolve_audio_fallback


# taken and partially modified from http://code.activestate.com/recipes/578019
def bytes2human(n):
Expand Down Expand Up @@ -261,13 +277,32 @@ def get_book_info(isbn):
ui.message(last)


def get_word_info(word):
def lookup_word(text):
"""Looks `text` up, trying each form of the word against each configured source.

Returns the normalised entry and the word that produced it, or (None, None).
Only the very last attempt reports its failure, so the user hears a single outcome.
"""
sources = [settings.get_definition_provider()]
if settings.should_use_fallback():
fallback = providers.FALLBACK_PROVIDER
if fallback not in sources:
sources.append(fallback)
attempts = [(source, word) for source in sources for word in word_lookup_candidates(text)]
for index, (source, word) in enumerate(attempts):
entry = source.fetch(word, reportErrors=index == len(attempts) - 1)
if entry is not None:
return entry, word
return None, None


def get_word_info(text):
global last, lastAudio, lastWord, lastIsWordDefinition
entry = definitions.fetch_word_entry(word)
entry, word = lookup_word(text)
if entry is None:
return
fields = []
phonetic = definitions.get_entry_phonetic(entry)
phonetic = entry.get("phonetic")
if phonetic:
# translators: label for the phonetic pronunciation of a word
fields.append(_("pronunciation") + ": " + phonetic)
Expand All @@ -277,11 +312,17 @@ def get_word_info(word):
ui.message(_("unable to find definition for word"))
return
tones.beep(300, 200)
lastAudio = definitions.get_entry_audio(entry)
lastAudio = entry.get("audio", [])
lastWord = word
lastIsWordDefinition = True
last = ". ".join(fields)
ui.message(last)
# The chosen dictionary may not carry pronunciation audio at all, in which case looking
# elsewhere for it is part of what the fallback option asks for. That is a request, so it
# happens once the definition has been spoken rather than delaying it. The dialog that
# uses the audio needs a second keypress, which takes longer than the lookup does.
if not lastAudio and settings.should_use_fallback():
lastAudio = providers.get_commons_audio(word)


def get_url_info(addr, timeout=10):
Expand Down Expand Up @@ -356,15 +397,50 @@ def word_count(string):
return len(string.split())


# Strips punctuation from the ends of a word only, so internal apostrophes and hyphens
# (let's, so-so) survive. \W is unicode aware, which also catches smart quotes and dashes.
SURROUNDING_PUNCTUATION = re.compile(r"^[\W_]+|[\W_]+$")


def strip_surrounding_punctuation(text):
return SURROUNDING_PUNCTUATION.sub("", text)


def word_lookup_candidates(text):
"""Forms of `text` to look up, in order of preference.

The stripped form comes first, since selected text usually carries adjacent punctuation.
The original is kept as a fallback for words where the punctuation is part of the
word itself, eg. 'tis or e.g.
"""
stripped = strip_surrounding_punctuation(text)
# Nothing but punctuation or symbols, so there is no word here to define
if not stripped:
return []
candidates = [stripped]
if text != stripped:
candidates.append(text)
return candidates


class GlobalPlugin(globalPluginHandler.GlobalPlugin):

scriptCategory = _("Text Information")

def __init__(self, *args, **kwargs):
super(GlobalPlugin, self).__init__(*args, **kwargs)
gui.settingsDialogs.NVDASettingsDialog.categoryClasses.append(
settings.TextInformationSettingsPanel
)

def terminate(self, *args, **kwargs):
definitions.shutdown_audio_worker()
try:
gui.settingsDialogs.NVDASettingsDialog.categoryClasses.remove(
settings.TextInformationSettingsPanel
)
except ValueError:
log.debug("Settings panel was already removed")
super(GlobalPlugin, self).terminate(*args, **kwargs)

def script_getClipInfo(self, gesture):
Expand Down Expand Up @@ -416,7 +492,9 @@ def script_getLast(self, gesture):
if lastIsWordDefinition:
# translators: title of the dialog showing a word's definition and pronunciation audio buttons
title = _("Definition for {0}").format(lastWord)
definitions.show_audio_browseable_message("\n".join(last.split(". ")), title, lastAudio)
definitions.show_audio_browseable_message(
"\n".join(last.split(". ")), title, lastAudio, lastWord
)
else:
ui.browseableMessage("\n".join(last.split(". ")), "text information")
else:
Expand Down Expand Up @@ -459,10 +537,10 @@ def get_info(self, text):
# translators: message spoken after selecting text that contains a URL
final += _("URL, retrieving page information...")
threading.Thread(target=get_url_info, args=(text,)).start()
elif w == 1:
elif w == 1 and word_lookup_candidates(text):
# translators: message spoken after selecting text that contains a word (will be defined)
final += _("retrieving word information...")
t = threading.Thread(target=get_word_info, args=(text,)).start()
threading.Thread(target=get_word_info, args=(text,)).start()
if not final:
final += "text contains " + str(w) + (" words" if w != 1 else " word")
ui.message(final)
Expand Down
Loading