blob: 2cfb466c706b25dc79e81b2375fefa4b85753544 [file] [log] [blame] [edit]
/*
* BRLTTY - A background process providing access to the console screen (when in
* text mode) for a blind person using a refreshable braille display.
*
* Copyright (C) 1995-2023 by The BRLTTY Developers.
*
* BRLTTY comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
#include "prologue.h"
#include <stdio.h>
#include "embed.h"
#include "strfmt.h"
#include "cmd_queue.h"
#include "cmd_speech.h"
#include "cmd_utils.h"
#include "brl_cmds.h"
#include "prefs.h"
#include "alert.h"
#include "spk.h"
#include "scr.h"
#include "update.h"
#include "core.h"
#ifdef ENABLE_SPEECH_SUPPORT
static void
sayScreenRegion (int left, int top, int width, int height, int track, SayMode mode) {
if (mode == sayImmediate) muteSpeech(&spk, __func__);
size_t count = width * height;
ScreenCharacter characters[count];
readScreen(left, top, width, height, characters);
{
ScreenCharacter *character = characters;
ScreenCharacter *end = character + count;
character += width - 1;
while (character < end) {
if (iswspace(character->text)) character->text = WC_C('\n');
character += width;
}
}
spk.track.isActive = track;
spk.track.screenNumber = scr.number;
spk.track.firstLine = top;
spk.track.speechLocation = SPK_LOC_NONE;
sayScreenCharacters(characters, count, 0);
}
static void
sayScreenLines (int line, int count, int track, SayMode mode) {
sayScreenRegion(0, line, scr.cols, count, track, mode);
}
static void
speakDone (const ScreenCharacter *line, int column, int count, int spell) {
ScreenCharacter internalBuffer[count];
if (line) {
line = &line[column];
} else {
readScreen(column, ses->spky, count, 1, internalBuffer);
line = internalBuffer;
}
speakCharacters(line, count, spell, 1);
placeBrailleWindowHorizontally(ses->spkx);
slideBrailleWindowVertically(ses->spky);
suppressAutospeak();
}
static void
speakCurrentCharacter (void) {
speakDone(NULL, ses->spkx, 1, 0);
}
static void
speakCurrentLine (void) {
speakDone(NULL, 0, scr.cols, 0);
}
static int
handleSpeechCommands (int command, void *data) {
switch (command & BRL_MSK_CMD) {
case BRL_CMD_RESTARTSPEECH:
restartSpeechDriver();
break;
case BRL_CMD_SPK_STOP:
disableSpeechDriver(gettext("speech driver stopped"));
break;
case BRL_CMD_SPK_START:
enableSpeechDriver(1);
break;
case BRL_CMD_SPKHOME: {
if (scr.number == spk.track.screenNumber) {
if (startScreenCursorRouting(ses->spkx, ses->spky)) {
break;
}
}
alert(ALERT_COMMAND_REJECTED);
break;
}
case BRL_CMD_MUTE:
muteSpeech(&spk, "command");
break;
case BRL_CMD_SAY_LINE:
sayScreenLines(ses->winy, 1, 0, prefs.sayLineMode);
break;
case BRL_CMD_SAY_ALL:
sayScreenLines(0, scr.rows, 1, sayImmediate);
break;
case BRL_CMD_SAY_ABOVE:
sayScreenLines(0, ses->winy+1, 1, sayImmediate);
break;
case BRL_CMD_SAY_BELOW:
sayScreenLines(ses->winy, scr.rows-ses->winy, 1, sayImmediate);
break;
case BRL_CMD_SAY_SOFTER:
if (!canSetSpeechVolume(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechVolume > 0) {
setSpeechVolume(&spk, --prefs.speechVolume, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SAY_LOUDER:
if (!canSetSpeechVolume(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechVolume < SPK_VOLUME_MAXIMUM) {
setSpeechVolume(&spk, ++prefs.speechVolume, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SAY_SLOWER:
if (!canSetSpeechRate(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechRate > 0) {
setSpeechRate(&spk, --prefs.speechRate, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SAY_FASTER:
if (!canSetSpeechRate(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechRate < SPK_RATE_MAXIMUM) {
setSpeechRate(&spk, ++prefs.speechRate, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SAY_LOWER:
if (!canSetSpeechPitch(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechPitch > 0) {
setSpeechPitch(&spk, --prefs.speechPitch, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SAY_HIGHER:
if (!canSetSpeechPitch(&spk)) {
alert(ALERT_COMMAND_REJECTED);
} else if (prefs.speechPitch < SPK_PITCH_MAXIMUM) {
setSpeechPitch(&spk, ++prefs.speechPitch, 1);
} else {
alert(ALERT_NO_CHANGE);
}
break;
case BRL_CMD_SPEAK_CURR_CHAR:
speakCurrentCharacter();
break;
case BRL_CMD_SPEAK_PREV_CHAR:
if (ses->spkx > 0) {
ses->spkx -= 1;
speakCurrentCharacter();
} else if (ses->spky > 0) {
ses->spky -= 1;
ses->spkx = scr.cols - 1;
alert(ALERT_WRAP_UP);
speakCurrentCharacter();
} else {
alert(ALERT_BOUNCE);
}
break;
case BRL_CMD_SPEAK_NEXT_CHAR:
if (ses->spkx < (scr.cols - 1)) {
ses->spkx += 1;
speakCurrentCharacter();
} else if (ses->spky < (scr.rows - 1)) {
ses->spky += 1;
ses->spkx = 0;
alert(ALERT_WRAP_DOWN);
speakCurrentCharacter();
} else {
alert(ALERT_BOUNCE);
}
break;
case BRL_CMD_SPEAK_FRST_CHAR: {
ScreenCharacter characters[scr.cols];
int column;
readScreenRow(ses->spky, scr.cols, characters);
if ((column = findFirstNonSpaceCharacter(characters, scr.cols)) >= 0) {
ses->spkx = column;
speakDone(characters, column, 1, 0);
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
case BRL_CMD_SPEAK_LAST_CHAR: {
ScreenCharacter characters[scr.cols];
int column;
readScreenRow(ses->spky, scr.cols, characters);
if ((column = findLastNonSpaceCharacter(characters, scr.cols)) >= 0) {
ses->spkx = column;
speakDone(characters, column, 1, 0);
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
{
int direction;
int spell;
case BRL_CMD_SPEAK_PREV_WORD:
direction = -1;
spell = 0;
goto speakWord;
case BRL_CMD_SPEAK_NEXT_WORD:
direction = 1;
spell = 0;
goto speakWord;
case BRL_CMD_SPEAK_CURR_WORD:
direction = 0;
spell = 0;
goto speakWord;
case BRL_CMD_SPELL_CURR_WORD:
direction = 0;
spell = 1;
goto speakWord;
speakWord:
{
int row = ses->spky;
int column = ses->spkx;
ScreenCharacter characters[scr.cols];
int onCurrentWord;
int onSpace;
int from = column;
int to = from + 1;
findWord:
readScreenRow(row, scr.cols, characters);
onCurrentWord = (row == ses->spky) && !iswspace(characters[column].text);
onSpace = !onCurrentWord;
if (direction < 0) {
while (1) {
if (column == 0) {
if (!onSpace && !onCurrentWord) {
ses->spkx = from = column;
ses->spky = row;
break;
}
if (row == 0) goto noWord;
if (row-- == ses->spky) alert(ALERT_WRAP_UP);
column = scr.cols;
goto findWord;
}
{
int isSpace = iswspace(characters[--column].text);
if (isSpace != onSpace) {
if (onCurrentWord) {
onCurrentWord = 0;
} else if (!onSpace) {
ses->spkx = from = column + 1;
ses->spky = row;
break;
}
if (!isSpace) to = column + 1;
onSpace = isSpace;
}
}
}
} else if (direction > 0) {
while (1) {
if (++column == scr.cols) {
if (!onSpace && !onCurrentWord) {
to = column;
ses->spkx = from;
ses->spky = row;
break;
}
if (row == (scr.rows - 1)) goto noWord;
if (row++ == ses->spky) alert(ALERT_WRAP_DOWN);
column = -1;
goto findWord;
}
{
int isSpace = iswspace(characters[column].text);
if (isSpace != onSpace) {
if (onCurrentWord) {
onCurrentWord = 0;
} else if (!onSpace) {
to = column;
ses->spkx = from;
ses->spky = row;
break;
}
if (!isSpace) from = column;
onSpace = isSpace;
}
}
}
} else if (!onSpace) {
while (from > 0) {
if (iswspace(characters[--from].text)) {
from += 1;
break;
}
}
while (to < scr.cols) {
if (iswspace(characters[to].text)) break;
to += 1;
}
}
speakDone(characters, from, to-from, spell);
break;
}
noWord:
alert(ALERT_BOUNCE);
break;
}
case BRL_CMD_SPEAK_CURR_LINE:
speakCurrentLine();
break;
{
int increment;
int limit;
case BRL_CMD_SPEAK_PREV_LINE:
increment = -1;
limit = 0;
goto speakLine;
case BRL_CMD_SPEAK_NEXT_LINE:
increment = 1;
limit = scr.rows - 1;
goto speakLine;
speakLine:
if (ses->spky == limit) {
alert(ALERT_BOUNCE);
} else {
if (prefs.skipIdenticalLines) {
ScreenCharacter original[scr.cols];
ScreenCharacter current[scr.cols];
unsigned int count = 0;
readScreenRow(ses->spky, scr.cols, original);
do {
readScreenRow(ses->spky+=increment, scr.cols, current);
if (!isSameRow(original, current, scr.cols, isSameText)) break;
alertLineSkipped(&count);
} while (ses->spky != limit);
} else {
ses->spky += increment;
}
speakCurrentLine();
}
break;
}
case BRL_CMD_SPEAK_FRST_LINE: {
ScreenCharacter characters[scr.cols];
int row = 0;
while (row < scr.rows) {
readScreenRow(row, scr.cols, characters);
if (!isAllSpaceCharacters(characters, scr.cols)) break;
row += 1;
}
if (row < scr.rows) {
ses->spky = row;
ses->spkx = 0;
speakCurrentLine();
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
case BRL_CMD_SPEAK_LAST_LINE: {
ScreenCharacter characters[scr.cols];
int row = scr.rows - 1;
while (row >= 0) {
readScreenRow(row, scr.cols, characters);
if (!isAllSpaceCharacters(characters, scr.cols)) break;
row -= 1;
}
if (row >= 0) {
ses->spky = row;
ses->spkx = 0;
speakCurrentLine();
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
case BRL_CMD_DESC_CURR_CHAR: {
char description[0X50];
STR_BEGIN(description, sizeof(description));
STR_FORMAT(formatPhoneticPhrase, ses->spkx, ses->spky);
STR_END;
sayString(&spk, description, SAY_OPT_MUTE_FIRST);
break;
}
case BRL_CMD_ROUTE_CURR_LOCN: {
if (!startScreenCursorRouting(ses->spkx, ses->spky)) {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
case BRL_CMD_SPEAK_CURR_LOCN: {
char buffer[0X50];
snprintf(buffer, sizeof(buffer), "%s %d, %s %d",
gettext("line"), ses->spky+1,
gettext("column"), ses->spkx+1);
sayString(&spk, buffer, SAY_OPT_MUTE_FIRST);
break;
}
case BRL_CMD_SPEAK_INDENT:
speakIndent(NULL, 0, 1);
break;
default: {
int arg = command & BRL_MSK_ARG;
switch (command & BRL_MSK_BLK) {
case BRL_CMD_BLK(ROUTE_SPEECH): {
int column, row;
if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
ses->spkx = column;
ses->spky = row;
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
default:
return 0;
}
}
}
return 1;
}
#endif /* ENABLE_SPEECH_SUPPORT */
int
addSpeechCommands (void) {
#ifdef ENABLE_SPEECH_SUPPORT
return pushCommandHandler("speech", KTB_CTX_DEFAULT,
handleSpeechCommands, NULL, NULL);
#else /* ENABLE_SPEECH_SUPPORT */
return 0;
#endif /* ENABLE_SPEECH_SUPPORT */
}