Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: optionally start logging at NVDA startup #10

Merged
merged 5 commits into from
Aug 31, 2023
Merged
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
83 changes: 66 additions & 17 deletions addon/globalPlugins/speechLogger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# NVDA Speech Logger add-on, V23.2
# NVDA Speech Logger add-on, V23.3
#
# Copyright (C) 2022-2023 Luke Davis <XLTechie@newanswertech.com>
# Initially based on code ideas suggested by James Scholes.
Expand Down Expand Up @@ -46,13 +46,18 @@
from scriptHandler import script
from logHandler import log
from globalCommands import SCRCAT_TOOLS, SCRCAT_CONFIG
from core import postNvdaStartup

from .configUI import SpeechLoggerSettings, getConf
from .immutableKeyObj import ImmutableKeyObj
from . import extensionPoint

addonHandler.initTranslation()

try:
addonHandler.initTranslation()
except addonHandler.AddonError:
log.error(
"Attempted to initialize translations in an inappropriate context. May be running from scratchpad."
)

@unique
class Origin(Enum):
Expand Down Expand Up @@ -104,7 +109,11 @@ def __init__(self):
# Should we log the timestamp when we start/stop a log session?
startStopTimestamps=True,
# Should we log during Say All/Read To End?
logSayAll = True
logSayAll=True,
# Should we start logging when launched?
logAtStartup=False,
# Becomes True if we were initially set to log at startup
loggedAtStartup=False
)
#: Filenames are obtained from NVDA configuration, and setup in applyUserConfig().
self.files: ImmutableKeyObj = ImmutableKeyObj(local=None, remote=None)
Expand All @@ -124,6 +133,10 @@ def __init__(self):
# If we are supposed to rotate logs, do that now.
if self.flags.rotate:
self.rotateLogs()
# If we are supposed to start logging at NVDA startup, register a handler for that
if self.flags.logAtStartup:
postNvdaStartup.register(self.startLocalLog)
self.flags.loggedAtStartup = True
# Wrap speech.speech.speak, so we can get its output first
self._speak_orig = speech.speech.speak
@wraps(speech.speech.speak)
Expand Down Expand Up @@ -158,6 +171,9 @@ def speechLogger_speakWithoutPauses( # noqa: C901
SpeechWithoutPauses.speakWithoutPauses = speechLogger_speakWithoutPauses

def terminate(self) -> None:
# Stop all logging that may be in progress
self.stopRemoteLog()
self.stopLocalLog()
# Remove the NVDA settings panel
if not globalVars.appArgs.secure:
gui.settingsDialogs.NVDASettingsDialog.categoryClasses.remove(SpeechLoggerSettings)
Expand All @@ -166,9 +182,49 @@ def terminate(self) -> None:
speech.speech.speak = self._speak_orig
SpeechWithoutPauses.speakWithoutPauses = SpeechWithoutPauses._speakWithoutPauses_orig
# Unregister extensionPoints
if self.flags.loggedAtStartup:
postNvdaStartup.unregister(self.startLocalLog)
extensionPoint._configChanged.unregister(self.applyUserConfig)
super().terminate()

def startLocalLog(self, automatic: bool = True) -> bool:
# If we are already logging, log a warning and return
if self.flags.localActive:
log.warning("Attempted to start logging speech when already logging speech!")
return True
# Must check whether we can or should log
if self.flags.logLocal:
self.flags.localActive = True # Start logging with next utterance
if automatic:
log.info("Began logging local speech at NVDA startup.")
else:
log.info("User initiated logging of local speech.")
return True
else:
return False

def stopLocalLog(self) -> bool:
if self.flags.localActive: # Currently logging, stop
# Write a message to the log stating that we are no longer logging
self.logToFile(self.files.local, None, self.dynamicLogStoppedText)
self.flags.localActive = False
self.flags.startedLocalLog = False
log.info("Stopped logging local speech.")
return True
else:
return False

def stopRemoteLog(self) -> bool:
if self.flags.remoteActive: # We were logging, stop
# Write a message to the log stating that we have stopped logging
self.logToFile(self.files.remote, None, self.dynamicLogStoppedText)
self.flags.remoteActive = False
self.flags.startedRemoteLog = False
log.info("Stopped logging remote speech.")
return True
else:
return False

def applyUserConfig(self, triggeredByExtensionPoint: bool = True) -> None:
"""Configures internal variables according to those set in NVDA config.

Expand Down Expand Up @@ -237,6 +293,9 @@ def applyUserConfig(self, triggeredByExtensionPoint: bool = True) -> None:
# In the config, tsMode will be 0 for off, higher for the other two options.
self.flags.startStopTimestamps = True if getConf("tsMode") > 0 else False
self.flags.logSayAll = bool(getConf("logSayAll"))
# In the config, possible logAtStartup values are:
# 0 for never, 1 for always, 2 for only if logging was on when shutdown (not yet implemented).
self.flags.logAtStartup = True if getConf("logAtStartup") == 1 else False
# Stage 5: utterance separation
# For this one we may need the configured custom separator. However, it seems that
# some part of NVDA or Configobj, escapes escape chars such as \t. We must undo that.
Expand Down Expand Up @@ -361,17 +420,11 @@ def _registerCallback(self) -> bool:
)
def script_toggleLocalSpeechLogging(self, gesture):
"""Toggles whether we are actively logging local speech."""
if self.flags.localActive: # Currently logging, stop
# Write a message to the log stating that we are no longer logging
self.logToFile(self.files.local, None, self.dynamicLogStoppedText)
self.flags.localActive = False
self.flags.startedLocalLog = False
if self.stopLocalLog(): # Stop the local log; returns True if logging was stopped
# Translators: message to tell the user that we are no longer logging.
ui.message(_("Stopped logging local speech."))
else: # Currently not logging, start
# Must check whether we can or should log
if self.flags.logLocal:
self.flags.localActive = True
if self.startLocalLog(False):
# Translators: a message to tell the user that we are now logging.
ui.message(_("Started logging local speech."))
else:
Expand All @@ -386,11 +439,7 @@ def script_toggleLocalSpeechLogging(self, gesture):
)
def script_toggleRemoteSpeechLogging(self, gesture):
"""Toggles whether we are actively logging remote speech."""
if self.flags.remoteActive: # We were logging, stop
# Write a message to the log stating that we have stopped logging
self.logToFile(self.files.remote, None, self.dynamicLogStoppedText)
self.flags.remoteActive = False
self.flags.startedRemoteLog = False
if self.stopRemoteLog(): # Stops remote logging if we were; returns True if stopped
# Translators: message to tell the user that we are no longer logging.
ui.message(_("Stopped logging remote speech."))
else: # We weren't logging, start
Expand Down
42 changes: 38 additions & 4 deletions addon/globalPlugins/speechLogger/configUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@

from . import extensionPoint

addonHandler.initTranslation()
try:
addonHandler.initTranslation()
except addonHandler.AddonError:
log.error(
"Attempted to initialize translations in an inappropriate context. May be running from scratchpad."
)

#: Default separator, when an invalid one has been set
DEFAULT_SEPARATOR = 0
#: Default timestamp mode, when an invalid one has been set
DEFAULT_TS_MODE = 1
#: Default log at startup selection, when config is invalid
DEFAULT_LOG_AT_STARTUP = 0

#: speechLogger Add-on config database
config.conf.spec["speechLogger"] = {
Expand All @@ -38,8 +45,9 @@
"rotate": "boolean(default=False)",
"separator": "string(default='2spc')",
"customSeparator": "string(default='')",
"tsMode": "integer(min=0,default=1,max=1)",
"logSayAll": "boolean(default=true)"
"tsMode": f"integer(min=0, default={DEFAULT_TS_MODE}, max=1)",
"logSayAll": "boolean(default=true)",
"logAtStartup": f"integer(min=0, default={DEFAULT_LOG_AT_STARTUP}, max=1)"
}

def getConf(item: str) -> str:
Expand Down Expand Up @@ -102,6 +110,14 @@ class SpeechLoggerSettings(gui.settingsDialogs.SettingsPanel):
# Translators: A timestamp mode option in the timestamps mode combobox
_("When a log begins or ends")
)

#: Possible values for the log at startup configuration combobox
logAtStartupDisplayChoices: tuple = (
# Translators: An option in the log at startup combobox
_("Never"),
# Translators: An option in the log at startup combobox
_("Always")
)


def makeSettings(self, settingsSizer) -> None:
Expand Down Expand Up @@ -227,6 +243,23 @@ def makeSettings(self, settingsSizer) -> None:
self.logSayAllCB = miscGroupHelper.addItem(wx.CheckBox(miscGroupBox, label=logSayAllCBLabel))
self.logSayAllCB.SetValue(getConf("logSayAll"))

# Translators: this is the label for a combobox which determines whether to begin logging at startup.
logAtStartupComboLabel: str = _("Begin logging at startup")
self.logAtStartupChoiceControl = miscGroupHelper.addLabeledControl(
logAtStartupComboLabel, wx.Choice, choices=self.logAtStartupDisplayChoices
)
# Iterate the combobox choices, and pick the one listed in config
for index, name in enumerate(self.logAtStartupDisplayChoices):
if index == getConf("logAtStartup"):
self.logAtStartupChoiceControl.SetSelection(index)
break
else: # Unrecognized choice saved in configuration
log.debugWarning(
'Could not set log at startup to the config derived option of "'
f'{getConf("logAtStartup")}". Using default.'
)
self.logAtStartupChoiceControl.SetSelection(DEFAULT_LOG_AT_STARTUP) # Use default

def onSave(self):
"""Save the settings to the Normal Configuration."""
# Make sure we're operating in the "normal" profile
Expand All @@ -242,9 +275,10 @@ def onSave(self):
setConf("customSeparator", self.customSeparatorControl.Value)
setConf("tsMode", self.tsModeChoiceControl.Selection)
setConf("logSayAll", self.logSayAllCB.Value)
setConf("logAtStartup", self.logAtStartupChoiceControl.Selection)

def postSave(self):
"""After saving settings, set a flag to cause a config re-read by the add-on."""
"""After saving settings, notify the extensionPoint to cause a config re-read by the add-on."""
# Make sure we're operating in the "normal" profile
if config.conf.profiles[-1].name is None and len(config.conf.profiles) == 1:
extensionPoint._configChanged.notify()
Expand Down
2 changes: 1 addition & 1 deletion buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _(arg):
# Translators: Long description to be shown for this add-on on add-on information from add-ons manager
"addon_description": _("""Logs speech utterances to a file. Can also log NVDA remote session speech from the NVDA Remote add-on, to the same or another file."""),
# version
"addon_version": "23.2.10",
"addon_version": "23.3.01",
# Author(s)
"addon_author": "Luke Davis <XLTechie@newanswertech.com>, James Scholes",
# URL for the add-on documentation support
Expand Down
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 23.3.01

* New feature: can be configured to automatically start logging at NVDA startup. (#9)
* Now stop all logging during add-on termination (fixes unreported bug with plugin reloads).

### 23..2.08

* Fixed the position of a checkbox in configuration display (contributed in #8 by @CyrilleB79).
Expand Down
5 changes: 4 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ If you can think of some use case that requires it to operate differently in dif
* Separator. This combobox lets you choose one of the available utterance separators. See below for more information.
* Custom separator. This field lets you enter a custom utterance separator (see below), which is used if "custom" is chosen in the combobox.
* Timestamp mode. This combobox allows you to choose between no timestamps, and a timestamp at the start and end of each log session.
* Log speech during say-all (read to end) mode. As of version 23.2, this add-on logs speech generated when you press NVDA+DownArrow (NVDA+a in laptop layout). If you would rather not have this kind of narrative long reading logged, un-check this box.
* Log speech during say-all (read to end) mode. This add-on logs speech generated when you press NVDA+DownArrow (NVDA+a in laptop layout). If you would rather not have that kind of narrative long reading logged, un-check this box.
* Begin logging at startup. You can set this option to "Always", if you want speech to be logged automatically when NVDA starts. This only applies to local speech, and the default is "never".

#### Utterance separator

Expand All @@ -45,6 +46,7 @@ If you wanted it to be a newline followed by a tab, you could enter "`\n\t`".
This add-on has two gestures set by default.
You can change them in the NVDA Input Gestures Tools category.
Look for "Toggles logging of local speech" and "Toggles logging of remote speech".

* NVDA+Alt+L: start/stop logging of local speech.
* NVDA+Shift+Alt+L: start/stop logging of remote speech.

Expand All @@ -64,4 +66,5 @@ If you would like to suggest a feature or report a bug, please reach out by emai
As always, I appreciate hearing that my add-ons are useful, and what people are using them for.

[1]: https://www.nvaccess.org/addonStore/legacy?file=speechLogger

[2]: https://github.com/opensourcesys/speechLogger/issues/new