• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdelibs-4.14.38 API Reference
  • KDE Home
  • Contact Us
 

KDEUI

  • kdeui
  • shortcuts
kgesture.cpp
Go to the documentation of this file.
1/* This file is part of the KDE libraries
2 Copyright (C) 2006,2007 Andreas Hartmetz (ahartmetz@gmail.com)
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18*/
19
20#include "kgesture.h"
21#include <klocalizedstring.h>
22#include <kdebug.h>
23#include <math.h>
24#include <QStringList>
25
26inline float metric(float dx, float dy)
27{
28 //square root of that or not? - not square root has possible advantages
29 return (dx*dx + dy*dy);
30}
31
32class KShapeGesturePrivate
33{
34public:
35 KShapeGesturePrivate()
36 {
37 }
38 KShapeGesturePrivate(const KShapeGesturePrivate &other)
39 : m_shape(other.m_shape),
40 m_lengthTo(other.m_lengthTo),
41 m_curveLength(other.m_curveLength)
42 {
43 }
44 QPolygon m_shape;
45 QVector<float> m_lengthTo;
46 float m_curveLength;
47 QString m_friendlyName;
48};
49
50KShapeGesture::KShapeGesture()
51 : d(new KShapeGesturePrivate)
52{
53}
54
55
56KShapeGesture::KShapeGesture(const QPolygon &shape)
57 : d(new KShapeGesturePrivate)
58{
59 setShape(shape);
60}
61
62
63KShapeGesture::KShapeGesture(const QString &description)
64 : d(new KShapeGesturePrivate)
65{
66 QStringList sl = description.split(',');
67 d->m_friendlyName = sl.takeFirst();
68
69 bool ok = true;
70 QPolygon poly;
71 int x, y;
72 QStringList::const_iterator it = sl.constBegin();
73 while (it != sl.constEnd()) {
74 x = (*it).toInt(&ok);
75 ++it;
76 if (!ok || it == sl.constEnd())
77 break;
78 y = (*it).toInt(&ok);
79 if (!ok)
80 break;
81 ++it;
82 poly.append(QPoint(x, y));
83 }
84 if (!ok) {
85 d->m_friendlyName.clear();
86 return;
87 }
88
89 setShape(poly);
90}
91
92
93KShapeGesture::KShapeGesture(const KShapeGesture &other)
94 : d(new KShapeGesturePrivate(*(other.d)))
95{
96}
97
98
99KShapeGesture::~KShapeGesture()
100{
101 delete d;
102}
103
104
105void KShapeGesture::setShape(const QPolygon &shape)
106{
107 //Scale and translate into a 100x100 square with its
108 //upper left corner at origin.
109 d->m_shape = shape;
110 QRect bounding = shape.boundingRect();
111 //TODO: don't change aspect ratio "too much" to avoid problems with straight lines
112 //TODO: catch all bad input, like null height/width
113
114 //compensate for QRect weirdness
115 bounding.setWidth(bounding.width() - 1);
116 bounding.setHeight(bounding.height() - 1);
117
118 float xScale = bounding.width() ? 100.0 / bounding.width() : 1.0;
119 float yScale = bounding.height() ? 100.0 / bounding.height() : 1.0;
120 d->m_shape.translate(-bounding.left(), -bounding.top());
121 for (int i=0; i < d->m_shape.size(); i++) {
122 d->m_shape[i].setX((int)(xScale * (float)d->m_shape[i].x()));
123 d->m_shape[i].setY((int)(yScale * (float)d->m_shape[i].y()));
124 }
125
126 //calculate accumulated lengths of lines making up the polygon
127 Q_ASSERT(d->m_shape.size() > 1);
128 d->m_curveLength = 0.0;
129 d->m_lengthTo.clear();
130 d->m_lengthTo.reserve(d->m_shape.size());
131 d->m_lengthTo.append(d->m_curveLength);
132
133 int prevX = d->m_shape[0].x();
134 int prevY = d->m_shape[0].y();
135 for (int i=1; i < d->m_shape.size(); i++) {
136 int curX = d->m_shape[i].x();
137 int curY = d->m_shape[i].y();
138 d->m_curveLength += metric(curX-prevX, curY - prevY);
139 d->m_lengthTo.append(d->m_curveLength);
140 prevX = curX;
141 prevY = curY;
142 }
143}
144
145
146void KShapeGesture::setShapeName(const QString &friendlyName)
147{
148 d->m_friendlyName = friendlyName;
149}
150
151
152QString KShapeGesture::shapeName() const
153{
154 return d->m_friendlyName;
155}
156
157
158bool KShapeGesture::isValid() const
159{
160 return !d->m_shape.isEmpty();
161}
162
163
164QString KShapeGesture::toString() const
165{
166 if (!isValid())
167 return QString();
168
169 //TODO: what if the name contains a "," or ";"? Limit the name to letters?
170 QString ret = d->m_friendlyName;
171
172 int i;
173 for (i = 0; i < d->m_shape.size(); i++) {
174 ret.append(',');
175 ret.append(QString::number(d->m_shape[i].x()));
176 ret.append(',');
177 ret.append(QString::number(d->m_shape[i].y()));
178 }
179
180 return ret;
181}
182
183
184QByteArray KShapeGesture::toSvg(const QString &attributes) const
185{
186 if (!isValid()) {
187 return QByteArray();
188 //TODO: KDE standard debug output
189 }
190 const char *prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"
191 "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
192 "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"
193 "<svg width=\"100\" height=\"100\" version=\"1.1\" "
194 "xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M";
195 const char *epilog1 = "\" fill=\"none\" ";
196 const char *epilog2 = " /></svg>";
197 QByteArray ret(prolog);
198
199 ret.append(QString::number(d->m_shape[0].x()).toUtf8());
200 ret.append(",");
201 ret.append(QString::number(d->m_shape[0].y()).toUtf8());
202
203 for (int i=1; i < d->m_shape.size(); i++) {
204 ret.append("L");
205 ret.append(QString::number(d->m_shape[i].x()).toUtf8());
206 ret.append(",");
207 ret.append(QString::number(d->m_shape[i].y()).toUtf8());
208 }
209
210 ret.append(epilog1);
211 ret.append(attributes.toUtf8());
212 ret.append(epilog2);
213 return ret;
214}
215
216
217/*
218 algorithm: iterate in order over 30 points on our shape and measure the
219 minimum distance to any point on the other shape. never go backwards on
220 the other shape to also check direction of movement.
221 This algorithm is best applied like a->distance(b) + b->distance(a).
222 fabs(a->distance(b) - b->distance(a)) might turn out to be very interesting,
223 too. in fact, i think it's the most interesting value.
224 */
225float KShapeGesture::distance(const KShapeGesture &other, float abortThreshold) const
226{
227 Q_UNUSED(abortThreshold); //for optimizations, later
228 const QPolygon &o_shape = other.d->m_shape;
229 const QVector<float> &o_lengthTo = other.d->m_lengthTo;
230 float x = 0;
231 float y = 0;
232 float mx = 0;
233 float my = 0;
234 float position = 0;
235 float ox = 0;
236 float oy = 0;
237 float oposition = 0;
238 float omx = 0;
239 float omy = 0;
240 float oxB = 0;
241 float oyB = 0;
242 float opositionB = 0;
243 float omxB = 0;
244 float omyB = 0;
245 float dist = 0;
246 float distB = 0;
247 float desiredPosition = 0;
248 float strokeLength = 0;
249 float retval = 0.0;
250 int pointIndex = 0, opointIndex = 0, opointIndexB = 0;
251
252 //set up starting point on our shape
253 x = d->m_shape[0].x();
254 y = d->m_shape[0].y();
255 strokeLength = d->m_lengthTo[1];
256 mx = (d->m_shape[1].x() - x) / strokeLength;
257 my = (d->m_shape[1].y() - y) / strokeLength;
258 position = 0.0;
259
260 //set up lower bound of search interval on other shape
261 ox = o_shape[0].x();
262 oy = o_shape[0].y();
263 strokeLength = o_lengthTo[1];
264 omx = (o_shape[1].x() - ox) / strokeLength;
265 omy = (o_shape[1].y() - oy) / strokeLength;
266 oposition = 0.0;
267 dist = metric(ox-x, oy-y);
268
269 for (int i = 0; i <= 30; i++) {
270 //go to comparison point on our own polygon
271 //30.0001 to prevent getting out-of-bounds pointIndex
272 desiredPosition = d->m_curveLength / 30.0001 * (float)i;
273 if (desiredPosition > d->m_lengthTo[pointIndex+1]) {
274
275 while (desiredPosition > d->m_lengthTo[pointIndex+1])
276 pointIndex++;
277
278 x = d->m_shape[pointIndex].x();
279 y = d->m_shape[pointIndex].y();
280 position = d->m_lengthTo[pointIndex];
281 strokeLength = d->m_lengthTo[pointIndex+1] - position;
282 mx = (d->m_shape[pointIndex+1].x() - x) / strokeLength;
283 my = (d->m_shape[pointIndex+1].y() - y) / strokeLength;
284 }
285 x += mx * (desiredPosition - position);
286 y += my * (desiredPosition - position);
287 position = desiredPosition;
288
289 //set up upper bound of search interval on other shape
290 desiredPosition = qMin(oposition + other.d->m_curveLength / 15.00005,
291 other.d->m_curveLength - 0.0001);
292 if (i == 0 || desiredPosition > o_lengthTo[opointIndexB+1]) {
293
294 while (desiredPosition > o_lengthTo[opointIndexB+1])
295 opointIndexB++;
296
297 oxB = o_shape[opointIndexB].x();
298 oyB = o_shape[opointIndexB].y();
299 opositionB = o_lengthTo[opointIndexB];
300 strokeLength = o_lengthTo[opointIndexB+1] - opositionB;
301 omxB = (o_shape[opointIndexB+1].x() - oxB) / strokeLength;
302 omyB = (o_shape[opointIndexB+1].y() - oyB) / strokeLength;
303 }
304 oxB += omxB * (desiredPosition - opositionB);
305 oyB += omyB * (desiredPosition - opositionB);
306 opositionB = desiredPosition;
307 distB = metric(oxB-x, oyB-y);
308
309 //binary search for nearest point on other shape
310 for (int j = 0; j < 6; j++) {
311 desiredPosition = (oposition + opositionB) * 0.5;
312 if (dist < distB) {
313 //retract upper bound to desiredPosition
314 //copy state of lower bound to upper bound, advance it from there
315 oxB = ox; oyB = oy;
316 omxB = omx; omyB = omy;
317 opointIndexB = opointIndex; opositionB = oposition;
318
319 if (desiredPosition > o_lengthTo[opointIndexB+1]) {
320
321 while (desiredPosition > o_lengthTo[opointIndexB+1])
322 opointIndexB++;
323
324 oxB = o_shape[opointIndexB].x();
325 oyB = o_shape[opointIndexB].y();
326 opositionB = o_lengthTo[opointIndexB];
327 strokeLength = o_lengthTo[opointIndexB+1] - opositionB;
328 omxB = (o_shape[opointIndexB+1].x() - oxB) / strokeLength;
329 omyB = (o_shape[opointIndexB+1].y() - oyB) / strokeLength;
330 }
331 oxB += omxB * (desiredPosition - opositionB);
332 oyB += omyB * (desiredPosition - opositionB);
333 opositionB = desiredPosition;
334 distB = metric(oxB-x, oyB-y);
335 } else {
336 //advance lower bound to desiredPosition
337 if (desiredPosition > o_lengthTo[opointIndex+1]) {
338
339 while (desiredPosition > o_lengthTo[opointIndex+1])
340 opointIndex++;
341
342 ox = o_shape[opointIndex].x();
343 oy = o_shape[opointIndex].y();
344 oposition = o_lengthTo[opointIndex];
345 strokeLength = o_lengthTo[opointIndex+1] - oposition;
346 omx = (o_shape[opointIndex+1].x() - ox) / strokeLength;
347 omy = (o_shape[opointIndex+1].y() - oy) / strokeLength;
348 }
349 ox += omx * (desiredPosition - oposition);
350 oy += omy * (desiredPosition - oposition);
351 oposition = desiredPosition;
352 dist = metric(ox-x, oy-y);
353 }
354 }
355 retval += qMin(dist, distB);
356 }
357 //scale value to make it roughly invariant against step width
358 return retval / 30.0;
359}
360
361
362KShapeGesture &KShapeGesture::operator=(const KShapeGesture &other)
363{
364 d->m_lengthTo = other.d->m_lengthTo;
365 d->m_shape = other.d->m_shape;
366 d->m_curveLength = other.d->m_curveLength;
367 return *this;
368}
369
370
371bool KShapeGesture::operator==(const KShapeGesture &other) const
372{
373 //a really fast and workable shortcut
374 if (fabs(d->m_curveLength - other.d->m_curveLength) > 0.1)
375 return false;
376 return d->m_shape == other.d->m_shape;
377}
378
379bool KShapeGesture::operator!=(const KShapeGesture &other) const
380{
381 return !operator==(other);
382}
383
384uint KShapeGesture::hashable() const
385{
386 uint hash = 0;
387
388 foreach (const QPoint &point, d->m_shape)
389 hash += qHash(point.x()) + qHash(point.y());
390
391 return hash;
392}
393
394
395/********************************************************
396 * KRockerGesture *
397 *******************************************************/
398
399class KRockerGesturePrivate
400{
401public:
402 KRockerGesturePrivate()
403 : m_hold(Qt::NoButton),
404 m_thenPush(Qt::NoButton)
405 {
406 }
407 KRockerGesturePrivate(const KRockerGesturePrivate &other)
408 : m_hold(other.m_hold),
409 m_thenPush(other.m_thenPush)
410 {
411 }
412 Qt::MouseButton m_hold;
413 Qt::MouseButton m_thenPush;
414};
415
416KRockerGesture::KRockerGesture()
417 : d( new KRockerGesturePrivate )
418{
419}
420
421
422KRockerGesture::KRockerGesture(Qt::MouseButton hold, Qt::MouseButton thenPush)
423 : d( new KRockerGesturePrivate )
424{
425 setButtons(hold, thenPush);
426}
427
428
429KRockerGesture::KRockerGesture(const QString &description)
430 : d( new KRockerGesturePrivate )
431{
432 if (description.length() != 2)
433 return;
434
435 Qt::MouseButton hold, thenPush;
436 Qt::MouseButton *current = &hold;
437 for (int i = 0; i < 2; i++) {
438 switch (description[i].toLatin1()) {
439 case 'L':
440 *current = Qt::LeftButton;
441 break;
442 case 'R':
443 *current = Qt::RightButton;
444 break;
445 case 'M':
446 *current = Qt::MidButton;
447 break;
448 case '1':
449 *current = Qt::XButton1;
450 break;
451 case '2':
452 *current = Qt::XButton2;
453 break;
454 default:
455 return;
456 }
457 current = &thenPush;
458 }
459 d->m_hold = hold;
460 d->m_thenPush = thenPush;
461}
462
463
464KRockerGesture::KRockerGesture(const KRockerGesture &other)
465 : d( new KRockerGesturePrivate(*(other.d)) )
466{
467}
468
469
470KRockerGesture::~KRockerGesture()
471{
472 delete d;
473}
474
475
476void KRockerGesture::setButtons(Qt::MouseButton hold, Qt::MouseButton thenPush)
477{
478 if (hold == thenPush) {
479 d->m_hold = Qt::NoButton;
480 d->m_thenPush = Qt::NoButton;
481 return;
482 }
483
484 int button = hold;
485 for (int i = 0; i < 2; i++) {
486 switch (button) {
487 case Qt::LeftButton:
488 case Qt::RightButton:
489 case Qt::MidButton:
490 case Qt::XButton1:
491 case Qt::XButton2:
492 break;
493 default:
494 d->m_hold = Qt::NoButton;
495 d->m_thenPush = Qt::NoButton;
496 return;
497 }
498 button = thenPush;
499 }
500
501 d->m_hold = hold;
502 d->m_thenPush = thenPush;
503}
504
505
506void KRockerGesture::getButtons(Qt::MouseButton *hold, Qt::MouseButton *thenPush) const
507{
508 *hold = d->m_hold;
509 *thenPush = d->m_thenPush;
510}
511
512
513QString KRockerGesture::mouseButtonName(Qt::MouseButton button)
514{
515 switch (button) {
516 case Qt::LeftButton:
517 return i18nc("left mouse button", "left button");
518 break;
519 case Qt::MidButton:
520 return i18nc("middle mouse button", "middle button");
521 break;
522 case Qt::RightButton:
523 return i18nc("right mouse button", "right button");
524 break;
525 default:
526 return i18nc("a nonexistent value of mouse button", "invalid button");
527 break;
528 }
529}
530
531
532QString KRockerGesture::rockerName() const
533{
534 if (!isValid())
535 return QString();
536 //return i18nc("an invalid mouse gesture of type \"hold down one button, then press another button\"",
537 // "invalid rocker gesture");
538 else
539 return i18nc("a kind of mouse gesture: hold down one mouse button, then press another button",
540 "Hold %1, then push %2", mouseButtonName(d->m_hold), mouseButtonName(d->m_thenPush));
541}
542
543
544bool KRockerGesture::isValid() const
545{
546 return (d->m_hold != Qt::NoButton);
547}
548
549
550QString KRockerGesture::toString() const
551{
552 if (!isValid())
553 return QString();
554 QString ret;
555 int button = d->m_hold;
556 char desc;
557 for (int i = 0; i < 2; i++) {
558 switch (button) {
559 case Qt::LeftButton:
560 desc = 'L';
561 break;
562 case Qt::RightButton:
563 desc = 'R';
564 break;
565 case Qt::MidButton:
566 desc = 'M';
567 break;
568 case Qt::XButton1:
569 desc = '1';
570 break;
571 case Qt::XButton2:
572 desc = '2';
573 break;
574 default:
575 return QString();
576 }
577 ret.append(desc);
578 button = d->m_thenPush;
579 }
580 return ret;
581}
582
583
584KRockerGesture &KRockerGesture::operator=(const KRockerGesture &other)
585{
586 d->m_hold = other.d->m_hold;
587 d->m_thenPush = other.d->m_thenPush;
588 return *this;
589}
590
591
592bool KRockerGesture::operator==(const KRockerGesture &other) const
593{
594 return d->m_hold == other.d->m_hold && d->m_thenPush == other.d->m_thenPush;
595}
596
597bool KRockerGesture::operator!=(const KRockerGesture &other) const
598{
599 return !operator==(other);
600}
601
602uint KRockerGesture::hashable() const
603{
604 //make it asymmetric
605 return qHash(d->m_hold) + d->m_thenPush;
606}
KRockerGesture
Definition: kgesture.h:153
KRockerGesture::operator==
bool operator==(const KRockerGesture &other) const
Return whether this gesture is equal to the other gesture.
Definition: kgesture.cpp:592
KRockerGesture::operator=
KRockerGesture & operator=(const KRockerGesture &other)
Set this gesture to the other gesture.
Definition: kgesture.cpp:584
KRockerGesture::KRockerGesture
KRockerGesture()
Create a new invalid rocker gesture.
Definition: kgesture.cpp:416
KRockerGesture::~KRockerGesture
~KRockerGesture()
Destructor.
Definition: kgesture.cpp:470
KRockerGesture::hashable
uint hashable() const
Return an opaque value for use in hash tables.
Definition: kgesture.cpp:602
KRockerGesture::mouseButtonName
static QString mouseButtonName(Qt::MouseButton button)
Return a user-friendly name for the mouse button button.
Definition: kgesture.cpp:513
KRockerGesture::getButtons
void getButtons(Qt::MouseButton *hold, Qt::MouseButton *thenPush) const
Write the button combination to hold and thenPush.
Definition: kgesture.cpp:506
KRockerGesture::rockerName
QString rockerName() const
Return a user-friendly name of the button combination.
Definition: kgesture.cpp:532
KRockerGesture::operator!=
bool operator!=(const KRockerGesture &other) const
Return the opposite of operator==()
Definition: kgesture.cpp:597
KRockerGesture::isValid
bool isValid() const
Return true if this gesture is valid.
Definition: kgesture.cpp:544
KRockerGesture::setButtons
void setButtons(Qt::MouseButton hold, Qt::MouseButton thenPush)
set button combination to trigger
Definition: kgesture.cpp:476
KRockerGesture::toString
QString toString() const
Return a string representation of this gesture.
Definition: kgesture.cpp:550
KShapeGesture
Definition: kgesture.h:38
KShapeGesture::toString
QString toString() const
Return a string representation of this gesture.
Definition: kgesture.cpp:164
KShapeGesture::setShape
void setShape(const QPolygon &shape)
Set the shape to draw to trigger this gesture.
Definition: kgesture.cpp:105
KShapeGesture::operator=
KShapeGesture & operator=(const KShapeGesture &other)
Set this gesture to the other gesture.
Definition: kgesture.cpp:362
KShapeGesture::distance
float distance(const KShapeGesture &other, float abortThreshold) const
Return a difference measurement betwenn this gesture and the other gesture.
Definition: kgesture.cpp:225
KShapeGesture::operator!=
bool operator!=(const KShapeGesture &other) const
Return the opposite of operator==()
Definition: kgesture.cpp:379
KShapeGesture::setShapeName
void setShapeName(const QString &friendlyName)
set a user-visible name for this gesture's shape, like "triangle" or "line".
Definition: kgesture.cpp:146
KShapeGesture::~KShapeGesture
~KShapeGesture()
Destructor.
Definition: kgesture.cpp:99
KShapeGesture::shapeName
QString shapeName() const
Return the user-visible name for this gesture's shape, like "triangle" or "line".
Definition: kgesture.cpp:152
KShapeGesture::toSvg
QByteArray toSvg(const QString &attributes=QString()) const
Return an idealized SVG image of this gesture.
Definition: kgesture.cpp:184
KShapeGesture::hashable
uint hashable() const
Return an opaque value for use in hash tables.
Definition: kgesture.cpp:384
KShapeGesture::operator==
bool operator==(const KShapeGesture &other) const
Return whether this gesture is equal to the other gesture.
Definition: kgesture.cpp:371
KShapeGesture::KShapeGesture
KShapeGesture()
Create a new invalid shape gesture.
Definition: kgesture.cpp:50
KShapeGesture::isValid
bool isValid() const
Return true if this gesture is valid.
Definition: kgesture.cpp:158
kdebug.h
metric
float metric(float dx, float dy)
Definition: kgesture.cpp:26
kgesture.h
qHash
uint qHash(int)
klocalizedstring.h
i18nc
QString i18nc(const char *ctxt, const char *text)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Feb 20 2023 00:00:00 by doxygen 1.9.6 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KDEUI

Skip menu "KDEUI"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Modules
  • Related Pages

kdelibs-4.14.38 API Reference

Skip menu "kdelibs-4.14.38 API Reference"
  • DNSSD
  • Interfaces
  •   KHexEdit
  •   KMediaPlayer
  •   KSpeech
  •   KTextEditor
  • kconf_update
  • KDE3Support
  •   KUnitTest
  • KDECore
  • KDED
  • KDEsu
  • KDEUI
  • KDEWebKit
  • KDocTools
  • KFile
  • KHTML
  • KImgIO
  • KInit
  • kio
  • KIOSlave
  • KJS
  •   KJS-API
  •   WTF
  • kjsembed
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUnitConversion
  • KUtils
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver
Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal