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