diff options
Diffstat (limited to 'engines/agi/predictive.cpp')
-rw-r--r-- | engines/agi/predictive.cpp | 565 |
1 files changed, 374 insertions, 191 deletions
diff --git a/engines/agi/predictive.cpp b/engines/agi/predictive.cpp index 5e086de2bb..67ebed5f05 100644 --- a/engines/agi/predictive.cpp +++ b/engines/agi/predictive.cpp @@ -28,6 +28,7 @@ #include "agi/keyboard.h" #include "common/func.h" +#include "common/config-manager.h" namespace Agi { @@ -35,99 +36,76 @@ namespace Agi { #define kModeNum 1 #define kModeAbc 2 -static byte s_asciiToNumTable[256]; +#define MAXLINELEN 80 + +uint8 countWordsInString(char *str) { + // Count the number of (space separated) words in the given string. + char *ptr; + + if (!str) + return 0; -void setAsciiToNumTableBatch(const char *chars, byte value) { - while (*chars) { - s_asciiToNumTable[tolower(*chars)] = value; - s_asciiToNumTable[toupper(*chars)] = value; - chars++; + ptr = strchr(str, ' '); + if (!ptr) { + debug("Invalid dictionary line"); + return 0; } -} -void initAsciiToNumTable() { - memset(s_asciiToNumTable, 0, sizeof(s_asciiToNumTable)); - - setAsciiToNumTableBatch("1'-.&", 1); - setAsciiToNumTableBatch("2abc", 2); - setAsciiToNumTableBatch("3def", 3); - setAsciiToNumTableBatch("4ghi", 4); - setAsciiToNumTableBatch("5jkl", 5); - setAsciiToNumTableBatch("6mno", 6); - setAsciiToNumTableBatch("7pqrs", 7); - setAsciiToNumTableBatch("8tuv", 8); - setAsciiToNumTableBatch("9wxyz", 9); + uint8 num = 1; + ptr++; + while ((ptr = strchr(ptr, ' '))) { + ptr++; + num++; + } + return num; } -class SearchTree { -public: - //byte val; - //SearchTree *parent; // TODO: Could be used to speed up re-searches - SearchTree *children[10]; +void bringWordtoTop(char *str, int wordnum) { + // This function reorders the words on the given pred.dic line + // by moving the word at position 'wordnum' to the front (that is, right behind + // right behind the numerical code word at the start of the line). Common::StringList words; - - SearchTree() { - memset(children, 0, sizeof(children)); - } - - SearchTree *getChild(byte val) { - assert(val < 10); - if (children[val] == 0) { - children[val] = new SearchTree(); - } - return children[val]; - } + char buf[MAXLINELEN]; - SearchTree *findChildWithWords() { - if (!words.empty()) - return this; - - SearchTree *child = 0; - for (int i = 0; i < 10 && !child; ++i) { - if (children[i]) - child = children[i]->findChildWithWords(); - } - - return child; - } - -}; - - -void AgiEngine::insertSearchNode(const char *word) { - // Insert the word into the tree - SearchTree *tree = _searchTreeRoot; - assert(tree); - for (int i = 0; word[i] != 0; ++i) { - byte key = s_asciiToNumTable[(int)word[i]]; - if (key == 0) - return; // abort! - tree = tree->getChild(key); + if (!str) + return; + strncpy(buf, str, MAXLINELEN); + char *word = strtok(buf, " "); + if (!word) { + debug("Invalid dictionary line"); + return; } - // TODO: Sort words, remove duplicates... ? - tree->words.push_back(word); + words.push_back(word); + while ((word = strtok(NULL, " ")) != NULL) + words.push_back(word); + words.insert_at(1, words.remove_at(wordnum + 1)); + + Common::String tmp; + for (uint8 i = 0; i < words.size(); i++) + tmp += words[i] + " "; + tmp.deleteLastChar(); + memcpy(str, tmp.c_str(), strlen(str)); } bool AgiEngine::predictiveDialog(void) { - int key, active = 0; + int key = 0, active = -1, lastactive = 0; bool rc = false; - int x; + uint8 x; int y; int bx[17], by[17]; - String prefix = ""; - char temp[MAXWORDLEN + 1]; + String prefix; + char temp[MAXWORDLEN + 1], repeatcount[MAXWORDLEN]; AgiBlock tmpwindow; - - // FIXME: Move this to a more appropriate place. - initAsciiToNumTable(); + bool navigationwithkeys = false; + bool processkey; const char *buttonStr[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }; const char *buttons[] = { "(1)'-.&", "(2)abc", "(3)def", "(4)ghi", "(5)jkl", "(6)mno", "(7)pqrs", "(8)tuv", "(9)wxyz", - "next", "add", + "(#)next", "add", "<", "Cancel", "OK", "Pre", "(0) ", NULL @@ -141,13 +119,21 @@ bool AgiEngine::predictiveDialog(void) { 15, 0, 15, 0, 14, 0, 15, 0, 0, 0 }; - const char *modes[] = { "Pre", "123", "Abc" }; + const char *modes[] = { "(*)Pre", "(*)123", "(*)Abc" }; - if (!_searchTreeRoot) { + // FIXME: Move this to a more appropriate place. + if (!_predictiveDictText) { loadDict(); - if (!_searchTreeRoot) + if (!_predictiveDictText) return false; } + _predictiveDictActLine = NULL; + uint8 numMatchingWords = 0; + + _predictiveDialogRunning = true; + _system->setFeatureState(OSystem::kFeatureDisableKeyFiltering, true); + + memset(repeatcount, 0, MAXWORDLEN); // show the predictive dialog. // if another window is already in display, save its state into tmpwindow @@ -158,13 +144,11 @@ bool AgiEngine::predictiveDialog(void) { _gfx->drawRectangle(62, 54, 249, 66, MSG_BOX_TEXT); _gfx->flushBlock(62, 54, 249, 66); - _gfx->printCharacter(3, 11, _game.cursorChar, MSG_BOX_COLOUR, MSG_BOX_TEXT); - bx[15] = 73; // Zero/space by[15] = 120; - bx[9] = 120; // next + bx[9] = 110; // next by[9] = 120; - bx[10] = 160; // add + bx[10] = 172; // add by[10] = 120; bx[14] = 200; // Mode by[14] = 120; @@ -188,14 +172,13 @@ bool AgiEngine::predictiveDialog(void) { } /* clear key queue */ - while (_gfx->keypress()) { + while (_gfx->keypress()) _gfx->getKey(); - } - _currentCode = ""; - _currentWord = ""; + prefix.clear(); + _currentCode.clear(); + _currentWord.clear(); _wordNumber = 0; - _activeTreeNode = 0; int mode = kModePre; @@ -207,7 +190,8 @@ bool AgiEngine::predictiveDialog(void) { int color1 = colors[i * 2]; int color2 = colors[i * 2 + 1]; - if (i == 9 && !(_activeTreeNode && _activeTreeNode->words.size() > 1)) { // Next + if (i == 9 && !((mode != kModeAbc && _predictiveDictActLine && numMatchingWords > 1) + || (mode == kModeAbc && _currentWord.size() && _currentWord.lastChar() != ' '))) { // Next color2 = 7; } @@ -222,94 +206,253 @@ bool AgiEngine::predictiveDialog(void) { } } - if (_currentWord != "") { - temp[MAXWORDLEN] = 0; + temp[MAXWORDLEN] = 0; - strncpy(temp, prefix.c_str(), MAXWORDLEN); - strncat(temp, _currentWord.c_str(), MAXWORDLEN); + strncpy(temp, prefix.c_str(), MAXWORDLEN); + strncat(temp, _currentWord.c_str(), MAXWORDLEN); - for (int i = prefix.size() + _currentCode.size(); i < MAXWORDLEN; i++) - temp[i] = ' '; + for (int i = prefix.size() + _currentCode.size(); i < MAXWORDLEN; i++) + temp[i] = ' '; - printText(temp, 0, 8, 7, MAXWORDLEN, 15, 0); - _gfx->flushBlock(62, 54, 249, 66); - } + printText(temp, 0, 8, 7, MAXWORDLEN, 15, 0); + _gfx->flushBlock(62, 54, 249, 66); + + if (active >= 0 && !navigationwithkeys) { + // provide visual feedback only when not navigating with the arrows + // so that the user can see the active button. + active = -1; + needRefresh = true; + } else + needRefresh = false; + + _gfx->doUpdate(); } _gfx->pollTimer(); /* msdos driver -> does nothing */ key = doPollKeyboard(); + processkey = false; switch (key) { case KEY_ENTER: - rc = true; - goto press; + if (navigationwithkeys) { + // when the user has utilized arrow key navigation, + // interpret enter as 'click' on the active button + active = lastactive; + } else { + // else it is a shortcut for 'Ok' + active = 13; + } + processkey = true; + break; case KEY_ESCAPE: rc = false; goto getout; case BUTTON_LEFT: + navigationwithkeys = false; for (int i = 0; buttons[i]; i++) { if (_gfx->testButton(bx[i], by[i], buttons[i])) { - needRefresh = true; active = i; + processkey = true; + break; + } + } + break; + case KEY_BACKSPACE: + active = 11; + processkey = true; + break; + case '#': + active = 9; + processkey = true; + break; + case '*': + active = 14; + processkey = true; + break; + case 0x09: /* Tab */ + navigationwithkeys = true; + debugC(3, kDebugLevelText, "Focus change"); + lastactive = active = lastactive + 1; + active %= ARRAYSIZE(buttons) - 1; + needRefresh = true; + break; + case KEY_LEFT: + navigationwithkeys = true; + if (lastactive == 0 || lastactive == 3 || lastactive == 6) + active = lastactive + 2; + else if (lastactive == 9) + active = 15; + else if (lastactive == 11) + active = 11; + else if (lastactive == 12) + active = 13; + else if (lastactive == 14) + active = 10; + else + active = lastactive - 1; + lastactive = active; + needRefresh = true; + break; + case KEY_RIGHT: + navigationwithkeys = true; + if (lastactive == 2 || lastactive == 5 || lastactive == 8) + active = lastactive - 2; + else if (lastactive == 10) + active = 14; + else if (lastactive == 11) + active = 11; + else if (lastactive == 13) + active = 12; + else if (lastactive == 15) + active = 9; + else + active = lastactive + 1; + lastactive = active; + needRefresh = true; + break; + case KEY_UP: + navigationwithkeys = true; + if (lastactive <= 2) + active = 11; + else if (lastactive == 9 || lastactive == 10) + active = lastactive - 2; + else if (lastactive == 11) + active = 13; + else if (lastactive == 14) + active = 8; + else if (lastactive == 15) + active = 6; + else + active = lastactive - 3; + lastactive = active; + needRefresh = true; + break; + case KEY_DOWN: + navigationwithkeys = true; + if (lastactive == 6) + active = 15; + else if (lastactive == 7 || lastactive == 8) + active = lastactive + 2; + else if (lastactive == 11) + active = 0; + else if (lastactive == 12 || lastactive == 13) + active = 11; + else if (lastactive == 14 || lastactive == 15) + active = lastactive - 2; + else + active = lastactive + 3; + lastactive = active; + needRefresh = true; + break; + default: + // handle numeric buttons + if (key >= '1' && key <= '9') { + active = key - '1'; + processkey = true; + } else if (key == '0') { + active = 15; + processkey = true; + } + break; + } - if (active == 15 && mode != kModeNum) { // Space - strncpy(temp, _currentWord.c_str(), _currentCode.size()); - - temp[_currentCode.size()] = 0; - - prefix += temp; - prefix += " "; - _currentCode = ""; - } if (active < 9 || active == 11 || active == 15) { // number or backspace - if (active == 11) { // backspace - if (_currentCode.size()) { - _currentCode.deleteLastChar(); - } else { - if (prefix.size()) - prefix.deleteLastChar(); - } - } else if (active == 15) { // zero + if (processkey) { + if (active >= 0) { + needRefresh = true; + lastactive = active; + if (active == 15 && mode != kModeNum) { // Space + // bring MRU word at the top of the list when changing words + if (mode == kModePre && _predictiveDictActLine && numMatchingWords > 1 && _wordNumber != 0) + bringWordtoTop(_predictiveDictActLine, _wordNumber); + strncpy(temp, _currentWord.c_str(), _currentCode.size()); + temp[_currentCode.size()] = 0; + prefix += temp; + prefix += " "; + _currentCode.clear(); + _currentWord.clear(); + numMatchingWords = 0; + memset(repeatcount, 0, MAXWORDLEN); + } else if (active < 9 || active == 11 || active == 15) { // number or backspace + if (active == 11) { // backspace + if (_currentCode.size()) { + repeatcount[_currentCode.size() - 1] = 0; + _currentCode.deleteLastChar(); + } else { + if (prefix.size()) + prefix.deleteLastChar(); + } + } else if (prefix.size() + _currentCode.size() < MAXWORDLEN - 1) { // don't overflow the dialog line + if (active == 15) { // zero _currentCode += buttonStr[9]; } else { _currentCode += buttonStr[active]; } + } - if (mode == kModeNum) { + switch (mode) { + case kModeNum: _currentWord = _currentCode; - } else if (mode == kModePre) { + break; + case kModePre: if (!matchWord() && _currentCode.size()) { _currentCode.deleteLastChar(); matchWord(); } + numMatchingWords = countWordsInString(_predictiveDictActLine); + break; + case kModeAbc: + for (x = 0; x < _currentCode.size(); x++) + if (_currentCode[x] >= '1') + temp[x] = buttons[_currentCode[x] - '1'][3 + repeatcount[x]]; + temp[_currentCode.size()] = 0; + _currentWord = temp; + } + } else if (active == 9) { // next + if (mode == kModePre) { + if (_predictiveDictActLine && numMatchingWords > 1) { + _wordNumber = (_wordNumber + 1) % numMatchingWords; + char tmp[MAXLINELEN]; + strncpy(tmp, _predictiveDictActLine, MAXLINELEN); + char *tok = strtok(tmp, " "); + for (uint8 i = 0; i <= _wordNumber; i++) + tok = strtok(NULL, " "); + _currentWord = String(tok, _currentCode.size()); } - } else if (active == 9) { // next - int totalWordsNumber = _activeTreeNode ? _activeTreeNode->words.size() : 0; - if (totalWordsNumber > 0) { - _wordNumber = (_wordNumber + 1) % totalWordsNumber; - _currentWord = String(_activeTreeNode->words[_wordNumber].c_str(), _currentCode.size()); + } else if (mode == kModeAbc){ + x = _currentCode.size(); + if (x) { + if (_currentCode.lastChar() == '1' || _currentCode.lastChar() == '7' || _currentCode.lastChar() == '9') + repeatcount[x - 1] = (repeatcount[x - 1] + 1) % 4; + else + repeatcount[x - 1] = (repeatcount[x - 1] + 1) % 3; + if (_currentCode.lastChar() >= '1') + _currentWord[x - 1] = buttons[_currentCode[x - 1] - '1'][3 + repeatcount[x - 1]]; } - } else if (active == 10) { // add - debug(0, "add"); - } else if (active == 13) { // Ok - rc = true; - goto press; - } else if (active == 14) { // Mode - mode++; - if (mode > kModeAbc) - mode = kModePre; - } else { - goto press; } + } else if (active == 10) { // add + debug(0, "add"); + } else if (active == 13) { // Ok + // bring MRU word at the top of the list when ok'ed out of the dialog + if (mode == kModePre && _predictiveDictActLine && numMatchingWords > 1 && _wordNumber != 0) + bringWordtoTop(_predictiveDictActLine, _wordNumber); + rc = true; + goto press; + } else if (active == 14) { // Mode + mode++; + if (mode > kModeAbc) + mode = kModePre; + + // truncate current input at mode change + strncpy(temp, _currentWord.c_str(), _currentCode.size()); + temp[_currentCode.size()] = 0; + prefix += temp; + _currentCode.clear(); + _currentWord.clear(); + memset(repeatcount, 0, MAXWORDLEN); + } else { + goto press; } } - break; - case 0x09: /* Tab */ - debugC(3, kDebugLevelText, "Focus change"); - active++; - active %= ARRAYSIZE(buttons) - 1; - needRefresh = true; - break; } - _gfx->doUpdate(); } press: @@ -330,80 +473,120 @@ bool AgiEngine::predictiveDialog(void) { _gfx->doUpdate(); } + _system->setFeatureState(OSystem::kFeatureDisableKeyFiltering, false); + _predictiveDialogRunning = false; + return rc; } -#define MAXLINELEN 80 - void AgiEngine::loadDict(void) { - Common::File in; - char buf[MAXLINELEN]; - int words = 0, lines = 0; + Common::File inFile; + int lines = 0; - if (!in.open("pred.txt")) - return; + ConfMan.registerDefault("predictive_dictionary", "pred.dic"); - _searchTreeRoot = new SearchTree(); - words = 0; + uint32 time1 = _system->getMillis(); + if (!inFile.open(ConfMan.get("predictive_dictionary"))) + return; - while (!in.eos() && in.readLine(buf, MAXLINELEN)) { - // Skip leading & trailing whitespaces - char *word = Common::trim(buf); + char *ptr; + int size = inFile.size(); - // Skip empty lines - if (*word == 0) - continue; - + _predictiveDictText = (char *)malloc(size + 1); + if (!_predictiveDictText) { + warning("Not enough memory to load the predictive dictionary"); + return; + } + inFile.read(_predictiveDictText, size); + _predictiveDictText[size] = 0; + uint32 time2 = _system->getMillis(); + debug("Time to read %s: %d bytes, %d ms", inFile.name(), size, time2-time1); + inFile.close(); + + ptr = _predictiveDictText; + lines = 1; + while ((ptr = strchr(ptr, '\n'))) { lines++; + ptr++; + } - // The lines are of the form: "1234 word word" - // I.e. first comes a T9 number, then a space separated list of - // words with that T9 code. We simply ignore the T9 code, and then - // insert the words in order of occurance. - char *tok = strtok(word, " \t"); - if (tok) { - while ((tok = strtok(NULL, " ")) != NULL) { - insertSearchNode(tok); - words++; - } - } + _predictiveDictLine = (char **)calloc(1, sizeof(char *) * lines); + if (_predictiveDictLine == NULL) { + warning("Cannot allocate memory for line index buffer."); + return; + } + _predictiveDictLine[0] = _predictiveDictText; + ptr = _predictiveDictText; + int i = 1; + while ((ptr = strchr(ptr, '\n'))) { + *ptr = 0; + ptr++; + _predictiveDictLine[i++] = ptr; } + if (_predictiveDictLine[lines - 1][0] == 0) + lines--; - debug(0, "Loaded %d lines with %d words", lines, words); + _predictiveDictLineCount = lines; + debug("Loaded %d lines", _predictiveDictLineCount); + + uint32 time3 = _system->getMillis(); + printf("Time to parse pred.dic: %d, total: %d\n", time3-time2, time3-time1); } bool AgiEngine::matchWord(void) { if (_currentCode.empty()) { return false; } - - // Lookup word in the search tree - SearchTree *tree = _searchTreeRoot; - assert(tree); - for (uint i = 0; i < _currentCode.size(); ++i) { - int key = _currentCode[i] - '0'; - if (key < 1 || key > 9) { - tree = 0; - break; // Invalid key/code value, abort! - } - tree = tree->children[key]; - if (!tree) - break; // No matching entry in the search tree, abort! + // Lookup word in the dictionary + int line, span, cmpRes, len; + char target[MAXWORDLEN]; + + strncpy(target, _currentCode.c_str(), MAXWORDLEN); + strcat(target, " "); + + // do the search at most two times: + // first try to match the exact code, by matching also the space after the code + // if there is not an exact match, do it once more for the best matching prefix (drop the space) + len = _currentCode.size() + 1; + for (int i = 0; i < 2; ++i) { + line = (_predictiveDictLineCount + 1) / 2 - 1; + // find out the 2^upper_int(log2(_predictiveDictLineCount)) + for (span = 1; span < _predictiveDictLineCount; span <<= 1) + ; + span >>= 1; + do { + cmpRes = strncmp(_predictiveDictLine[line], target, len); + if (cmpRes > 0) { + span /= 2; + line -= span; + if (line < 0) + line = 0; + } else if (cmpRes < 0) { + span /= 2; + line += span; + if (line >= _predictiveDictLineCount) + line = _predictiveDictLineCount - 1; + } + } while (cmpRes && span); + if (cmpRes == 0) // Exact match found? -> stop now + break; + len--; // Remove the trailing space } - - if (tree) - tree = tree->findChildWithWords(); - _wordNumber = 0; - _activeTreeNode = tree; _currentWord.clear(); - - - if (tree) { - _currentWord = String(_activeTreeNode->words[_wordNumber].c_str(), _currentCode.size()); + _wordNumber = 0; + if (!strncmp(_predictiveDictLine[line], target, len)) { + _predictiveDictActLine = _predictiveDictLine[line]; + char tmp[MAXLINELEN]; + strncpy(tmp, _predictiveDictActLine, MAXLINELEN); + char *tok = strtok(tmp, " "); + tok = strtok(NULL, " "); + _currentWord = String(tok, _currentCode.size()); + return true; + } else { + _predictiveDictActLine = NULL; + return false; } - - return tree != 0; } } // End of namespace Agi |