#include <QFile>
#include <QTextCursor>
#include <QTextStream>
#include <QSet>
#include "engine.h"
/// Default file name to load/save dictionary
const char *FileDictionaryT9::szDefFileName= "emulT9.dict";
Engine::Engine(QMainWindow *parent):
_keyScan (parent)
{
/// connect KeyScan
connect (&_keyScan, SIGNAL(keyPressed(int)), parent, SLOT(keyPressed()));
connect (&_keyScan, SIGNAL(keyPressed(int)), &_typer, SLOT(keyPressed(int)));
connect (&_keyScan, SIGNAL(keyPressed(int)), &_stopWatch, SLOT(keyPressed()));
/// connect StopWatch and typer
connect (parent, SIGNAL(start()), &_stopWatch, SLOT(start()));
connect (parent, SIGNAL(start()), &_typer, SLOT(init()));
connect (parent, SIGNAL(stop()), &_stopWatch, SLOT(stop()));
connect (parent, SIGNAL(stop()), &_typer, SLOT(flush()));
connect (parent, SIGNAL(reset()), &_stopWatch, SLOT(reset()));
connect (parent, SIGNAL(reset()), &_typer, SLOT(clear()));
connect (&_typer, SIGNAL(textSizeChanged(int)), &_stopWatch, SLOT(textSizeChanged(int)));
}
void Engine::connectToDesign (Ui::MainWindow &design)
{
_keyScan.connectToDesign (design); ///< connect KeyScan to main window UI
_stopWatch.connectToDesign (design); ///< connect StopWatch to main window UI
_typer.connectToDesign (design); ///< connect Typer to main window UI
}
KeyScan::KeyScan(QMainWindow *mainWnd):
_sc4 (QKeySequence("4"), mainWnd),
_sc6 (QKeySequence("6"), mainWnd)
{
_sc4.setContext (Qt::ApplicationShortcut);
_sc6.setContext (Qt::ApplicationShortcut);
}
void KeyScan::connectToDesign (Ui::MainWindow &design)
{
connect (design.buttLeft, SIGNAL(clicked()), this, SLOT(buttLeftClicked()));
connect (design.buttC, SIGNAL(clicked()), this, SLOT(buttCClicked()));
connect (design.buttRight, SIGNAL(clicked()), this, SLOT(buttRightClicked()));
connect (design.butt11, SIGNAL(clicked()), this, SLOT(butt11Clicked()));
connect (design.butt12, SIGNAL(clicked()), this, SLOT(butt12Clicked()));
connect (design.butt13, SIGNAL(clicked()), this, SLOT(butt13Clicked()));
connect (design.butt21, SIGNAL(clicked()), this, SLOT(butt21Clicked()));
connect (design.butt22, SIGNAL(clicked()), this, SLOT(butt22Clicked()));
connect (design.butt23, SIGNAL(clicked()), this, SLOT(butt23Clicked()));
connect (design.butt31, SIGNAL(clicked()), this, SLOT(butt31Clicked()));
connect (design.butt32, SIGNAL(clicked()), this, SLOT(butt32Clicked()));
connect (design.butt33, SIGNAL(clicked()), this, SLOT(butt33Clicked()));
connect (design.butt41, SIGNAL(clicked()), this, SLOT(butt41Clicked()));
connect (design.butt42, SIGNAL(clicked()), this, SLOT(butt42Clicked()));
connect (design.butt43, SIGNAL(clicked()), this, SLOT(butt43Clicked()));
connect (&_sc4, SIGNAL(activated()), design.butt21, SLOT(animateClick()));
connect (&_sc4, SIGNAL(activatedAmbiguously()), design.butt21, SLOT(animateClick()));
connect (&_sc6, SIGNAL(activated()), design.butt23, SLOT(animateClick()));
connect (&_sc6, SIGNAL(activatedAmbiguously()), design.butt23, SLOT(animateClick()));
}
void KeyScan::buttLeftClicked() { emit keyPressed (KEY_LEFT); }
void KeyScan::buttCClicked() { emit keyPressed (KEY_C); }
void KeyScan::buttRightClicked() { emit keyPressed (KEY_RIGHT); }
void KeyScan::butt11Clicked() { emit keyPressed (KEY_DOT); }
void KeyScan::butt12Clicked() { emit keyPressed (KEY_ABC); }
void KeyScan::butt13Clicked() { emit keyPressed (KEY_DEF); }
void KeyScan::butt21Clicked() { emit keyPressed (KEY_GHI); }
void KeyScan::butt22Clicked() { emit keyPressed (KEY_JKL); }
void KeyScan::butt23Clicked() { emit keyPressed (KEY_MNO); }
void KeyScan::butt31Clicked() { emit keyPressed (KEY_PQRS); }
void KeyScan::butt32Clicked() { emit keyPressed (KEY_TUV); }
void KeyScan::butt33Clicked() { emit keyPressed (KEY_WXYZ); }
void KeyScan::butt41Clicked() { emit keyPressed (KEY_SHIFT); }
void KeyScan::butt42Clicked() { emit keyPressed (KEY_ROT); }
void KeyScan::butt43Clicked() { emit keyPressed (KEY_GAP); }
StopWatch::StopWatch():
_iTicks(0), _iHits(0), _iTextSize(0)
{
connect (&_qTimer, SIGNAL(timeout()), this, SLOT(tick()));
}
void StopWatch::connectToDesign (Ui::MainWindow &design)
{
/// save pointers to managed widgets
_labelTime= design.labelTime;
_labelHitsPerMin= design.labelHitsPerMin;
_labelCharsPerMin= design.labelCharsPerMin;
_textEdit= design.textEdit;
}
void StopWatch::start()
{
_qTimer.start (iPeriod);
}
void StopWatch::stop()
{
_qTimer.stop();
}
void StopWatch::reset()
{
_iTicks=0;
_iHits=0;
update();
}
void StopWatch::tick()
{
_iTicks++;
update();
}
void StopWatch::keyPressed()
{
_iHits++;
}
void StopWatch::textSizeChanged (int iSize)
{
_iTextSize= iSize;
}
/// draw statistics
void StopWatch::update()
{
/// draw time elapsed
if (_iTicks) {
int iTicks= _iTicks;
const int iTenths= iTicks%10; iTicks/= 10;
const int iSecs= iTicks%60; iTicks/=60;
const int iMins= iTicks;
_labelTime->setText (QString(tr("<b>%1:%2</b>.%3")).arg(iMins,2,10,QChar('0')).arg(iSecs,2,10,QChar('0')).arg(iTenths,1));
} else
_labelTime->setText (QString());
/// draw hits/min
QString hitsPerMin= (_iTicks && _iHits) ?
QString (tr("<b>%1</b> Hits/min")).arg (static_cast<double>(_iHits)/_iTicks*600,0,'f',1):
"";
_labelHitsPerMin->setText (hitsPerMin);
/// draw letters /min
QString charsPerMin= (_iTicks && _iTextSize) ?
QString (tr("<b>%1</b> Letters/min")).arg (static_cast<double>(_iTextSize)/_iTicks*600,0,'f',1):
"";
_labelCharsPerMin->setText (charsPerMin);
}
AlphaSwitcher::AlphaSwitcher()
{
init();
}
void AlphaSwitcher::init()
{
_state= AS_FIRST;
}
void AlphaSwitcher::toggle()
{
switch (_state) {
case AS_LOW: _state=AS_FIRST; break;
case AS_FIRST: _state=AS_HIGH; break;
case AS_HIGH: _state=AS_LOW; break;
}
}
void AlphaSwitcher::notify()
{
if (_state== AS_FIRST)
_state= AS_LOW;
}
QString AlphaSwitcher::getStatus() const
{
switch (_state) {
default:
case AS_LOW: return "abc";
case AS_FIRST: return "Abc";
case AS_HIGH: return "ABC";
}
}
QChar AlphaSwitcher::translate (QChar c)
{
if (!c.isLetter ())
///< Do not translate non-alpha characters.
return c;
if (_state== AS_LOW)
return c.toLower();
else
return c.toUpper();
}
Rotator::Rotator (const QString &str):
_str(str), _iNow(0)
{
}
void Rotator::reset()
{
_iNow= 0;
}
void Rotator::rotate()
{
if (++_iNow >= _str.length())
_iNow=0;
}
QChar Rotator::getCurrent()
{
return _str[_iNow];
}
TyperWrapper::TyperWrapper():
_currentTyper(0), _iTimer(0),
_bSuggest(false), _bAtEnd(false),
_lastKey(0), _iSelBegin(0), _iSelEnd(0), _iTextSize(0)
{
/// Timer initializaton
_timer.setSingleShot (true);
connect (&_timer, SIGNAL(timeout()), this, SLOT(flush()));
}
TyperWrapper::~TyperWrapper()
{
delete _currentTyper;
}
void TyperWrapper::connectToDesign(Ui::MainWindow &design)
{
/// Save pointers to managed widgets.
_textEdit= design.textEdit;
_labelEngineStatus= design.labelEngineStatus;
/// Typer switching initialization
connect (design.cbT9, SIGNAL(toggled(bool)), this, SLOT(setT9(bool)));
setT9 (true);
/// Show extra carret (text is empty now)
showCarret();
}
void TyperWrapper::setTimer (int iTimer)
{
_iTimer= iTimer;
}
void TyperWrapper::putSuggest (const QString &str)
{
hideCarret();
_iSelEnd= _iSelBegin + str.length();
QTextCursor cursor= _textEdit-> textCursor();
cursor.insertText (str);
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
cursor.setPosition (_iSelEnd, QTextCursor::KeepAnchor);
_textEdit-> setTextCursor(cursor);
_bSuggest= true;
_strSuggest= str;
}
namespace {
inline bool isKeyAlpha (int key) { return KEY_ABC<= key && key <= KEY_WXYZ; }
}
void TyperWrapper::keyPressed (int iKey)
{
switch (iKey) {
case KEY_LEFT: {
///< Move left if possible.
flush();
if (0< _iSelBegin)
_iSelEnd= --_iSelBegin;
hideCarret();
showCarret();
break;
}
case KEY_RIGHT: {
///< Move right if possible.
if (_bSuggest) {
flush();
break;
}
hideCarret();
const int iTextLength= (_textEdit->toPlainText()).length();
if (iTextLength> _iSelBegin)
_iSelEnd= ++_iSelBegin;
showCarret();
break;
}
case KEY_SHIFT:
/// Switch upper/lower-case
_alphaSwitch.toggle();
updateStatus();
break;
case KEY_GAP:
/// Insert gap.
if (_lastKey== KEY_GAP) {
_strSuggest.append (" ");
putSuggest (_strSuggest);
break;
}
flush();
putSuggest (" ");
break;
case KEY_C: {
///< Delete if possible.
if (!_bSuggest) {
flush();
QTextCursor cursor= _textEdit-> textCursor();
if (cursor.atStart()) {
///< Delete not possible
/// Try back-space.
keyPressed (KEY_RIGHT);
cursor= _textEdit->textCursor();
}
cursor.deletePreviousChar();
_iSelBegin= cursor.anchor();
_iSelEnd= cursor.position();
_textEdit-> setTextCursor (cursor);
break;
}
}
default:
if (
_lastKey== KEY_GAP ||
(iKey==KEY_DOT && isKeyAlpha(_lastKey)) ||
(isKeyAlpha(iKey) && _lastKey==KEY_DOT))
///< Write gap(s) now.
flush();
/// Delegate key event to typer engine.
_currentTyper-> keyPressed (static_cast<EKey>(iKey));
startTimer();
}
///< Save key code for next wheel
switch (iKey) {
case KEY_SHIFT:
case KEY_ROT:
break;
default:
_lastKey= iKey;
advertiseTextSize();
}
}
void TyperWrapper::init()
{
// TODO: Put something meaningful here?
}
void TyperWrapper::flush()
{
_timer.stop();
if (_bSuggest) {
/// Destroy selection
_iSelBegin= _iSelEnd;
QTextCursor cursor= _textEdit-> textCursor();
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
_textEdit-> setTextCursor(cursor);
}
_bSuggest= false;
showCarret();
_currentTyper-> flush();
}
void TyperWrapper::clear()
{
flush();
_alphaSwitch.init();
updateStatus();
_textEdit-> selectAll();
_textEdit-> insertPlainText ("");
_iSelBegin= _iSelEnd= 0;
_lastKey= KEY_NULL;
_bSuggest= false;
_bAtEnd= false;
_strSuggest= "";
showCarret();
}
void TyperWrapper::setT9 (bool bT9)
{
if (_currentTyper) {
/// Destroy current typer.
flush();
delete _currentTyper;
}
if (bT9)
_currentTyper= new TyperT9(this);
else
_currentTyper= new TyperStd(this);
updateStatus();
}
void TyperWrapper::showCarret()
{
/// Show extra carret
QTextCharFormat fUnderline;
fUnderline.setFontUnderline (true);
QTextCursor cursor= _textEdit-> textCursor();
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
if (!_bAtEnd &&cursor.atEnd()) {
_bAtEnd= true;
cursor.insertText (" ");
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
}
cursor.movePosition (QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
cursor.setCharFormat (fUnderline);
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
_textEdit-> setTextCursor (cursor);
}
void TyperWrapper::hideCarret()
{
/// Hide extra carret
QTextCharFormat fDef;
QTextCursor cursor= _textEdit-> textCursor();
if (_bAtEnd) {
cursor.deleteChar();
_bAtEnd= false;
}
/*cursor.select (QTextCursor::Document);*/
cursor.movePosition (QTextCursor::Start, QTextCursor::MoveAnchor);
cursor.movePosition (QTextCursor::End, QTextCursor::KeepAnchor);
cursor.setCharFormat (fDef);
cursor.setPosition (_iSelBegin, QTextCursor::MoveAnchor);
cursor.setPosition (_iSelEnd, QTextCursor::KeepAnchor);
_textEdit-> setTextCursor (cursor);
}
void TyperWrapper::updateStatus()
{
/// Update engine status label
QString status= _currentTyper-> getStatusPrefix();
status.append ("<b>");
status.append (_alphaSwitch.getStatus());
status.append ("</b>");
_labelEngineStatus-> setText (status);
}
void TyperWrapper::startTimer()
{
if (_iTimer<=0)
///< Timer disabled
return;
_timer.start (_iTimer);
}
void TyperWrapper::advertiseTextSize()
{
int iSize= (_textEdit-> toPlainText()).length();
if (_bAtEnd)
iSize--;
if (iSize== _iTextSize)
return;
_iTextSize= iSize;
emit textSizeChanged (iSize);
}
void TyperWrapper::notify()
{
_alphaSwitch.notify();
updateStatus();
}
TyperStd::TyperStd (TyperWrapper *parent):
_parent(parent), _lastKey(KEY_NULL), _currentRotator(0)
{
}
TyperStd::~TyperStd()
{
delete _currentRotator;
}
void TyperStd::keyPressed (EKey key)
{
if (_currentRotator && key== _lastKey) {
///< Rotate
_currentRotator-> rotate();
putSuggest();
return;
}
if (key== KEY_C) {
///< Delete last
_parent-> putSuggest("");
flush();
}
if (_currentRotator)
///< Write now last character
_parent->flush();
/// Create new rotator
switch (key) {
case KEY_DOT: _currentRotator= new Rotator (".,?-:10()@;"); break;
case KEY_ABC: _currentRotator= new Rotator ("abc2"); break;
case KEY_DEF: _currentRotator= new Rotator ("def3"); break;
case KEY_GHI: _currentRotator= new Rotator ("ghi4"); break;
case KEY_JKL: _currentRotator= new Rotator ("jkl5"); break;
case KEY_MNO: _currentRotator= new Rotator ("mno6"); break;
case KEY_PQRS: _currentRotator= new Rotator ("pqrs7"); break;
case KEY_TUV: _currentRotator= new Rotator ("tuv8"); break;
case KEY_WXYZ: _currentRotator= new Rotator ("wxyz9"); break;
default: return;
}
putSuggest();
_parent-> notify();
_lastKey= key;
}
void TyperStd::flush()
{
delete _currentRotator;
_currentRotator= 0;
}
QString TyperStd::getStatusPrefix()
{
return QString(); ///< No extra status
}
void TyperStd::putSuggest()
{
_parent->putSuggest (_parent->translate (_currentRotator-> getCurrent()));
}
void RotatorT9::add (const QString &str)
{
_list.append (str);
reset();
}
void RotatorT9::rotate()
{
if (++_now== _list.end())
reset();
}
QString RotatorT9::getCurrent()
{
if (empty())
return QString();
else
return *_now;
}
QChar DictionaryT9::keyToHashChar (EKey key)
{
switch (key) {
case KEY_DOT: return '1';
case KEY_ABC: return '2';
case KEY_DEF: return '3';
case KEY_GHI: return '4';
case KEY_JKL: return '5';
case KEY_MNO: return '6';
case KEY_PQRS: return '7';
case KEY_TUV: return '8';
case KEY_WXYZ: return '9';
default: return '0';
}
}
QChar DictionaryT9::charToHashChar (QChar c)
{
if (c >= '1' && c <= '9')
///< Digit
return c;
if (QString (".,?-:0()@;+*/").contains (c))
///< No alpha nor digit
return '1';
/// Try interpret as alpha
c= c.toUpper();
if (c >= 'A' && c <= 'C') return '2';
if (c >= 'D' && c <= 'F') return '3';
if (c >= 'G' && c <= 'I') return '4';
if (c >= 'J' && c <= 'L') return '5';
if (c >= 'M' && c <= 'O') return '6';
if (c >= 'P' && c <= 'S') return '7';
if (c >= 'T' && c <= 'V') return '8';
if (c >= 'W' && c <= 'Z') return '9';
/// Non-writable character?
return '0';
}
QString DictionaryT9::wordToHash (const QString &word)
{
int iLength= word.length();
QString hash;
for (int i=0; i<iLength; i++)
hash.append (charToHashChar (word [i]));
return hash;
}
void DictionaryT9::add (const QString &word)
{
_hashTable [wordToHash (word)].insert (word);
}
RotatorT9 *DictionaryT9::createRotator (const QString &hash)
{
int iHashLength= hash.length();
/// Build set of suggested strings for given hash
QSet<QString> set;
/// TODO: Fix effectivity problem (sequentially pass trough hash table)
for (THashTable::const_iterator iHashItem= _hashTable.begin(); iHashItem!= _hashTable.end(); iHashItem++) {
QString hashCurrent= iHashItem.key();
hashCurrent.truncate (iHashLength);
if (hashCurrent!= hash)
///< Hash not match
continue;
const TSet &list= iHashItem.value();
for (TSet::const_iterator i= list.begin(); i!= list.end(); i++) {
QString val= *i;
val.truncate (iHashLength);
set.insert (val); ///< Insert suggested string to set
}
}
if (set.empty())
///< No sugested string found for given hash
return 0;
/// Create rotator from set
RotatorT9 *r= new RotatorT9;
for (QSet<QString>::const_iterator i= set.begin(); i!= set.end(); i++)
r-> add (*i);
return r;
}
void FileDictionaryT9::load (const char *szFileName)
{
QFile file (szFileName);
if (!file.open (QIODevice::ReadOnly))
return;
QString word;
for (QTextStream stream (&file); !stream.atEnd(); stream >> word)
add (word);
file.close();
}
void FileDictionaryT9::save (const char *szFileName)
{
QFile file (szFileName);
if (!file.open (QIODevice::WriteOnly))
return;
QTextStream stream (&file);
for (THashTable::const_iterator iList= _hashTable.begin(); iList!= _hashTable.end(); iList++) {
const TSet &list= *iList;
for (TSet::const_iterator i= list.begin(); i!= list.end(); i++)
stream << *i << "\n";
}
file.close();
}
AutoLoadSaveDictionaryT9::AutoLoadSaveDictionaryT9()
{
/// load base set
const QString baseSet ("abcdefghijklmnopqrstuvwxyz0123456789.,?-:()@;*+/");
const int iLength= baseSet.length();
for (int i=0; i< iLength; i++)
add (QString (baseSet [i]));
/// load dictionary from default location
load();
/// load dictionary from files given in command-line arguments
const QStringList args (QCoreApplication::arguments());
const QStringList fileList (args.filter (QRegExp ("^[^-]")));
for (QStringList::const_iterator i= fileList.begin(); i!= fileList.end(); i++)
load ((*i).toAscii());
}
AutoLoadSaveDictionaryT9::~AutoLoadSaveDictionaryT9()
{
/// This can be useful in future
//save();
}
TyperT9::TyperT9 (TyperWrapper *parent):
_parent(parent),
_dict(parent->_dictT9),
_currentRotator(0)
{
}
TyperT9::~TyperT9()
{
delete _currentRotator;
}
void TyperT9::keyPressed(EKey key)
{
switch (key) {
case KEY_ROT:
///< Rotation
if (!_currentRotator)
return;
_currentRotator-> rotate();
break;
case KEY_C:
///< Backspace
_hashString.chop (1);
_isUpperVector.remove (_isUpperVector.size()-1);
if (_hashString.isEmpty()) {
_parent-> putSuggest ("");
_parent-> flush();
return;
}
delete _currentRotator;
_currentRotator= _dict.createRotator (_hashString);
break;
default:
///< Key pressed (incremental search)
_hashString.append (DictionaryT9::keyToHashChar (key));
_isUpperVector.append (_parent-> isUpper());
delete _currentRotator;
_currentRotator= _dict.createRotator (_hashString);
if (!_currentRotator) {
_hashString.chop (1);
_isUpperVector.remove (_isUpperVector.size()-1);
} else {
_parent-> notify();
}
}
if (_currentRotator)
///< Display suggestion to user
putSuggest();
}
void TyperT9::flush()
{
/// Stop rotating now.
delete _currentRotator;
_currentRotator=0;
_hashString.clear();
_isUpperVector.clear();
}
QString TyperT9::getStatusPrefix()
{
return "<font color=\"#E00000\">T9</font> ";
}
void TyperT9::putSuggest()
{
QString in= (_currentRotator-> getCurrent()).toLower();
QString out;
int iLength= _isUpperVector.size();
for (int i=0; i< iLength; i++) {
QChar c= in[i];
out.append (_isUpperVector[i] ? c.toUpper() : c);
}
_parent -> putSuggest (out);
}