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

KHTML

  • sources
  • kde-4.12
  • kdelibs
  • khtml
  • svg
SVGAnimationElement.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2004, 2005 Nikolas Zimmermann <wildfox@kde.org>
3  2004, 2005, 2006, 2007 Rob Buis <buis@kde.org>
4  Copyright (C) 2007 Eric Seidel <eric@webkit.org>
5  Copyright (C) 2008 Apple Inc. All rights reserved.
6 
7  This file is part of the KDE project
8 
9  This library is free software; you can redistribute it and/or
10  modify it under the terms of the GNU Library General Public
11  License as published by the Free Software Foundation; either
12  version 2 of the License, or (at your option) any later version.
13 
14  This library is distributed in the hope that it will be useful,
15  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  Library General Public License for more details.
18 
19  You should have received a copy of the GNU Library General Public License
20  along with this library; see the file COPYING.LIB. If not, write to
21  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  Boston, MA 02110-1301, USA.
23 */
24 
25 #include "config.h"
26 #if ENABLE(SVG_ANIMATION)
27 #include "SVGAnimationElement.h"
28 
29 #include "CSSComputedStyleDeclaration.h"
30 #include "CSSParser.h"
31 #include "CSSPropertyNames.h"
32 #include "Document.h"
33 #include "Event.h"
34 #include "EventListener.h"
35 #include "FloatConversion.h"
36 #include "HTMLNames.h"
37 #include "SVGElementInstance.h"
38 #include "SVGNames.h"
39 #include "SVGURIReference.h"
40 #include "SVGUseElement.h"
41 #include "XLinkNames.h"
42 #include <math.h>
43 
44 using namespace std;
45 
46 namespace WebCore {
47 
48 SVGAnimationElement::SVGAnimationElement(const QualifiedName& tagName, Document* doc)
49  : SVGSMILElement(tagName, doc)
50  , SVGTests()
51  , SVGExternalResourcesRequired()
52  , m_animationValid(false)
53 {
54 }
55 
56 SVGAnimationElement::~SVGAnimationElement()
57 {
58 }
59 
60 static void parseKeyTimes(const String& parse, Vector<float>& result, bool verifyOrder)
61 {
62  result.clear();
63  Vector<String> parseList;
64  parse.split(';', parseList);
65  for (unsigned n = 0; n < parseList.size(); ++n) {
66  String timeString = parseList[n];
67  bool ok;
68  float time = timeString.toFloat(&ok);
69  if (!ok || time < 0 || time > 1.f)
70  goto fail;
71  if (verifyOrder) {
72  if (!n) {
73  if (time != 0)
74  goto fail;
75  } else if (time < result.last())
76  goto fail;
77  }
78  result.append(time);
79  }
80  return;
81 fail:
82  result.clear();
83 }
84 
85 static void parseKeySplines(const String& parse, Vector<UnitBezier>& result)
86 {
87  result.clear();
88  Vector<String> parseList;
89  parse.split(';', parseList);
90  for (unsigned n = 0; n < parseList.size(); ++n) {
91  Vector<String> parseSpline;
92  parseList[n].split(',', parseSpline);
93  // The spec says the sepator is a space, all tests use commas. Weird.
94  if (parseSpline.size() == 1)
95  parseList[n].split(' ', parseSpline);
96  if (parseSpline.size() != 4)
97  goto fail;
98  double curveValues[4];
99  for (unsigned i = 0; i < 4; ++i) {
100  String parseNumber = parseSpline[i];
101  bool ok;
102  curveValues[i] = parseNumber.toDouble(&ok);
103  if (!ok || curveValues[i] < 0.0 || curveValues[i] > 1.0)
104  goto fail;
105  }
106  result.append(UnitBezier(curveValues[0], curveValues[1], curveValues[2], curveValues[3]));
107  }
108  return;
109 fail:
110  result.clear();
111 }
112 
113 void SVGAnimationElement::parseMappedAttribute(MappedAttribute* attr)
114 {
115  if (attr->name() == SVGNames::valuesAttr)
116  attr->value().string().split(';', m_values);
117  else if (attr->name() == SVGNames::keyTimesAttr)
118  parseKeyTimes(attr->value(), m_keyTimes, true);
119  else if (attr->name() == SVGNames::keyPointsAttr && hasTagName(SVGNames::animateMotionTag)) {
120  // This is specified to be an animateMotion attribute only but it is simpler to put it here
121  // where the other timing calculatations are.
122  parseKeyTimes(attr->value(), m_keyPoints, false);
123  } else if (attr->name() == SVGNames::keySplinesAttr)
124  parseKeySplines(attr->value(), m_keySplines);
125  else {
126  if (SVGTests::parseMappedAttribute(attr))
127  return;
128  if (SVGExternalResourcesRequired::parseMappedAttribute(attr))
129  return;
130  SVGSMILElement::parseMappedAttribute(attr);
131  }
132 }
133 
134 void SVGAnimationElement::attributeChanged(Attribute* attr, bool preserveDecls)
135 {
136  // Assumptions may not hold after an attribute change.
137  m_animationValid = false;
138  SVGSMILElement::attributeChanged(attr, preserveDecls);
139 }
140 
141 float SVGAnimationElement::getStartTime() const
142 {
143  return narrowPrecisionToFloat(intervalBegin().value());
144 }
145 
146 float SVGAnimationElement::getCurrentTime() const
147 {
148  return narrowPrecisionToFloat(elapsed().value());
149 }
150 
151 float SVGAnimationElement::getSimpleDuration(ExceptionCode&) const
152 {
153  return narrowPrecisionToFloat(simpleDuration().value());
154 }
155 
156 bool SVGAnimationElement::beginElement(ExceptionCode& ec)
157 {
158  return beginElementAt(0, ec);
159 }
160 
161 bool SVGAnimationElement::beginElementAt(float offset, ExceptionCode& ec)
162 {
163  addBeginTime(elapsed() + offset);
164  return true;
165 }
166 
167 bool SVGAnimationElement::endElement(ExceptionCode& ec)
168 {
169  return endElementAt(0, ec);
170 }
171 
172 bool SVGAnimationElement::endElementAt(float offset, ExceptionCode& ec)
173 {
174  if (offset < 0)
175  return false;
176 
177  addEndTime(elapsed() + offset);
178  return true;
179 }
180 
181 SVGAnimationElement::AnimationMode SVGAnimationElement::animationMode() const
182 {
183  // http://www.w3.org/TR/2001/REC-smil-animation-20010904/#AnimFuncValues
184  if (hasTagName(SVGNames::setTag))
185  return ToAnimation;
186  if (!animationPath().isEmpty())
187  return PathAnimation;
188  if (hasAttribute(SVGNames::valuesAttr))
189  return ValuesAnimation;
190  if (!toValue().isEmpty())
191  return fromValue().isEmpty() ? ToAnimation : FromToAnimation;
192  if (!byValue().isEmpty())
193  return fromValue().isEmpty() ? ByAnimation : FromByAnimation;
194  return NoAnimation;
195 }
196 
197 SVGAnimationElement::CalcMode SVGAnimationElement::calcMode() const
198 {
199  static const AtomicString discrete("discrete");
200  static const AtomicString linear("linear");
201  static const AtomicString paced("paced");
202  static const AtomicString spline("spline");
203  const AtomicString& value = getAttribute(SVGNames::calcModeAttr);
204  if (value == discrete)
205  return CalcModeDiscrete;
206  if (value == linear)
207  return CalcModeLinear;
208  if (value == paced)
209  return CalcModePaced;
210  if (value == spline)
211  return CalcModeSpline;
212  return hasTagName(SVGNames::animateMotionTag) ? CalcModePaced : CalcModeLinear;
213 }
214 
215 SVGAnimationElement::AttributeType SVGAnimationElement::attributeType() const
216 {
217  static const AtomicString css("CSS");
218  static const AtomicString xml("XML");
219  const AtomicString& value = getAttribute(SVGNames::attributeTypeAttr);
220  if (value == css)
221  return AttributeTypeCSS;
222  if (value == xml)
223  return AttributeTypeXML;
224  return AttributeTypeAuto;
225 }
226 
227 String SVGAnimationElement::toValue() const
228 {
229  return getAttribute(SVGNames::toAttr);
230 }
231 
232 String SVGAnimationElement::byValue() const
233 {
234  return getAttribute(SVGNames::byAttr);
235 }
236 
237 String SVGAnimationElement::fromValue() const
238 {
239  return getAttribute(SVGNames::fromAttr);
240 }
241 
242 bool SVGAnimationElement::isAdditive() const
243 {
244  static const AtomicString sum("sum");
245  const AtomicString& value = getAttribute(SVGNames::additiveAttr);
246  return value == sum || animationMode() == ByAnimation;
247 }
248 
249 bool SVGAnimationElement::isAccumulated() const
250 {
251  static const AtomicString sum("sum");
252  const AtomicString& value = getAttribute(SVGNames::accumulateAttr);
253  return value == sum && animationMode() != ToAnimation;
254 }
255 
256 bool SVGAnimationElement::hasValidTarget() const
257 {
258  return targetElement();
259 }
260 
261 bool SVGAnimationElement::attributeIsCSS(const String& attributeName)
262 {
263  // FIXME: We should have a map of all SVG properties and their attribute types so we
264  // could validate animations better. The spec is very vague about this.
265  unsigned id = cssPropertyID(attributeName);
266  // SVG range
267  if (id >= CSSPropertyClipPath && id <= CSSPropertyWritingMode)
268  return true;
269  // Regular CSS properties also in SVG
270  return id == CSSPropertyColor || id == CSSPropertyDisplay || id == CSSPropertyOpacity
271  || (id >= CSSPropertyFont && id <= CSSPropertyFontWeight)
272  || id == CSSPropertyOverflow || id == CSSPropertyVisibility;
273 }
274 
275 bool SVGAnimationElement::targetAttributeIsCSS() const
276 {
277  AttributeType type = attributeType();
278  if (type == AttributeTypeCSS)
279  return true;
280  if (type == AttributeTypeXML)
281  return false;
282  return attributeIsCSS(attributeName());
283 }
284 
285 void SVGAnimationElement::setTargetAttributeAnimatedValue(const String& value)
286 {
287  if (!hasValidTarget())
288  return;
289  SVGElement* target = targetElement();
290  String attributeName = this->attributeName();
291  if (!target || attributeName.isEmpty() || value.isNull())
292  return;
293 
294  // We don't want the instance tree to get rebuild. Instances are updated in the loop below.
295  if (target->isStyled())
296  static_cast<SVGStyledElement*>(target)->setInstanceUpdatesBlocked(true);
297 
298  ExceptionCode ec;
299  bool isCSS = targetAttributeIsCSS();
300  if (isCSS) {
301  // FIXME: This should set the override style, not the inline style.
302  // Sadly override styles are not yet implemented.
303  target->style()->setProperty(attributeName, value, "", ec);
304  } else {
305  // FIXME: This should set the 'presentation' value, not the actual
306  // attribute value. Whatever that means in practice.
307  target->setAttribute(attributeName, value, ec);
308  }
309 
310  if (target->isStyled())
311  static_cast<SVGStyledElement*>(target)->setInstanceUpdatesBlocked(false);
312 
313  // If the target element is used in an <use> instance tree, update that as well.
314  HashSet<SVGElementInstance*>* instances = document()->accessSVGExtensions()->instancesForElement(target);
315  if (!instances)
316  return;
317  HashSet<SVGElementInstance*>::iterator end = instances->end();
318  for (HashSet<SVGElementInstance*>::iterator it = instances->begin(); it != end; ++it) {
319  SVGElement* shadowTreeElement = (*it)->shadowTreeElement();
320  ASSERT(shadowTreeElement);
321  if (isCSS)
322  shadowTreeElement->style()->setProperty(attributeName, value, "", ec);
323  else
324  shadowTreeElement->setAttribute(attributeName, value, ec);
325  (*it)->correspondingUseElement()->setChanged();
326  }
327 }
328 
329 void SVGAnimationElement::calculateKeyTimesForCalcModePaced()
330 {
331  ASSERT(calcMode() == CalcModePaced);
332  ASSERT(animationMode() == ValuesAnimation);
333 
334  unsigned valuesCount = m_values.size();
335  ASSERT(valuesCount > 1);
336  Vector<float> keyTimesForPaced;
337  float totalDistance = 0;
338  keyTimesForPaced.append(0);
339  for (unsigned n = 0; n < valuesCount - 1; ++n) {
340  // Distance in any units
341  float distance = calculateDistance(m_values[n], m_values[n + 1]);
342  if (distance < 0)
343  return;
344  totalDistance += distance;
345  keyTimesForPaced.append(distance);
346  }
347  if (!totalDistance)
348  return;
349 
350  // Normalize.
351  for (unsigned n = 1; n < keyTimesForPaced.size() - 1; ++n)
352  keyTimesForPaced[n] = keyTimesForPaced[n - 1] + keyTimesForPaced[n] / totalDistance;
353  keyTimesForPaced[keyTimesForPaced.size() - 1] = 1.f;
354 
355  // Use key times calculated based on pacing instead of the user provided ones.
356  m_keyTimes.swap(keyTimesForPaced);
357 }
358 
359 static inline double solveEpsilon(double duration) { return 1. / (200. * duration); }
360 
361 float SVGAnimationElement::calculatePercentForSpline(float percent, unsigned splineIndex) const
362 {
363  ASSERT(calcMode() == CalcModeSpline);
364  ASSERT(splineIndex < m_keySplines.size());
365  UnitBezier bezier = m_keySplines[splineIndex];
366  SMILTime duration = simpleDuration();
367  if (!duration.isFinite())
368  duration = 100.0;
369  return narrowPrecisionToFloat(bezier.solve(percent, solveEpsilon(duration.value())));
370 }
371 
372 float SVGAnimationElement::calculatePercentFromKeyPoints(float percent) const
373 {
374  ASSERT(!m_keyPoints.isEmpty());
375  ASSERT(calcMode() != CalcModePaced);
376  unsigned keyTimesCount = m_keyTimes.size();
377  ASSERT(keyTimesCount > 1);
378  ASSERT(m_keyPoints.size() == keyTimesCount);
379 
380  unsigned index;
381  for (index = 1; index < keyTimesCount; ++index) {
382  if (m_keyTimes[index] >= percent)
383  break;
384  }
385  --index;
386 
387  float fromPercent = m_keyTimes[index];
388  float toPercent = m_keyTimes[index + 1];
389  float fromKeyPoint = m_keyPoints[index];
390  float toKeyPoint = m_keyPoints[index + 1];
391 
392  if (calcMode() == CalcModeDiscrete)
393  return percent == 1.0f ? toKeyPoint : fromKeyPoint;
394 
395  float keyPointPercent = percent == 1.0f ? 1.0f : (percent - fromPercent) / (toPercent - fromPercent);
396 
397  if (calcMode() == CalcModeSpline) {
398  ASSERT(m_keySplines.size() == m_keyPoints.size() - 1);
399  keyPointPercent = calculatePercentForSpline(keyPointPercent, index);
400  }
401  return (toKeyPoint - fromKeyPoint) * keyPointPercent + fromKeyPoint;
402 }
403 
404 void SVGAnimationElement::currentValuesFromKeyPoints(float percent, float& effectivePercent, String& from, String& to) const
405 {
406  ASSERT(!m_keyPoints.isEmpty());
407  ASSERT(m_keyPoints.size() == m_keyTimes.size());
408  ASSERT(calcMode() != CalcModePaced);
409  effectivePercent = calculatePercentFromKeyPoints(percent);
410  unsigned index = effectivePercent == 1.0f ? m_values.size() - 2 : static_cast<unsigned>(effectivePercent * (m_values.size() - 1));
411  from = m_values[index];
412  to = m_values[index + 1];
413 }
414 
415 void SVGAnimationElement::currentValuesForValuesAnimation(float percent, float& effectivePercent, String& from, String& to) const
416 {
417  unsigned valuesCount = m_values.size();
418  ASSERT(m_animationValid);
419  ASSERT(valuesCount > 1);
420 
421  CalcMode calcMode = this->calcMode();
422  if (!m_keyPoints.isEmpty() && calcMode != CalcModePaced)
423  return currentValuesFromKeyPoints(percent, effectivePercent, from, to);
424 
425  unsigned keyTimesCount = m_keyTimes.size();
426  ASSERT(!keyTimesCount || valuesCount == keyTimesCount);
427  ASSERT(!keyTimesCount || (keyTimesCount > 1 && m_keyTimes[0] == 0));
428 
429  unsigned index;
430  for (index = 1; index < keyTimesCount; ++index) {
431  if (m_keyTimes[index] >= percent)
432  break;
433  }
434  --index;
435 
436  if (calcMode == CalcModeDiscrete) {
437  if (!keyTimesCount)
438  index = percent == 1.0f ? valuesCount - 1 : static_cast<unsigned>(percent * valuesCount);
439  from = m_values[index];
440  to = m_values[index];
441  effectivePercent = 0.0f;
442  return;
443  }
444 
445  float fromPercent;
446  float toPercent;
447  if (keyTimesCount) {
448  fromPercent = m_keyTimes[index];
449  toPercent = m_keyTimes[index + 1];
450  } else {
451  index = static_cast<unsigned>(percent * (valuesCount - 1));
452  fromPercent = static_cast<float>(index) / (valuesCount - 1);
453  toPercent = static_cast<float>(index + 1) / (valuesCount - 1);
454  }
455 
456  if (index == valuesCount - 1)
457  --index;
458  from = m_values[index];
459  to = m_values[index + 1];
460  ASSERT(toPercent > fromPercent);
461  effectivePercent = percent == 1.0f ? 1.0f : (percent - fromPercent) / (toPercent - fromPercent);
462 
463  if (calcMode == CalcModeSpline) {
464  ASSERT(m_keySplines.size() == m_values.size() - 1);
465  effectivePercent = calculatePercentForSpline(effectivePercent, index);
466  }
467 }
468 
469 void SVGAnimationElement::startedActiveInterval()
470 {
471  m_animationValid = false;
472 
473  if (!hasValidTarget())
474  return;
475 
476  AnimationMode animationMode = this->animationMode();
477  if (animationMode == NoAnimation)
478  return;
479  if (animationMode == FromToAnimation)
480  m_animationValid = calculateFromAndToValues(fromValue(), toValue());
481  else if (animationMode == ToAnimation) {
482  // For to-animations the from value is the current accumulated value from lower priority animations.
483  // The value is not static and is determined during the animation.
484  m_animationValid = calculateFromAndToValues(String(), toValue());
485  } else if (animationMode == FromByAnimation)
486  m_animationValid = calculateFromAndByValues(fromValue(), byValue());
487  else if (animationMode == ByAnimation)
488  m_animationValid = calculateFromAndByValues(String(), byValue());
489  else if (animationMode == ValuesAnimation) {
490  CalcMode calcMode = this->calcMode();
491  m_animationValid = m_values.size() > 1
492  && (calcMode == CalcModePaced || !hasAttribute(SVGNames::keyTimesAttr) || hasAttribute(SVGNames::keyPointsAttr) || (m_values.size() == m_keyTimes.size()))
493  && (calcMode == CalcModeDiscrete || !m_keyTimes.size() || m_keyTimes.last() == 1.0)
494  && (calcMode != CalcModeSpline || (m_keySplines.size() && (m_keySplines.size() == m_values.size() - 1) || m_keySplines.size() == m_keyPoints.size() - 1))
495  && (!hasAttribute(SVGNames::keyPointsAttr) || (m_keyTimes.size() > 1 && m_keyTimes.size() == m_keyPoints.size()));
496  if (calcMode == CalcModePaced && m_animationValid)
497  calculateKeyTimesForCalcModePaced();
498  } else if (animationMode == PathAnimation)
499  m_animationValid = calcMode() == CalcModePaced || !hasAttribute(SVGNames::keyPointsAttr) || (m_keyTimes.size() > 1 && m_keyTimes.size() == m_keyPoints.size());
500 }
501 
502 void SVGAnimationElement::updateAnimation(float percent, unsigned repeat, SVGSMILElement* resultElement)
503 {
504  if (!m_animationValid)
505  return;
506 
507  float effectivePercent;
508  if (animationMode() == ValuesAnimation) {
509  String from;
510  String to;
511  currentValuesForValuesAnimation(percent, effectivePercent, from, to);
512  if (from != m_lastValuesAnimationFrom || to != m_lastValuesAnimationTo ) {
513  m_animationValid = calculateFromAndToValues(from, to);
514  if (!m_animationValid)
515  return;
516  m_lastValuesAnimationFrom = from;
517  m_lastValuesAnimationTo = to;
518  }
519  } else if (!m_keyPoints.isEmpty() && calcMode() != CalcModePaced)
520  effectivePercent = calculatePercentFromKeyPoints(percent);
521  else
522  effectivePercent = percent;
523 
524  calculateAnimatedValue(effectivePercent, repeat, resultElement);
525 }
526 
527 void SVGAnimationElement::endedActiveInterval()
528 {
529 }
530 
531 }
532 
533 // vim:ts=4:noet
534 #endif // ENABLE(SVG_ANIMATION)
535 
WebCore::SVGNames::attributeTypeAttr
DOM::QualifiedName attributeTypeAttr
Definition: SVGNames.cpp:112
WebCore::SVGNames::keyTimesAttr
DOM::QualifiedName keyTimesAttr
Definition: SVGNames.cpp:205
WebCore::SVGNames::keyPointsAttr
DOM::QualifiedName keyPointsAttr
Definition: SVGNames.cpp:203
WebCore::SVGNames::valuesAttr
DOM::QualifiedName valuesAttr
Definition: SVGNames.cpp:326
WebCore::SVGNames::fromAttr
DOM::QualifiedName fromAttr
Definition: SVGNames.cpp:174
WebCore::narrowPrecisionToFloat
float narrowPrecisionToFloat(T)
WebCore::SVGNames::calcModeAttr
DOM::QualifiedName calcModeAttr
Definition: SVGNames.cpp:121
FloatConversion.h
SVGUseElement.h
WebCore::SVGNames::setTag
DOM::QualifiedName setTag
Definition: SVGNames.cpp:29
SVGNames.h
ok
KGuiItem ok()
WebCore::SVGNames::toAttr
DOM::QualifiedName toAttr
Definition: SVGNames.cpp:311
SVGURIReference.h
WebCore::SVGNames::byAttr
DOM::QualifiedName byAttr
Definition: SVGNames.cpp:120
WebCore::SVGNames::additiveAttr
DOM::QualifiedName additiveAttr
Definition: SVGNames.cpp:104
SVGAnimationElement.h
XLinkNames.h
WebCore::SVGNames::accumulateAttr
DOM::QualifiedName accumulateAttr
Definition: SVGNames.cpp:103
WebCore::SVGNames::keySplinesAttr
DOM::QualifiedName keySplinesAttr
Definition: SVGNames.cpp:204
SVGElementInstance.h
WebCore::String
DOM::DOMString String
Definition: PlatformString.h:8
end
const KShortcut & end()
WebCore::SVGNames::animateMotionTag
DOM::QualifiedName animateMotionTag
Definition: SVGNames.cpp:27
This file is part of the KDE documentation.
Documentation copyright © 1996-2014 The KDE developers.
Generated on Tue Oct 14 2014 22:51:22 by doxygen 1.8.7 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KHTML

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

kdelibs API Reference

Skip menu "kdelibs 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
  • kjsembed
  •   WTF
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUnitConversion
  • KUtils
  • Nepomuk
  • Nepomuk-Core
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver

Search



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