File indexing completed on 2024-12-01 13:34:29
0001 /* 0002 SPDX-FileCopyrightText: 1999-2001 Lubos Lunak <l.lunak@kde.org> 0003 SPDX-FileCopyrightText: 2008 Michael Jansen <kde@michael-jansen.biz> 0004 0005 SPDX-License-Identifier: LGPL-2.0-only 0006 */ 0007 0008 /* 0009 This file includes code from EasyStroke, Copyright (c) 2008-2009, 0010 Thomas Jaeger <ThJaeger@gmail.com> 0011 http://easystroke.sourceforge.net 0012 0013 Permission to use, copy, modify, and/or distribute this software for any 0014 purpose with or without fee is hereby granted, provided that the above 0015 copyright notice and this permission notice appear in all copies. 0016 0017 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 0018 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 0019 AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 0020 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 0021 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 0022 OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 0023 PERFORMANCE OF THIS SOFTWARE. 0024 */ 0025 0026 #include <math.h> 0027 0028 #include "action_data/action_data.h" 0029 #include "triggers/gestures.h" 0030 #include "triggers/triggers.h" 0031 #include "windows_handler.h" 0032 0033 #include <QFile> 0034 0035 #include <KConfigGroup> 0036 #include <QDebug> 0037 0038 namespace KHotKeys 0039 { 0040 GestureTriggerVisitor::~GestureTriggerVisitor() 0041 { 0042 } 0043 0044 GestureTrigger::GestureTrigger(ActionData *data_P, const StrokePoints &pointdata_P) 0045 : Trigger(data_P) 0046 , _pointdata(pointdata_P) 0047 { 0048 } 0049 0050 GestureTrigger::~GestureTrigger() 0051 { 0052 gesture_handler->unregister_handler(this, SLOT(handle_gesture(StrokePoints))); 0053 } 0054 0055 void GestureTrigger::accept(TriggerVisitor &visitor) 0056 { 0057 if (GestureTriggerVisitor *v = dynamic_cast<GestureTriggerVisitor *>(&visitor)) { 0058 v->visit(*this); 0059 } else { 0060 qDebug() << "Visitor error"; 0061 } 0062 } 0063 0064 void GestureTrigger::activate(bool activate_P) 0065 { 0066 if (activate_P) 0067 gesture_handler->register_handler(this, SLOT(handle_gesture(StrokePoints))); 0068 else 0069 gesture_handler->unregister_handler(this, SLOT(handle_gesture(StrokePoints))); 0070 } 0071 0072 void GestureTrigger::cfg_write(KConfigGroup &cfg_P) const 0073 { 0074 // we want to write using KConfig, so we'll need strings - 0075 // one for each attribute of each point. 0076 QStringList strings; 0077 0078 int n = _pointdata.size(); 0079 0080 for (int i = 0; i < n; i++) { 0081 strings.append(QString::number(_pointdata[i].s)); 0082 strings.append(QString::number(_pointdata[i].delta_s)); 0083 strings.append(QString::number(_pointdata[i].angle)); 0084 strings.append(QString::number(_pointdata[i].x)); 0085 strings.append(QString::number(_pointdata[i].y)); 0086 } 0087 0088 base::cfg_write(cfg_P); 0089 cfg_P.writeEntry("GesturePointData", strings); 0090 cfg_P.writeEntry("Type", "GESTURE"); // overwrites value set in base::cfg_write() 0091 } 0092 0093 Trigger *GestureTrigger::copy(ActionData *data_P) const 0094 { 0095 qDebug() << "GestureTrigger::copy()"; 0096 return new GestureTrigger(data_P ? data_P : data, pointData()); 0097 } 0098 0099 const QString GestureTrigger::description() const 0100 { 0101 return i18n("Gesture trigger"); 0102 } 0103 0104 const StrokePoints &GestureTrigger::pointData() const 0105 { 0106 return _pointdata; 0107 } 0108 0109 void GestureTrigger::setPointData(const StrokePoints &data) 0110 { 0111 _pointdata = data; 0112 } 0113 0114 void GestureTrigger::setPointData(const QStringList &strings) 0115 { 0116 // number of points that can be read 0117 // (each string is one of 5 coordinates) 0118 int n = strings.length() / 5; 0119 _pointdata.resize(n); 0120 0121 for (int i = 0; i < n; i++) { 0122 _pointdata[i].s = strings[i * 5 + 0].toDouble(); 0123 _pointdata[i].delta_s = strings[i * 5 + 1].toDouble(); 0124 _pointdata[i].angle = strings[i * 5 + 2].toDouble(); 0125 _pointdata[i].x = strings[i * 5 + 3].toDouble(); 0126 _pointdata[i].y = strings[i * 5 + 4].toDouble(); 0127 } 0128 0129 #pragma CHECKME 0130 #if 0 0131 if(n < 5) 0132 { 0133 _pointdata.clear(); 0134 importKde3Gesture(*_config); 0135 } 0136 #endif 0137 } 0138 0139 void GestureTrigger::handle_gesture(const StrokePoints &pointdata_P) 0140 { 0141 qreal score; 0142 score = comparePointData(pointdata_P, _pointdata); 0143 0144 if (score > 0.7) { 0145 emit gotScore(data, score); 0146 } 0147 } 0148 0149 // try to import a gesture from KDE3 times which is composed of a string of 0150 // numbers 0151 void GestureTrigger::setKDE3Gesture(const QString &gestureCode) 0152 { 0153 if (gestureCode.isEmpty()) { 0154 _pointdata.clear(); 0155 return; 0156 } 0157 0158 Stroke stroke; 0159 0160 // the old format has very little data, so we'll interpolate a little 0161 // to make it work with the new format. 0162 // as the stroke expects the data in integer format we'll use large numbers. 0163 int oldx = -1; 0164 int oldy = -1; 0165 int newx = 0; 0166 int newy = 0; 0167 0168 for (int i = 0; i < gestureCode.length(); i++) { 0169 switch (gestureCode[i].toLatin1()) { 0170 case '1': 0171 newx = 0; 0172 newy = 2000; 0173 break; 0174 case '2': 0175 newx = 1000; 0176 newy = 2000; 0177 break; 0178 case '3': 0179 newx = 2000; 0180 newy = 2000; 0181 break; 0182 case '4': 0183 newx = 0; 0184 newy = 1000; 0185 break; 0186 case '5': 0187 newx = 1000; 0188 newy = 1000; 0189 break; 0190 case '6': 0191 newx = 2000; 0192 newy = 1000; 0193 break; 0194 case '7': 0195 newx = 0; 0196 newy = 0; 0197 break; 0198 case '8': 0199 newx = 1000; 0200 newy = 0; 0201 break; 0202 case '9': 0203 newx = 2000; 0204 newy = 0; 0205 break; 0206 0207 default: 0208 return; 0209 } 0210 0211 // interpolate 0212 if (oldx != -1) { 0213 stroke.record(oldx + 1 * (newx - oldx) / 4.0, oldy + 1 * (newy - oldy) / 4.0); 0214 stroke.record(oldx + 2 * (newx - oldx) / 4.0, oldy + 2 * (newy - oldy) / 4.0); 0215 stroke.record(oldx + 3 * (newx - oldx) / 4.0, oldy + 3 * (newy - oldy) / 4.0); 0216 } 0217 0218 // add the one point that is really known 0219 stroke.record(newx, newy); 0220 0221 oldx = newx; 0222 oldy = newy; 0223 } 0224 0225 // the calculations for the new format chop off some points at the end. 0226 // that's usually no problem, but here we'll want to compensate 0227 stroke.record(newx, newy); 0228 stroke.record(newx, newy); 0229 stroke.record(newx, newy); 0230 0231 _pointdata = stroke.processData(); 0232 } 0233 0234 // Create a score for how well two strokes match. 0235 // Algorithm taken from EasyStroke 0.4.1, modified to work in C++ and commented 0236 // for better maintainability. The algorithm logic should still be the same. 0237 // 0238 // The basic idea of the algorithm is to find points that are similar to each 0239 // other by iterating over both strokes and trying to match points so that the 0240 // cost (that is mostly defined by angle differences and the cost of the 0241 // cheapest predecessor) becomes minimal. 0242 // When the matching has arrived at the last point of both strokes the cost for 0243 // this match is taken as the overall cost and then transformed into a score 0244 // between 0 and 1. 0245 // 0246 // 0247 // If you're really interested in why this works you can have a look at the 0248 // small mathematical tex-document that is distributed with EasyStroke. 0249 // Probably won't help much though. 0250 qreal GestureTrigger::comparePointData(const StrokePoints &a, const StrokePoints &b) const 0251 { 0252 // 0.2 will be a very large value in these computations 0253 const qreal stroke_infinity = 0.2; 0254 0255 int m = a.size() - 1; 0256 int n = b.size() - 1; 0257 0258 // if there's too little data: return score of 0 0259 if (m < 1 || n < 1) 0260 return 0.0; 0261 0262 // array "cost" to save results of the cost calculations. 0263 // set all cells of cost to infinity. 0264 // we use nested vectors instead of a real array because the array size is 0265 // determined at runtime. 0266 QVector<QVector<qreal>> cost(m + 1, QVector<qreal>(n + 1, stroke_infinity)); 0267 0268 // we start at the beginnings of both strokes - with cost 0. 0269 cost[0][0] = 0.0; 0270 0271 // iterate over all cols and rows except for the last 0272 // (last one would be aborted anyway) 0273 // Attention: We don' use absolute coordinates in this method! x and y are 0274 // the indices of the current row and column in the cost-matrix. 0275 for (int x = 0; x < m; x++) { 0276 for (int y = 0; y < n; y++) { 0277 // cost already too high -> forget this one, it won't get better 0278 if (cost[x][y] >= stroke_infinity) 0279 continue; 0280 0281 qreal sx = a[x].s; 0282 qreal sy = b[y].s; 0283 0284 // these will get incremented later in the function. 0285 // they define how large the area is we'll jump around in to look 0286 // for cheaper ways 0287 int max_x = x; 0288 int max_y = y; 0289 0290 // this will be used to limit the number of iterations 0291 int k = 0; 0292 0293 // we'll have to control the repeated execution of code which 0294 // _should_ be a function, but it needs to access too 0295 // many of the variables of this function here. 0296 // rewriting all of this as a class makes it even more complicated, 0297 // so I'll define some variables that decide how often the chunk of 0298 // code should be run. 0299 0300 // jumpToEnd: make one last jump to cell m,n and end this 0301 bool jumpToEnd = false; 0302 bool iterateOverX2 = false, iterateOverY2 = false; 0303 0304 // variables that give the position in the matrix we want to jump 0305 // to. we'll iterate over these. 0306 int x2, y2; 0307 0308 // artificially limit the number of iterations. the changing of 0309 // max_x and max_y ensures we don't do the same all the time 0310 while (k < 4) { 0311 // first we set up our logic controllers 0312 0313 // if difference between s at max_x+1 and s at x is bigger 0314 // than difference between s at max_y+1 and y... 0315 if (a[max_x + 1].s - sx > b[max_y + 1].s - sy) { 0316 // widen our search in y-direction 0317 max_y++; 0318 0319 // if we're at the side of the matrix make the step to 0320 // the end and break 0321 if (max_y == n) { 0322 jumpToEnd = true; 0323 x2 = m; 0324 y2 = n; 0325 } else // check the cells with the new max_y 0326 { 0327 iterateOverX2 = true; 0328 x2 = x + 1; 0329 y2 = max_y; 0330 } 0331 0332 } 0333 0334 // differences bigger or equal in y-direction 0335 // -> same thing with y and x switched 0336 else { 0337 // widen our search in x-direction 0338 max_x++; 0339 0340 // if we're at the side -> step to the end and break 0341 if (max_x == m) { 0342 jumpToEnd = true; 0343 x2 = m; 0344 y2 = n; 0345 } else // check the cells with the new max_x 0346 { 0347 iterateOverY2 = true; 0348 x2 = max_x; 0349 y2 = y + 1; 0350 } 0351 } 0352 0353 // contained in this loop we have the code that 0354 // performs the step from current x,y to x2,y2 and 0355 // calculates the cost for it. if a new minimal cost for a cell 0356 // is found this new cost is written to it. 0357 while (jumpToEnd || (iterateOverX2 && x2 <= max_x) || (iterateOverY2 && y2 <= max_y)) { 0358 // very small value for s 0359 const qreal epsilon = 0.000001; 0360 0361 qreal delta_sx = a[x2].s - a[x].s; 0362 qreal delta_sy = b[y2].s - b[y].s; 0363 0364 // only consider doing this if the change of positions in x 0365 // and y is comparable and not too small. 0366 if (delta_sx < delta_sy * 2.2 && delta_sy < delta_sx * 2.2 && delta_sx >= epsilon && delta_sy >= epsilon) { 0367 k++; 0368 0369 // we compute the additional cost for this step 0370 // by computing the square of the angle difference of 0371 // the origin points and weighing it with how long the 0372 // angles remain like this... 0373 qreal c = (a[x].delta_s + b[y].delta_s) * angleSquareDifference(a[x].angle, b[y].angle); 0374 0375 // and adding similar values for all cells that need to 0376 // be crossed to get to the step target (by traversing 0377 // x and y separately) 0378 for (int ix = x + 1; ix < x2; ix++) 0379 c += a[ix].delta_s * angleSquareDifference(a[ix].angle, b[y].angle); 0380 for (int iy = y + 1; iy < y2; iy++) 0381 c += b[iy].delta_s * angleSquareDifference(a[x].angle, b[iy].angle); 0382 0383 // now add that to the cost of our starting point 0384 qreal new_cost = cost[x][y] + c; 0385 0386 // if we found a cheaper origin for the target than 0387 // before: save new minimal cost 0388 if (new_cost < cost[x2][y2]) 0389 cost[x2][y2] = new_cost; 0390 } 0391 0392 // control logic 0393 0394 if (jumpToEnd) 0395 break; 0396 if (iterateOverX2) 0397 x2++; 0398 if (iterateOverY2) 0399 y2++; 0400 } 0401 0402 // if we jumped to the end we're finished with this combination 0403 // of x and y 0404 if (jumpToEnd) 0405 break; 0406 0407 // reset the logic controllers 0408 iterateOverX2 = false; 0409 iterateOverY2 = false; 0410 } 0411 } 0412 } 0413 0414 // only task remaining is returning the results 0415 0416 // target cell (m, n) now hopefully has minimal cost. 0417 // we compute our score from that. 0418 qreal score = (1.0 - 5.0 * cost[m][n]); 0419 0420 return score; 0421 } 0422 0423 // gives us the square of the difference of two angles 0424 inline qreal GestureTrigger::angleSquareDifference(qreal alpha, qreal beta) const 0425 { 0426 qreal d = alpha - beta; 0427 0428 if (d < -1.0) 0429 d += 2.0; 0430 else if (d > 1.0) 0431 d -= 2.0; 0432 0433 return (d * d); 0434 } 0435 0436 } // namespace KHotKeys