Diffstat (limited to 'inputmethods/handwriting/qimpenmatch.cpp') (more/less context) (show whitespace changes)
-rw-r--r-- | inputmethods/handwriting/qimpenmatch.cpp | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/inputmethods/handwriting/qimpenmatch.cpp b/inputmethods/handwriting/qimpenmatch.cpp new file mode 100644 index 0000000..0d3e25a --- a/dev/null +++ b/inputmethods/handwriting/qimpenmatch.cpp | |||
@@ -0,0 +1,365 @@ | |||
1 | /********************************************************************** | ||
2 | ** Copyright (C) 2000 Trolltech AS. All rights reserved. | ||
3 | ** | ||
4 | ** This file is part of Qtopia Environment. | ||
5 | ** | ||
6 | ** This file may be distributed and/or modified under the terms of the | ||
7 | ** GNU General Public License version 2 as published by the Free Software | ||
8 | ** Foundation and appearing in the file LICENSE.GPL included in the | ||
9 | ** packaging of this file. | ||
10 | ** | ||
11 | ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE | ||
12 | ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. | ||
13 | ** | ||
14 | ** See http://www.trolltech.com/gpl/ for GPL licensing information. | ||
15 | ** | ||
16 | ** Contact info@trolltech.com if any conditions of this licensing are | ||
17 | ** not clear to you. | ||
18 | ** | ||
19 | **********************************************************************/ | ||
20 | |||
21 | #include "qimpenmatch.h" | ||
22 | |||
23 | #include <qpe/qdawg.h> | ||
24 | #include <qpe/global.h> | ||
25 | |||
26 | #include <qapplication.h> | ||
27 | #include <qtimer.h> | ||
28 | |||
29 | #include <limits.h> | ||
30 | |||
31 | #define ERROR_THRESHOLD 200000 | ||
32 | #define LOOKAHEAD_ERROR 2500 | ||
33 | //#define DEBUG_QIMPEN | ||
34 | |||
35 | QIMPenMatch::QIMPenMatch( QObject *parent, const char *name ) | ||
36 | : QObject( parent, name ) | ||
37 | { | ||
38 | strokes.setAutoDelete( TRUE ); | ||
39 | wordChars.setAutoDelete( TRUE ); | ||
40 | wordMatches.setAutoDelete( TRUE ); | ||
41 | |||
42 | multiTimer = new QTimer( this ); | ||
43 | connect( multiTimer, SIGNAL(timeout()), this, SLOT(endMulti()) ); | ||
44 | |||
45 | prevMatchChar = 0; | ||
46 | prevMatchError = INT_MAX; | ||
47 | charSet = 0; | ||
48 | multiCharSet = 0; | ||
49 | multiTimeout = 500; | ||
50 | canErase = FALSE; | ||
51 | doWordMatching = true; | ||
52 | } | ||
53 | |||
54 | QIMPenMatch::~QIMPenMatch() | ||
55 | { | ||
56 | } | ||
57 | |||
58 | void QIMPenMatch::setCharSet( QIMPenCharSet *cs ) | ||
59 | { | ||
60 | charSet = cs; | ||
61 | } | ||
62 | |||
63 | void QIMPenMatch::beginStroke() | ||
64 | { | ||
65 | multiTimer->stop(); | ||
66 | } | ||
67 | |||
68 | void QIMPenMatch::strokeEntered( QIMPenStroke *st ) | ||
69 | { | ||
70 | #ifdef DEBUG_QIMPEN | ||
71 | qDebug( "---------- new stroke -------------" ); | ||
72 | #endif | ||
73 | strokes.append( new QIMPenStroke( *st ) ); | ||
74 | |||
75 | QIMPenChar testChar; | ||
76 | QIMPenStrokeIterator it(strokes); | ||
77 | for ( ; it.current(); ++it ) { | ||
78 | testChar.addStroke( it.current() ); | ||
79 | } | ||
80 | |||
81 | QIMPenCharMatchList ml; | ||
82 | if ( strokes.count() > 1 && multiCharSet ) { | ||
83 | #ifdef DEBUG_QIMPEN | ||
84 | qDebug( "Matching against multi set" ); | ||
85 | #endif | ||
86 | ml = multiCharSet->match( &testChar ); | ||
87 | } else { | ||
88 | #ifdef DEBUG_QIMPEN | ||
89 | qDebug( "Matching against single set" ); | ||
90 | #endif | ||
91 | ml = charSet->match( &testChar ); | ||
92 | } | ||
93 | |||
94 | processMatches( ml ); | ||
95 | } | ||
96 | |||
97 | void QIMPenMatch::processMatches( QIMPenCharMatchList &ml ) | ||
98 | { | ||
99 | #ifdef DEBUG_QIMPEN | ||
100 | qDebug( "Entering strokes.count() = %d", strokes.count() ); | ||
101 | #endif | ||
102 | QIMPenCharMatch candidate1 = { INT_MAX, 0 }; | ||
103 | QIMPenCharMatch candidate2 = { INT_MAX, 0 }; | ||
104 | QIMPenCharMatchList ml2; | ||
105 | |||
106 | if ( ml.count() ) {//&& | ||
107 | // ml.first().penChar->penStrokes().count() == strokes.count() ) { | ||
108 | candidate1 = ml.first(); | ||
109 | #ifdef DEBUG_QIMPEN | ||
110 | qDebug( QString("Candidate1 = %1").arg(QChar(candidate1.penChar->character())) ); | ||
111 | #endif | ||
112 | } | ||
113 | |||
114 | if ( strokes.count() > 1 ) { | ||
115 | // See if the last stroke can match a new character | ||
116 | QIMPenChar testChar; | ||
117 | QIMPenStroke *st = strokes.at(strokes.count()-1); | ||
118 | testChar.addStroke( st ); | ||
119 | ml2 = charSet->match( &testChar ); | ||
120 | if ( ml2.count() ) { | ||
121 | candidate2 = ml2.first(); | ||
122 | #ifdef DEBUG_QIMPEN | ||
123 | qDebug( QString("Candidate2 = %1").arg(QChar(candidate2.penChar->character())) ); | ||
124 | #endif | ||
125 | } | ||
126 | } | ||
127 | |||
128 | bool eraseLast = FALSE; | ||
129 | bool output = TRUE; | ||
130 | |||
131 | if ( candidate1.penChar && candidate2.penChar ) { | ||
132 | // Hmmm, a multi-stroke or a new character are both possible. | ||
133 | // Bias the multi-stroke case. | ||
134 | if ( QMAX(candidate2.error, prevMatchError)*3 < candidate1.error ) { | ||
135 | int i = strokes.count()-1; | ||
136 | while ( i-- ) { | ||
137 | strokes.removeFirst(); | ||
138 | emit removeStroke(); | ||
139 | } | ||
140 | prevMatchChar = candidate2.penChar; | ||
141 | prevMatchError = candidate2.error; | ||
142 | multiCharSet = charSet; | ||
143 | ml = ml2; | ||
144 | #ifdef DEBUG_QIMPEN | ||
145 | qDebug( "** Using Candidate2" ); | ||
146 | #endif | ||
147 | } else { | ||
148 | if ( (prevMatchChar->character() >> 16) != Qt::Key_Backspace && | ||
149 | (prevMatchChar->character() >> 16) < QIMPenChar::ModeBase ) | ||
150 | eraseLast = TRUE; | ||
151 | prevMatchChar = candidate1.penChar; | ||
152 | prevMatchError = candidate1.error; | ||
153 | #ifdef DEBUG_QIMPEN | ||
154 | qDebug( "** Using Candidate1, with erase" ); | ||
155 | #endif | ||
156 | } | ||
157 | } else if ( candidate1.penChar ) { | ||
158 | if ( strokes.count() != 1 ) | ||
159 | eraseLast = TRUE; | ||
160 | else | ||
161 | multiCharSet = charSet; | ||
162 | prevMatchChar = candidate1.penChar; | ||
163 | prevMatchError = candidate1.error; | ||
164 | #ifdef DEBUG_QIMPEN | ||
165 | qDebug( "** Using Candidate1" ); | ||
166 | #endif | ||
167 | } else if ( candidate2.penChar ) { | ||
168 | int i = strokes.count()-1; | ||
169 | while ( i-- ) { | ||
170 | strokes.removeFirst(); | ||
171 | emit removeStroke(); | ||
172 | } | ||
173 | prevMatchChar = candidate2.penChar; | ||
174 | prevMatchError = candidate2.error; | ||
175 | multiCharSet = charSet; | ||
176 | ml = ml2; | ||
177 | #ifdef DEBUG_QIMPEN | ||
178 | qDebug( "** Using Candidate2" ); | ||
179 | #endif | ||
180 | } else { | ||
181 | if ( !ml.count() ) { | ||
182 | #ifdef DEBUG_QIMPEN | ||
183 | qDebug( "** Failed" ); | ||
184 | #endif | ||
185 | canErase = FALSE; | ||
186 | } else { | ||
187 | #ifdef DEBUG_QIMPEN | ||
188 | qDebug( "Need more strokes" ); | ||
189 | #endif | ||
190 | if ( strokes.count() == 1 ) | ||
191 | canErase = FALSE; | ||
192 | multiCharSet = charSet; | ||
193 | } | ||
194 | output = FALSE; | ||
195 | emit noMatch(); | ||
196 | } | ||
197 | |||
198 | if ( eraseLast && canErase ) { | ||
199 | #ifdef DEBUG_QIMPEN | ||
200 | qDebug( "deleting last" ); | ||
201 | #endif | ||
202 | emit erase(); | ||
203 | wordChars.removeLast(); | ||
204 | wordEntered.truncate( wordEntered.length() - 1 ); | ||
205 | } | ||
206 | |||
207 | if ( output ) { | ||
208 | emit matchedCharacters( ml ); | ||
209 | uint code = prevMatchChar->character() >> 16; | ||
210 | if ( code < QIMPenChar::ModeBase ) { | ||
211 | updateWordMatch( ml ); | ||
212 | emit keypress( prevMatchChar->character() ); | ||
213 | } | ||
214 | canErase = TRUE; | ||
215 | } | ||
216 | |||
217 | if ( strokes.count() ) | ||
218 | multiTimer->start( multiTimeout, TRUE ); | ||
219 | } | ||
220 | |||
221 | void QIMPenMatch::updateWordMatch( QIMPenCharMatchList &ml ) | ||
222 | { | ||
223 | if ( !ml.count() || !doWordMatching ) | ||
224 | return; | ||
225 | int ch = ml.first().penChar->character(); | ||
226 | QChar qch( ch ); | ||
227 | int code = ch >> 16; | ||
228 | if ( qch.isPunct() || qch.isSpace() || | ||
229 | code == Qt::Key_Enter || code == Qt::Key_Return || | ||
230 | code == Qt::Key_Tab || code == Qt::Key_Escape ) { | ||
231 | //qDebug( "Word Matching: Clearing word" ); | ||
232 | wordChars.clear(); | ||
233 | wordMatches.clear(); | ||
234 | wordEntered = QString(); | ||
235 | } else if ( code == Qt::Key_Backspace ) { | ||
236 | //qDebug( "Word Matching: Handle backspace" ); | ||
237 | wordChars.removeLast(); | ||
238 | wordEntered.truncate( wordEntered.length() - 1 ); | ||
239 | matchWords(); | ||
240 | } else { | ||
241 | QIMPenChar *matchCh; | ||
242 | |||
243 | wordChars.append( new QIMPenCharMatchList() ); | ||
244 | wordEntered += ml.first().penChar->character(); | ||
245 | |||
246 | QIMPenCharMatchList::Iterator it; | ||
247 | for ( it = ml.begin(); it != ml.end(); ++it ) { | ||
248 | matchCh = (*it).penChar; | ||
249 | |||
250 | if ( matchCh->penStrokes().count() == strokes.count() ) { | ||
251 | QChar ch(matchCh->character()); | ||
252 | if ( !ch.isPunct() && !ch.isSpace() ) { | ||
253 | wordChars.last()->append( QIMPenCharMatch( (*it) ) ); | ||
254 | } | ||
255 | } | ||
256 | } | ||
257 | matchWords(); | ||
258 | } | ||
259 | if ( !wordMatches.count() || wordMatches.getFirst()->word != wordEntered ) | ||
260 | wordMatches.prepend( new MatchWord( wordEntered, 0 ) ); | ||
261 | emit matchedWords( wordMatches ); | ||
262 | } | ||
263 | |||
264 | void QIMPenMatch::matchWords() | ||
265 | { | ||
266 | if ( wordEntered.length() > 0 ) { | ||
267 | // more leaniency if we don't have many matches | ||
268 | if ( badMatches < 200 ) | ||
269 | errorThreshold += (200 - badMatches) * 100; | ||
270 | } else | ||
271 | errorThreshold = ERROR_THRESHOLD; | ||
272 | wordMatches.clear(); | ||
273 | goodMatches = 0; | ||
274 | badMatches = 0; | ||
275 | if ( wordChars.count() > 0 ) { | ||
276 | maxGuess = (int)wordChars.count() * 2; | ||
277 | if ( maxGuess < 3 ) | ||
278 | maxGuess = 3; | ||
279 | QString str; | ||
280 | scanDict( Global::fixedDawg().root(), 0, str, 0 ); | ||
281 | /* | ||
282 | QListIterator<MatchWord> it( wordMatches); | ||
283 | for ( ; it.current(); ++it ) { | ||
284 | qDebug( QString("Match word: %1").arg(it.current()->word) ); | ||
285 | } | ||
286 | */ | ||
287 | } | ||
288 | //qDebug( "Possibles: Good %d, total %d", goodMatches, wordMatches.count() ); | ||
289 | wordMatches.sort(); | ||
290 | } | ||
291 | |||
292 | void QIMPenMatch::scanDict( const QDawg::Node* n, int ipos, const QString& str, int error ) | ||
293 | { | ||
294 | if ( !n ) | ||
295 | return; | ||
296 | if ( error / (ipos+1) > errorThreshold ) | ||
297 | return; | ||
298 | |||
299 | while (n) { | ||
300 | if ( goodMatches > 20 ) | ||
301 | break; | ||
302 | if ( ipos < (int)wordChars.count() ) { | ||
303 | int i; | ||
304 | QChar testCh = QChar(n->letter()); | ||
305 | QIMPenCharMatchList::Iterator it; | ||
306 | for ( i = 0, it = wordChars.at(ipos)->begin(); | ||
307 | it != wordChars.at(ipos)->end() && i < 8; ++it, i++ ) { | ||
308 | QChar ch( (*it).penChar->character() ); | ||
309 | if ( ch == testCh || ( !ipos && ch.lower() == testCh.lower() ) ) { | ||
310 | int newerr = error + (*it).error; | ||
311 | if ( testCh.category() == QChar::Letter_Uppercase ) | ||
312 | ch = testCh; | ||
313 | QString newstr( str + ch ); | ||
314 | if ( n->isWord() && ipos == (int)wordChars.count() - 1 ) { | ||
315 | wordMatches.append( new MatchWord( newstr, newerr ) ); | ||
316 | goodMatches++; | ||
317 | } | ||
318 | scanDict( n->jump(), ipos+1, newstr, newerr ); | ||
319 | } | ||
320 | } | ||
321 | } else if ( badMatches < 200 && ipos < maxGuess ) { | ||
322 | int d = ipos - wordChars.count(); | ||
323 | int newerr = error + ERROR_THRESHOLD + LOOKAHEAD_ERROR*d; | ||
324 | QString newstr( str + n->letter() ); | ||
325 | if ( n->isWord() ) { | ||
326 | wordMatches.append( new MatchWord( newstr, newerr ) ); | ||
327 | badMatches++; | ||
328 | } | ||
329 | scanDict( n->jump(), ipos+1, newstr, newerr ); | ||
330 | } | ||
331 | n = n->next(); | ||
332 | } | ||
333 | } | ||
334 | |||
335 | void QIMPenMatch::backspace() | ||
336 | { | ||
337 | wordChars.removeLast(); | ||
338 | wordEntered.truncate( wordEntered.length() - 1 ); | ||
339 | matchWords(); | ||
340 | if ( !wordMatches.count() || wordMatches.getFirst()->word != wordEntered ) | ||
341 | wordMatches.prepend( new MatchWord( wordEntered, 0 ) ); | ||
342 | emit matchedWords( wordMatches ); | ||
343 | if ( wordEntered.length() ) | ||
344 | canErase = TRUE; | ||
345 | } | ||
346 | |||
347 | void QIMPenMatch::endMulti() | ||
348 | { | ||
349 | int i = strokes.count(); | ||
350 | while ( i-- ) | ||
351 | emit removeStroke(); | ||
352 | strokes.clear(); | ||
353 | multiCharSet = 0; | ||
354 | } | ||
355 | |||
356 | void QIMPenMatch::resetState() | ||
357 | { | ||
358 | if ( !wordEntered.isEmpty() ) { | ||
359 | wordChars.clear(); | ||
360 | wordMatches.clear(); | ||
361 | wordEntered = QString(); | ||
362 | emit matchedWords( wordMatches ); | ||
363 | canErase = FALSE; | ||
364 | } | ||
365 | } | ||