17 #define ACTIVETOUCHESINFO_DEBUG 0
18 #define DIRECTIONALDRAGAREA_DEBUG 0
20 #include "DirectionalDragArea.h"
22 #include <QQuickWindow>
23 #include <QtCore/qmath.h>
26 #pragma GCC diagnostic push
27 #pragma GCC diagnostic ignored "-pedantic"
28 #include <private/qquickwindow_p.h>
29 #pragma GCC diagnostic pop
32 #include "TouchOwnershipEvent.h"
33 #include "TouchRegistry.h"
34 #include "UnownedTouchEvent.h"
38 #if DIRECTIONALDRAGAREA_DEBUG
39 #define ddaDebug(params) qDebug().nospace() << "[DDA(" << qPrintable(objectName()) << ")] " << params
40 #include "DebugHelpers.h"
43 const char *statusToString(DirectionalDragArea::Status status)
45 if (status == DirectionalDragArea::WaitingForTouch) {
46 return "WaitingForTouch";
47 }
else if (status == DirectionalDragArea::Undecided) {
55 #else // DIRECTIONALDRAGAREA_DEBUG
56 #define ddaDebug(params) ((void)0)
57 #endif // DIRECTIONALDRAGAREA_DEBUG
60 DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
62 , m_status(WaitingForTouch)
65 , m_direction(Direction::Rightwards)
68 , m_distanceThreshold(0)
69 , m_distanceThresholdSquared(0.)
71 , m_maxSilenceTime(200)
73 , m_compositionTime(60)
74 , m_numSamplesOnLastSpeedCheck(0)
75 , m_recognitionTimer(0)
76 , m_velocityCalculator(0)
77 , m_timeSource(new RealTimeSource)
78 , m_activeTouches(m_timeSource)
80 setRecognitionTimer(
new Timer(
this));
81 m_recognitionTimer->setInterval(60);
82 m_recognitionTimer->setSingleShot(
false);
84 m_velocityCalculator =
new AxisVelocityCalculator(
this);
86 connect(
this, &QQuickItem::enabledChanged,
this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
87 connect(
this, &QQuickItem::visibleChanged,
this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
90 Direction::Type DirectionalDragArea::direction()
const
95 void DirectionalDragArea::setDirection(Direction::Type direction)
97 if (direction != m_direction) {
98 m_direction = direction;
99 Q_EMIT directionChanged(m_direction);
103 void DirectionalDragArea::setMaxDeviation(qreal value)
105 if (m_dampedScenePos.maxDelta() != value) {
106 m_dampedScenePos.setMaxDelta(value);
107 Q_EMIT maxDeviationChanged(value);
111 qreal DirectionalDragArea::wideningAngle()
const
113 return m_wideningAngle;
116 void DirectionalDragArea::setWideningAngle(qreal angle)
118 if (angle == m_wideningAngle)
121 m_wideningAngle = angle;
125 qreal angleRadians = angle * M_PI / 180.0;
126 m_wideningFactor = qCos(angleRadians);
127 m_wideningFactor = m_wideningFactor * m_wideningFactor;
130 Q_EMIT wideningAngleChanged(angle);
133 void DirectionalDragArea::setDistanceThreshold(qreal value)
135 if (m_distanceThreshold != value) {
136 m_distanceThreshold = value;
137 m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold;
138 Q_EMIT distanceThresholdChanged(value);
142 void DirectionalDragArea::setMinSpeed(qreal value)
144 if (m_minSpeed != value) {
146 Q_EMIT minSpeedChanged(value);
150 void DirectionalDragArea::setMaxSilenceTime(
int value)
152 if (m_maxSilenceTime != value) {
153 m_maxSilenceTime = value;
154 Q_EMIT maxSilenceTimeChanged(value);
158 void DirectionalDragArea::setCompositionTime(
int value)
160 if (m_compositionTime != value) {
161 m_compositionTime = value;
162 Q_EMIT compositionTimeChanged(value);
166 void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
169 bool timerWasRunning =
false;
170 bool wasSingleShot =
false;
173 if (m_recognitionTimer) {
174 interval = m_recognitionTimer->interval();
175 timerWasRunning = m_recognitionTimer->isRunning();
176 if (m_recognitionTimer->parent() ==
this) {
177 delete m_recognitionTimer;
181 m_recognitionTimer = timer;
182 timer->setInterval(interval);
183 timer->setSingleShot(wasSingleShot);
184 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
185 this, &DirectionalDragArea::checkSpeed);
186 if (timerWasRunning) {
187 m_recognitionTimer->start();
191 void DirectionalDragArea::setTimeSource(
const SharedTimeSource &timeSource)
193 m_timeSource = timeSource;
194 m_velocityCalculator->setTimeSource(timeSource);
195 m_activeTouches.m_timeSource = timeSource;
198 qreal DirectionalDragArea::distance()
const
200 if (Direction::isHorizontal(m_direction)) {
201 return m_previousPos.x() - m_startPos.x();
203 return m_previousPos.y() - m_startPos.y();
207 void DirectionalDragArea::updateSceneDistance()
209 QPointF totalMovement = m_previousScenePos - m_startScenePos;
210 m_sceneDistance = projectOntoDirectionVector(totalMovement);
213 qreal DirectionalDragArea::sceneDistance()
const
215 return m_sceneDistance;
218 qreal DirectionalDragArea::touchX()
const
220 return m_previousPos.x();
223 qreal DirectionalDragArea::touchY()
const
225 return m_previousPos.y();
228 qreal DirectionalDragArea::touchSceneX()
const
230 return m_previousScenePos.x();
233 qreal DirectionalDragArea::touchSceneY()
const
235 return m_previousScenePos.y();
238 bool DirectionalDragArea::event(QEvent *event)
240 if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
241 touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
243 }
else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
244 unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
247 return QQuickItem::event(event);
251 void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
253 if (event->gained()) {
255 ids.append(event->touchId());
256 ddaDebug(
"grabbing touch");
257 grabTouchPoints(ids);
266 QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
267 if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
268 ddaDebug(
"removing mouse grabber");
269 window()->mouseGrabberItem()->ungrabMouse();
274 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
276 setStatus(WaitingForTouch);
280 void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
282 QTouchEvent *
event = unownedTouchEvent->touchEvent();
284 Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
286 ddaDebug(
"Unowned " << m_timeSource->msecsSinceReference() <<
" " << qPrintable(touchEventToString(event)));
289 case WaitingForTouch:
293 Q_ASSERT(isEnabled() && isVisible());
294 unownedTouchEvent_undecided(unownedTouchEvent);
301 m_activeTouches.update(event);
304 void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
306 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
308 qCritical() <<
"DirectionalDragArea[status=Undecided]: touch " << m_touchId
309 <<
"missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
310 "Considering it as released.";
312 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
313 setStatus(WaitingForTouch);
317 const QPointF &touchScenePos = touchPoint->scenePos();
319 if (touchPoint->state() == Qt::TouchPointReleased) {
321 ddaDebug(
"Touch has ended before recognition concluded");
322 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
323 emitSignalIfTapped();
324 setStatus(WaitingForTouch);
328 m_previousDampedScenePos.setX(m_dampedScenePos.x());
329 m_previousDampedScenePos.setY(m_dampedScenePos.y());
330 m_dampedScenePos.update(touchScenePos);
331 updateVelocityCalculator(touchScenePos);
333 if (!pointInsideAllowedArea()) {
334 ddaDebug(
"Rejecting gesture because touch point is outside allowed area.");
335 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
337 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
338 setStatus(WaitingForTouch);
342 if (!movingInRightDirection()) {
343 ddaDebug(
"Rejecting gesture because touch point is moving in the wrong direction.");
344 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
346 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
347 setStatus(WaitingForTouch);
351 setPreviousPos(touchPoint->pos());
352 setPreviousScenePos(touchScenePos);
354 if (isWithinTouchCompositionWindow()) {
357 ddaDebug(
"Sill within composition window. Let's wait more.");
361 if (movedFarEnough(touchScenePos)) {
362 TouchRegistry::instance()->requestTouchOwnership(m_touchId,
this);
363 setStatus(Recognized);
365 ddaDebug(
"Didn't move far enough yet. Let's wait more.");
369 void DirectionalDragArea::touchEvent(QTouchEvent *event)
374 ddaDebug(m_timeSource->msecsSinceReference() <<
" " << qPrintable(touchEventToString(event)));
376 if (!isEnabled() || !isVisible()) {
377 QQuickItem::touchEvent(event);
382 case WaitingForTouch:
383 touchEvent_absent(event);
386 touchEvent_undecided(event);
389 touchEvent_recognized(event);
393 m_activeTouches.update(event);
396 void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
400 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
408 if (isWithinTouchCompositionWindow()) {
411 ddaDebug(
"A new touch point came in but we're still within time composition window. Ignoring it.");
415 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
417 const QTouchEvent::TouchPoint *newTouchPoint =
nullptr;
418 for (
int i = 0; i < touchPoints.count() && allGood; ++i) {
419 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
420 if (touchPoint.state() == Qt::TouchPointPressed) {
426 m_touchId = touchPoint.id();
427 newTouchPoint = &touchPoint;
433 Q_ASSERT(newTouchPoint);
435 m_startPos = newTouchPoint->pos();
436 m_startScenePos = newTouchPoint->scenePos();
437 m_touchId = newTouchPoint->id();
438 m_dampedScenePos.reset(m_startScenePos);
439 m_velocityCalculator->setTrackedPosition(0.);
440 m_velocityCalculator->reset();
441 m_numSamplesOnLastSpeedCheck = 0;
443 setPreviousPos(m_startPos);
444 setPreviousScenePos(m_startScenePos);
445 updateSceneDirectionVector();
447 if (recognitionIsDisabled()) {
449 ddaDebug(
"Gesture recognition is disabled. Requesting touch ownership immediately.");
450 TouchRegistry::instance()->requestTouchOwnership(m_touchId,
this);
451 setStatus(Recognized);
455 TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId,
this);
457 setStatus(Undecided);
463 watchPressedTouchPoints(touchPoints);
468 void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
470 Q_ASSERT(event->type() == QEvent::TouchBegin);
471 Q_ASSERT(fetchTargetTouchPoint(event) ==
nullptr);
477 watchPressedTouchPoints(event->touchPoints());
479 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
481 ddaDebug(
"Multi-finger drags are not accepted");
483 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
485 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
487 setStatus(WaitingForTouch);
491 void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
493 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
496 qCritical() <<
"DirectionalDragArea[status=Recognized]: touch " << m_touchId
497 <<
"missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
498 "Considering it as released.";
499 setStatus(WaitingForTouch);
501 setPreviousPos(touchPoint->pos());
502 setPreviousScenePos(touchPoint->scenePos());
504 if (touchPoint->state() == Qt::TouchPointReleased) {
505 emitSignalIfTapped();
506 setStatus(WaitingForTouch);
511 void DirectionalDragArea::watchPressedTouchPoints(
const QList<QTouchEvent::TouchPoint> &touchPoints)
513 for (
int i = 0; i < touchPoints.count(); ++i) {
514 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
515 if (touchPoint.state() == Qt::TouchPointPressed) {
516 TouchRegistry::instance()->addTouchWatcher(touchPoint.id(),
this);
521 bool DirectionalDragArea::recognitionIsDisabled()
const
523 return distanceThreshold() <= 0 && compositionTime() <= 0;
526 void DirectionalDragArea::emitSignalIfTapped()
528 qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
529 if (touchDuration <= maxTapDuration()) {
534 const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
536 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
537 const QTouchEvent::TouchPoint *touchPoint = 0;
538 for (
int i = 0; i < touchPoints.size(); ++i) {
539 if (touchPoints.at(i).id() == m_touchId) {
540 touchPoint = &touchPoints.at(i);
547 bool DirectionalDragArea::pointInsideAllowedArea()
const
552 QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
553 m_dampedScenePos.y() - m_startScenePos.y());
555 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
556 totalMovement.y() * totalMovement.y();
558 if (squaredTotalMovSize == 0.) {
563 qreal projectedMovement = projectOntoDirectionVector(totalMovement);
566 qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
570 return cosineAngleSquared >= m_wideningFactor;
573 bool DirectionalDragArea::movingInRightDirection()
const
575 if (m_direction == Direction::Horizontal) {
578 QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
579 m_dampedScenePos.y() - m_previousDampedScenePos.y());
581 qreal scalarProjection = projectOntoDirectionVector(movementVector);
583 return scalarProjection >= 0.;
587 bool DirectionalDragArea::movedFarEnough(
const QPointF &point)
const
589 if (m_distanceThreshold <= 0.) {
593 QPointF totalMovement(point.x() - m_startScenePos.x(),
594 point.y() - m_startScenePos.y());
596 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
597 totalMovement.y() * totalMovement.y();
599 return squaredTotalMovSize > m_distanceThresholdSquared;
603 void DirectionalDragArea::checkSpeed()
605 Q_ASSERT(m_status == Undecided);
607 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
608 qreal speed = qFabs(m_velocityCalculator->calculate());
609 qreal minSpeedMsecs = m_minSpeed / 1000.0;
611 if (speed < minSpeedMsecs) {
612 ddaDebug(
"Rejecting gesture because it's below minimum speed.");
613 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
614 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
615 setStatus(WaitingForTouch);
619 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
620 m_silenceTime += m_recognitionTimer->interval();
622 if (m_silenceTime > m_maxSilenceTime) {
623 ddaDebug(
"Rejecting gesture because its silence time has been exceeded.");
624 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
625 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
626 setStatus(WaitingForTouch);
631 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
634 void DirectionalDragArea::giveUpIfDisabledOrInvisible()
636 if (!isEnabled() || !isVisible()) {
637 if (m_status == Undecided) {
638 TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId,
this);
640 TouchRegistry::instance()->addTouchWatcher(m_touchId,
this);
643 if (m_status != WaitingForTouch) {
644 ddaDebug(
"Resetting status because got disabled or made invisible");
645 setStatus(WaitingForTouch);
650 void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
652 if (newStatus == m_status)
655 DirectionalDragArea::Status oldStatus = m_status;
657 if (oldStatus == Undecided) {
658 m_recognitionTimer->stop();
661 m_status = newStatus;
662 Q_EMIT statusChanged(m_status);
664 ddaDebug(statusToString(oldStatus) <<
" -> " << statusToString(newStatus));
667 case WaitingForTouch:
668 Q_EMIT draggingChanged(
false);
671 m_recognitionTimer->start();
672 Q_EMIT draggingChanged(
true);
675 if (oldStatus == WaitingForTouch)
676 Q_EMIT draggingChanged(
true);
684 void DirectionalDragArea::setPreviousPos(
const QPointF &point)
686 bool xChanged = m_previousPos.x() != point.x();
687 bool yChanged = m_previousPos.y() != point.y();
689 m_previousPos = point;
692 Q_EMIT touchXChanged(point.x());
693 if (Direction::isHorizontal(m_direction))
694 Q_EMIT distanceChanged(distance());
698 Q_EMIT touchYChanged(point.y());
699 if (Direction::isVertical(m_direction))
700 Q_EMIT distanceChanged(distance());
704 void DirectionalDragArea::setPreviousScenePos(
const QPointF &point)
706 bool xChanged = m_previousScenePos.x() != point.x();
707 bool yChanged = m_previousScenePos.y() != point.y();
709 if (!xChanged && !yChanged)
712 qreal oldSceneDistance = sceneDistance();
713 m_previousScenePos = point;
714 updateSceneDistance();
716 if (oldSceneDistance != sceneDistance()) {
717 Q_EMIT sceneDistanceChanged(sceneDistance());
721 Q_EMIT touchSceneXChanged(point.x());
725 Q_EMIT touchSceneYChanged(point.y());
729 void DirectionalDragArea::updateVelocityCalculator(
const QPointF &scenePos)
731 QPointF totalSceneMovement = scenePos - m_startScenePos;
733 qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
735 m_velocityCalculator->setTrackedPosition(scalarProjection);
738 bool DirectionalDragArea::isWithinTouchCompositionWindow()
741 compositionTime() > 0 &&
742 !m_activeTouches.isEmpty() &&
743 m_timeSource->msecsSinceReference() <=
744 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
749 DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(
const SharedTimeSource &timeSource)
750 : m_timeSource(timeSource)
754 void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
756 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
758 #if ACTIVETOUCHESINFO_DEBUG
759 qDebug(
"[DDA::ActiveTouchesInfo] Nothing to Update");
764 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
765 for (
int i = 0; i < touchPoints.count(); ++i) {
766 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
767 if (touchPoint.state() == Qt::TouchPointPressed) {
768 addTouchPoint(touchPoint.id());
769 }
else if (touchPoint.state() == Qt::TouchPointReleased) {
770 removeTouchPoint(touchPoint.id());
775 #if ACTIVETOUCHESINFO_DEBUG
776 QString DirectionalDragArea::ActiveTouchesInfo::toString()
778 QString
string =
"(";
781 QTextStream stream(&
string);
782 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
783 stream <<
"(id=" << touchInfo->id <<
",startTime=" << touchInfo->startTime <<
")";
792 #endif // ACTIVETOUCHESINFO_DEBUG
794 void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(
int touchId)
796 ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
797 activeTouchInfo.id = touchId;
798 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
800 #if ACTIVETOUCHESINFO_DEBUG
801 qDebug() <<
"[DDA::ActiveTouchesInfo]" << qPrintable(toString());
805 qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(
int touchId)
809 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
810 if (touchId == touchInfo->id) {
811 result = touchInfo->startTime;
818 Q_ASSERT(result != -1);
822 void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(
int touchId)
824 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
825 if (touchId == touchInfo->id) {
826 m_touchInfoPool.freeSlot(touchInfo);
833 #if ACTIVETOUCHESINFO_DEBUG
834 qDebug() <<
"[DDA::ActiveTouchesInfo]" << qPrintable(toString());
838 qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
840 Q_ASSERT(!m_touchInfoPool.isEmpty());
842 qint64 highestStartTime = -1;
844 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
845 if (activeTouchInfo->startTime > highestStartTime) {
846 highestStartTime = activeTouchInfo->startTime;
851 return highestStartTime;
854 void DirectionalDragArea::updateSceneDirectionVector()
856 QPointF localOrigin(0., 0.);
857 QPointF localDirection;
858 switch (m_direction) {
859 case Direction::Upwards:
860 localDirection.rx() = 0.;
861 localDirection.ry() = -1.;
863 case Direction::Downwards:
864 localDirection.rx() = 0.;
865 localDirection.ry() = 1;
867 case Direction::Leftwards:
868 localDirection.rx() = -1.;
869 localDirection.ry() = 0.;
872 localDirection.rx() = 1.;
873 localDirection.ry() = 0.;
876 QPointF sceneOrigin = mapToScene(localOrigin);
877 QPointF sceneDirection = mapToScene(localDirection);
878 m_sceneDirectionVector = sceneDirection - sceneOrigin;
881 qreal DirectionalDragArea::projectOntoDirectionVector(
const QPointF &sceneVector)
const
884 return sceneVector.x() * m_sceneDirectionVector.x() +
885 sceneVector.y() * m_sceneDirectionVector.y();