Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2026 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program 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
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "polylineplot.h"
24 :
25 : #include <QCursor>
26 : #include <QPainter>
27 : #include <QPainterPath>
28 : #include <QPen>
29 : #include <QBrush>
30 : #include <QHoverEvent>
31 : #include <QMouseEvent>
32 :
33 : #include <algorithm>
34 : #include <cmath>
35 :
36 : #include "realfn.h"
37 :
38 : using namespace muse::uicomponents;
39 :
40 : namespace {
41 : constexpr double MOVE_THRESHOLD = 3.0;
42 : constexpr double EPSILON = 1e-12;
43 : constexpr double BOUNDARY_MARGIN = 0.1;
44 :
45 : static constexpr int INVALID_POINT_IDX = -1;
46 : static constexpr int PENDING_POINT_IDX = -2;
47 :
48 0 : static inline qreal toPxX(const QQuickItem* item, qreal xN)
49 : {
50 0 : return xN * item->width();
51 : }
52 :
53 0 : static inline qreal toPxY(const QQuickItem* item, qreal yN)
54 : {
55 0 : const qreal h = item->height();
56 0 : if (h <= 0.0) {
57 0 : return 0.0;
58 : }
59 :
60 : // NOTE: preserve full height of the line at top/bottom edges
61 0 : const auto* polyline = static_cast<const PolylinePlot*>(item);
62 0 : const qreal inset = std::clamp(polyline->lineWidth() * 0.5, 0.0, h * 0.5);
63 0 : const qreal drawableHeight = h - (2.0 * inset);
64 :
65 0 : return inset + (1.0 - yN) * drawableHeight;
66 : }
67 :
68 0 : static qreal pointToSegmentDistance(const QPointF& point, const QPointF& segmentStart, const QPointF& segmentEnd)
69 : {
70 0 : const QPointF segment = segmentEnd - segmentStart;
71 0 : const qreal segmentLengthSquared = QPointF::dotProduct(segment, segment);
72 0 : if (segmentLengthSquared <= EPSILON) {
73 0 : return std::hypot(point.x() - segmentStart.x(), point.y() - segmentStart.y());
74 : }
75 :
76 0 : const QPointF segmentStartToPoint = point - segmentStart;
77 0 : qreal t = QPointF::dotProduct(segmentStartToPoint, segment) / segmentLengthSquared;
78 0 : t = std::max<qreal>(0.0, std::min<qreal>(1.0, t));
79 :
80 0 : const QPointF closestPoint = segmentStart + segment * t;
81 0 : return std::hypot(point.x() - closestPoint.x(), point.y() - closestPoint.y());
82 : }
83 :
84 0 : static GhostPoint ghostPointToSegmentDistance(const QPointF& point, const QPointF& segmentStart, const QPointF& segmentEnd)
85 : {
86 0 : const QPointF segment = segmentEnd - segmentStart;
87 0 : const qreal segmentLengthSquared = QPointF::dotProduct(segment, segment);
88 0 : if (segmentLengthSquared <= EPSILON) {
89 0 : const qreal distance = std::hypot(point.x() - segmentStart.x(), point.y() - segmentStart.y());
90 0 : return { segmentStart, distance };
91 : }
92 :
93 0 : const QPointF segmentStartToPoint = point - segmentStart;
94 0 : qreal t = QPointF::dotProduct(segmentStartToPoint, segment) / segmentLengthSquared;
95 0 : t = std::max<qreal>(0.0, std::min<qreal>(1.0, t));
96 :
97 0 : const QPointF closestPoint = segmentStart + segment * t;
98 0 : const qreal distance = std::hypot(point.x() - closestPoint.x(), point.y() - closestPoint.y());
99 0 : return { closestPoint, distance };
100 : }
101 :
102 : // NOTE: can be replaced with std::lerp in C++20
103 0 : static double lerp(double a, double b, double t)
104 : {
105 0 : return a + (b - a) * t;
106 : }
107 :
108 : // compute Y at X using linear interpolation
109 0 : static double valueAtX(const QVector<QPointF>& sortedPoints, double x)
110 : {
111 0 : if (sortedPoints.isEmpty()) {
112 0 : return 0.0;
113 : }
114 :
115 0 : if (x <= sortedPoints.front().x()) {
116 0 : return sortedPoints.front().y();
117 : }
118 :
119 0 : if (x >= sortedPoints.back().x()) {
120 0 : return sortedPoints.back().y();
121 : }
122 :
123 0 : for (int i = 0; i < sortedPoints.size() - 1; ++i) {
124 0 : const auto& a = sortedPoints[i];
125 0 : const auto& b = sortedPoints[i + 1];
126 0 : if (x >= a.x() && x <= b.x()) {
127 0 : const double dx = b.x() - a.x();
128 0 : if (std::abs(dx) <= EPSILON) {
129 0 : return a.y();
130 : }
131 0 : const double t = (x - a.x()) / dx;
132 0 : return lerp(a.y(), b.y(), t);
133 : }
134 : }
135 :
136 0 : return sortedPoints.back().y();
137 : }
138 : }
139 :
140 0 : PolylinePlot::PolylinePlot(QQuickItem* parent)
141 0 : : QQuickPaintedItem(parent)
142 : {
143 0 : setAcceptHoverEvents(true);
144 0 : setAcceptedMouseButtons(Qt::LeftButton);
145 :
146 0 : setAntialiasing(true);
147 0 : setOpaquePainting(false);
148 :
149 0 : m_standardPointStyle = new PolylinePointStyle(this);
150 0 : QObject::connect(m_standardPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
151 0 : update();
152 0 : });
153 :
154 0 : m_ghostPointStyle = new PolylinePointStyle(this);
155 0 : QObject::connect(m_ghostPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
156 0 : update();
157 0 : });
158 :
159 0 : m_selectedPointStyle = new PolylinePointStyle(this);
160 0 : QObject::connect(m_selectedPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
161 0 : update();
162 0 : });
163 0 : }
164 :
165 0 : void PolylinePlot::cancelEdit()
166 : {
167 0 : m_hoveredOnLine = false;
168 0 : resetGestureState();
169 0 : }
170 :
171 0 : PolylinePointStyle* PolylinePlot::standardPointStyle()
172 : {
173 0 : return m_standardPointStyle;
174 : }
175 :
176 0 : PolylinePointStyle* PolylinePlot::ghostPointStyle()
177 : {
178 0 : return m_ghostPointStyle;
179 : }
180 :
181 0 : PolylinePointStyle* PolylinePlot::selectedPointStyle()
182 : {
183 0 : return m_selectedPointStyle;
184 : }
185 :
186 0 : bool PolylinePlot::ghostPointsEnabled() const
187 : {
188 0 : return m_ghostPointsEnabled;
189 : }
190 :
191 0 : void PolylinePlot::setGhostPointsEnabled(bool v)
192 : {
193 0 : if (m_ghostPointsEnabled == v) {
194 0 : return;
195 : }
196 :
197 0 : m_ghostPointsEnabled = v;
198 0 : emit ghostPointsEnabledChanged();
199 :
200 0 : update();
201 : }
202 :
203 0 : bool PolylinePlot::selectedPointsEnabled() const
204 : {
205 0 : return m_selectedPointsEnabled;
206 : }
207 :
208 0 : void PolylinePlot::setSelectedPointsEnabled(bool v)
209 : {
210 0 : if (m_selectedPointsEnabled == v) {
211 0 : return;
212 : }
213 :
214 0 : m_selectedPointsEnabled = v;
215 0 : emit selectedPointsEnabledChanged();
216 :
217 0 : update();
218 : }
219 :
220 0 : QColor PolylinePlot::lineColor() const
221 : {
222 0 : return m_lineColor;
223 : }
224 :
225 0 : void PolylinePlot::setLineColor(const QColor& c)
226 : {
227 0 : if (m_lineColor == c) {
228 0 : return;
229 : }
230 :
231 0 : m_lineColor = c;
232 0 : emit lineColorChanged();
233 0 : update();
234 : }
235 :
236 0 : qreal PolylinePlot::lineWidth() const
237 : {
238 0 : return m_lineWidth;
239 : }
240 :
241 0 : void PolylinePlot::setLineWidth(qreal w)
242 : {
243 0 : if (m_lineWidth == w) {
244 0 : return;
245 : }
246 :
247 0 : m_lineWidth = w;
248 0 : emit lineWidthChanged();
249 :
250 0 : update();
251 : }
252 :
253 0 : bool PolylinePlot::drawBackground() const
254 : {
255 0 : return m_drawBackground;
256 : }
257 :
258 0 : void PolylinePlot::setDrawBackground(bool v)
259 : {
260 0 : if (m_drawBackground == v) {
261 0 : return;
262 : }
263 :
264 0 : m_drawBackground = v;
265 0 : emit drawBackgroundChanged();
266 :
267 0 : update();
268 : }
269 :
270 0 : qreal PolylinePlot::baselineN() const
271 : {
272 0 : return m_baselineN;
273 : }
274 :
275 0 : void PolylinePlot::setBaselineN(qreal v)
276 : {
277 0 : v = clamp01(v);
278 :
279 0 : if (m_baselineN == v) {
280 0 : return;
281 : }
282 :
283 0 : m_baselineN = v;
284 0 : emit baselineNChanged();
285 :
286 0 : update();
287 : }
288 :
289 0 : qreal PolylinePlot::hitRadius() const
290 : {
291 0 : return m_hitRadius;
292 : }
293 :
294 0 : void PolylinePlot::setHitRadius(qreal r)
295 : {
296 0 : if (m_hitRadius == r) {
297 0 : return;
298 : }
299 :
300 0 : m_hitRadius = r;
301 0 : emit hitRadiusChanged();
302 : }
303 :
304 0 : bool PolylinePlot::isSnapEnabled() const
305 : {
306 0 : return m_isSnapEnabled;
307 : }
308 :
309 0 : void PolylinePlot::setIsSnapEnabled(bool v)
310 : {
311 0 : if (m_isSnapEnabled == v) {
312 0 : return;
313 : }
314 :
315 0 : m_isSnapEnabled = v;
316 0 : emit isSnapEnabledChanged();
317 : }
318 :
319 0 : qreal PolylinePlot::snapThresholdPx() const
320 : {
321 0 : return m_snapThresholdPx;
322 : }
323 :
324 0 : void PolylinePlot::setSnapThresholdPx(qreal v)
325 : {
326 0 : if (m_snapThresholdPx == v) {
327 0 : return;
328 : }
329 :
330 0 : m_snapThresholdPx = v;
331 0 : emit snapThresholdPxChanged();
332 : }
333 :
334 0 : QVector<QPointF> PolylinePlot::points() const
335 : {
336 0 : return m_points;
337 : }
338 :
339 0 : void PolylinePlot::setPoints(const QVector<QPointF>& pts)
340 : {
341 0 : if (m_points == pts) {
342 0 : if (m_points.isEmpty()) {
343 0 : updateBaselineFromDefaultValue();
344 :
345 : // invalidate colors under line
346 0 : if (!m_colorsUnderLine.empty()) {
347 0 : m_colorsUnderLine.clear();
348 0 : emit colorsUnderLineChanged();
349 : }
350 :
351 0 : update();
352 : }
353 0 : return;
354 : }
355 0 : m_points = pts;
356 0 : emit pointsChanged();
357 :
358 : // invalidate colors under line
359 0 : if (!m_colorsUnderLine.empty()) {
360 0 : m_colorsUnderLine.clear();
361 0 : emit colorsUnderLineChanged();
362 : }
363 :
364 0 : if (m_points.isEmpty()) {
365 0 : updateBaselineFromDefaultValue();
366 : }
367 :
368 : // if there's pending point, find its index and mark as ready for drag
369 0 : if (m_pressed && m_pressedPointIndex == PENDING_POINT_IDX && m_pressedOnLine) {
370 0 : if (width() > 0 && height() > 0 && !m_points.isEmpty()) {
371 0 : const auto ghostPoint = ghostPointToPolylinePx(m_pressPx);
372 :
373 0 : QPointF pN(clamp01(ghostPoint.point.x() / width()),
374 0 : 1.0 - clamp01(ghostPoint.point.y() / height()));
375 :
376 0 : const QPointF targetDomain = domainFromNormalized(pN);
377 :
378 0 : int bestIdx = INVALID_POINT_IDX;
379 0 : double bestScore = std::numeric_limits<double>::max();
380 :
381 0 : for (int i = 0; i < m_points.size(); ++i) {
382 0 : const double dx = std::abs(m_points[i].x() - targetDomain.x());
383 0 : const double dy = std::abs(m_points[i].y() - targetDomain.y());
384 0 : const double score = dx * 1000.0 + dy;
385 0 : if (score < bestScore) {
386 0 : bestScore = score;
387 0 : bestIdx = i;
388 : }
389 : }
390 :
391 0 : if (bestIdx >= 0) {
392 0 : m_pressedOnPoint = true;
393 0 : m_pressedPointIndex = bestIdx;
394 0 : m_draggedPointDomain = m_points[bestIdx];
395 0 : m_hasDraggedPointDomain = true;
396 : }
397 : }
398 : }
399 :
400 0 : rebuildVisiblePoints();
401 : }
402 :
403 0 : QVector<QColor> PolylinePlot::colorsUnderLine() const
404 : {
405 0 : return m_colorsUnderLine;
406 : }
407 :
408 0 : void PolylinePlot::setColorsUnderLine(const QVector<QColor>& c)
409 : {
410 0 : if (m_colorsUnderLine == c) {
411 0 : return;
412 : }
413 :
414 0 : m_colorsUnderLine = c;
415 0 : emit colorsUnderLineChanged();
416 :
417 0 : update();
418 : }
419 :
420 0 : qreal PolylinePlot::defaultValue() const
421 : {
422 0 : return m_defaultValue;
423 : }
424 :
425 0 : void PolylinePlot::setXRangeFrom(qreal v)
426 : {
427 0 : if (m_xFrom == v) {
428 0 : return;
429 : }
430 :
431 0 : m_xFrom = v;
432 0 : emit xRangeFromChanged();
433 :
434 0 : rebuildVisiblePoints();
435 : }
436 :
437 0 : qreal PolylinePlot::xRangeTo() const
438 : {
439 0 : return m_xTo;
440 : }
441 :
442 0 : void PolylinePlot::setXRangeTo(qreal v)
443 : {
444 0 : if (m_xTo == v) {
445 0 : return;
446 : }
447 :
448 0 : m_xTo = v;
449 0 : emit xRangeToChanged();
450 :
451 0 : rebuildVisiblePoints();
452 : }
453 :
454 0 : qreal PolylinePlot::yRangeFrom() const
455 : {
456 0 : return m_yFrom;
457 : }
458 :
459 0 : void PolylinePlot::setYRangeFrom(qreal v)
460 : {
461 0 : if (m_yFrom == v) {
462 0 : return;
463 : }
464 :
465 0 : m_yFrom = v;
466 0 : emit yRangeFromChanged();
467 :
468 0 : rebuildVisiblePoints();
469 : }
470 :
471 0 : qreal PolylinePlot::yRangeTo() const
472 : {
473 0 : return m_yTo;
474 : }
475 :
476 0 : void PolylinePlot::setYRangeTo(qreal v)
477 : {
478 0 : if (m_yTo == v) {
479 0 : return;
480 : }
481 :
482 0 : m_yTo = v;
483 0 : emit yRangeToChanged();
484 :
485 0 : rebuildVisiblePoints();
486 : }
487 :
488 0 : qreal PolylinePlot::ySplitNormalized() const
489 : {
490 0 : return m_ySplitNormalized;
491 : }
492 :
493 0 : void PolylinePlot::setYSplitNormalized(qreal v)
494 : {
495 0 : v = clamp01(v);
496 0 : if (m_ySplitNormalized == v) {
497 0 : return;
498 : }
499 :
500 0 : m_ySplitNormalized = v;
501 0 : emit ySplitNormalizedChanged();
502 :
503 0 : rebuildVisiblePoints();
504 : }
505 :
506 0 : qreal PolylinePlot::ySplitValue() const
507 : {
508 0 : return m_ySplitValue;
509 : }
510 :
511 0 : void PolylinePlot::setYSplitValue(qreal v)
512 : {
513 0 : const qreal low = std::min(m_yFrom, m_yTo);
514 0 : const qreal high = std::max(m_yFrom, m_yTo);
515 0 : qreal clampedValue = std::clamp(v, low, high);
516 :
517 0 : if (m_ySplitValue == clampedValue) {
518 0 : return;
519 : }
520 :
521 0 : m_ySplitValue = clampedValue;
522 0 : emit ySplitValueChanged();
523 :
524 0 : rebuildVisiblePoints();
525 : }
526 :
527 0 : bool PolylinePlot::yAxisInverse() const
528 : {
529 0 : return m_yAxisInverse;
530 : }
531 :
532 0 : void PolylinePlot::setYAxisInverse(bool v)
533 : {
534 0 : if (m_yAxisInverse == v) {
535 0 : return;
536 : }
537 :
538 0 : m_yAxisInverse = v;
539 0 : emit yAxisInverseChanged();
540 0 : rebuildVisiblePoints();
541 : }
542 :
543 0 : bool PolylinePlot::hasActivePoint() const
544 : {
545 0 : return m_hasActivePoint;
546 : }
547 :
548 0 : qreal PolylinePlot::activePointX() const
549 : {
550 0 : return m_activePointPx.x();
551 : }
552 :
553 0 : qreal PolylinePlot::activePointY() const
554 : {
555 0 : return m_activePointPx.y();
556 : }
557 :
558 0 : qreal PolylinePlot::activePointValue() const
559 : {
560 0 : return m_activePointValue;
561 : }
562 :
563 0 : void PolylinePlot::setDefaultValue(qreal v)
564 : {
565 0 : if (m_defaultValue == v) {
566 0 : return;
567 : }
568 :
569 0 : m_defaultValue = v;
570 0 : emit defaultValueChanged();
571 :
572 : // if there are no points, baseline should reflect defaultY immediately
573 0 : if (m_points.isEmpty()) {
574 0 : updateBaselineFromDefaultValue();
575 0 : rebuildVisiblePoints();
576 : }
577 : }
578 :
579 0 : qreal PolylinePlot::xRangeFrom() const
580 : {
581 0 : return m_xFrom;
582 : }
583 :
584 0 : qreal PolylinePlot::clamp01(qreal v) const
585 : {
586 0 : return std::max<qreal>(0.0, std::min<qreal>(1.0, v));
587 : }
588 :
589 0 : QPointF PolylinePlot::clamp01(const QPointF& p) const
590 : {
591 0 : return QPointF(clamp01(p.x()), clamp01(p.y()));
592 : }
593 :
594 0 : bool PolylinePlot::hasValidXRange() const
595 : {
596 0 : return std::isfinite(m_xFrom) && std::isfinite(m_xTo) && std::abs(m_xTo - m_xFrom) > EPSILON;
597 : }
598 :
599 0 : bool PolylinePlot::hasValidYRange() const
600 : {
601 0 : return std::isfinite(m_yFrom) && std::isfinite(m_yTo) && std::abs(m_yTo - m_yFrom) > EPSILON;
602 : }
603 :
604 0 : bool PolylinePlot::hasValidYSplit() const
605 : {
606 0 : return hasValidYRange() && !muse::RealIsEqualOrLess(m_ySplitNormalized, 0.0) && !muse::RealIsEqualOrMore(m_ySplitNormalized, 1.0);
607 : }
608 :
609 0 : qreal PolylinePlot::yDomainFromNormalized(qreal yNormalized) const
610 : {
611 0 : const qreal n = clamp01(yNormalized);
612 :
613 : // simple linear mapping (no split)
614 0 : if (!hasValidYSplit()) {
615 0 : return m_yFrom + n * (m_yTo - m_yFrom);
616 : }
617 :
618 0 : const qreal splitNormalized = m_ySplitNormalized;
619 0 : const qreal splitValue = m_ySplitValue;
620 :
621 : // first segment (bottom → split)
622 0 : if (n <= splitNormalized) {
623 0 : if (splitNormalized <= EPSILON) {
624 0 : return splitValue;
625 : }
626 :
627 0 : const qreal segmentN = n / splitNormalized;
628 0 : return m_yFrom + segmentN * (splitValue - m_yFrom);
629 : }
630 :
631 : // second segment (split → top)
632 0 : const qreal secondSegmentSize = 1.0 - splitNormalized;
633 0 : if (secondSegmentSize <= EPSILON) {
634 0 : return splitValue;
635 : }
636 :
637 0 : const qreal segmentN = (n - splitNormalized) / secondSegmentSize;
638 0 : return splitValue + segmentN * (m_yTo - splitValue);
639 : }
640 :
641 0 : qreal PolylinePlot::yNormalizedFromDomain(qreal yDomain) const
642 : {
643 : // simple linear mapping (no split)
644 0 : if (!hasValidYSplit()) {
645 0 : return (yDomain - m_yFrom) / (m_yTo - m_yFrom);
646 : }
647 :
648 0 : const qreal splitNormalized = m_ySplitNormalized;
649 0 : const qreal splitValue = m_ySplitValue;
650 :
651 0 : const bool increasing = (m_yTo >= m_yFrom);
652 :
653 0 : const bool isInFirstSegment
654 0 : =increasing ? (yDomain <= splitValue)
655 : : (yDomain >= splitValue);
656 :
657 : // ---- first segment ----
658 0 : if (isInFirstSegment) {
659 0 : const qreal segmentSize = splitValue - m_yFrom;
660 0 : if (std::abs(segmentSize) <= EPSILON) {
661 0 : return splitNormalized;
662 : }
663 :
664 0 : const qreal segmentN = (yDomain - m_yFrom) / segmentSize;
665 0 : return segmentN * splitNormalized;
666 : }
667 :
668 : // ---- second segment ----
669 0 : const qreal segmentSize = m_yTo - splitValue;
670 0 : if (std::abs(segmentSize) <= EPSILON) {
671 0 : return splitNormalized;
672 : }
673 :
674 0 : const qreal segmentN = (yDomain - splitValue) / segmentSize;
675 0 : return splitNormalized + segmentN * (1.0 - splitNormalized);
676 : }
677 :
678 0 : void PolylinePlot::updateBaselineFromDefaultValue()
679 : {
680 0 : const qreal baseline = normalizedFromDomain(QPointF(m_xFrom, m_defaultValue)).y();
681 0 : if (m_baselineN == baseline) {
682 0 : return;
683 : }
684 :
685 0 : m_baselineN = baseline;
686 0 : emit baselineNChanged();
687 : }
688 :
689 0 : void PolylinePlot::updateActivePoint()
690 : {
691 0 : const int hoveredDomainIdx = pointIndexAtPx(m_hoverPx);
692 0 : int draggedDisplayDomainIdx = INVALID_POINT_IDX;
693 0 : if (m_pressed && m_hasDraggedPointDomain && !m_points.isEmpty()) {
694 0 : double bestScore = std::numeric_limits<double>::max();
695 0 : for (int i = 0; i < m_points.size(); ++i) {
696 0 : const double dx = std::abs(m_points[i].x() - m_draggedPointDomain.x());
697 0 : const double dy = std::abs(m_points[i].y() - m_draggedPointDomain.y());
698 0 : const double score = dx * 1000.0 + dy;
699 0 : if (score < bestScore) {
700 0 : bestScore = score;
701 0 : draggedDisplayDomainIdx = i;
702 : }
703 : }
704 : }
705 :
706 : // m_pressedPointIndex must stay at the original model index (so consumed neighbors can be restored), but selection indices
707 : // need to shift when other indices are deleted...
708 0 : if (m_selectedPointsEnabled && draggedDisplayDomainIdx >= 0) {
709 0 : if (m_selectedPointsIndices.size() != 1 || !muse::contains(m_selectedPointsIndices, draggedDisplayDomainIdx)) {
710 0 : m_selectedPointsIndices.clear();
711 0 : m_selectedPointsIndices.insert(draggedDisplayDomainIdx);
712 : }
713 : }
714 :
715 0 : const int activeDomainIdx = (draggedDisplayDomainIdx >= 0)
716 0 : ? draggedDisplayDomainIdx
717 0 : : ((m_pressedPointIndex >= 0) ? m_pressedPointIndex : hoveredDomainIdx);
718 :
719 0 : if (activeDomainIdx < 0) {
720 0 : if (m_hasActivePoint) {
721 0 : m_hasActivePoint = false;
722 0 : m_activePointPx = {};
723 0 : m_activePointValue = 0.0;
724 0 : emit activePointChanged();
725 : }
726 0 : return;
727 : }
728 :
729 0 : QPointF activeN;
730 0 : bool found = false;
731 0 : for (int i = 0; i < m_pointsNVisible.size() && i < m_visibleToDomainIndex.size(); ++i) {
732 0 : if (m_visibleToDomainIndex[i] == activeDomainIdx) {
733 0 : activeN = m_pointsNVisible[i];
734 0 : found = true;
735 0 : break;
736 : }
737 : }
738 :
739 0 : if (!found) {
740 0 : if (m_hasActivePoint) {
741 0 : m_hasActivePoint = false;
742 0 : m_activePointPx = {};
743 0 : m_activePointValue = 0.0;
744 0 : emit activePointChanged();
745 : }
746 0 : return;
747 : }
748 :
749 0 : const QPointF newPx(toPxX(this, activeN.x()), toPxY(this, activeN.y()));
750 0 : const qreal newValue = (draggedDisplayDomainIdx >= 0 && m_hasDraggedPointDomain)
751 0 : ? m_draggedPointDomain.y()
752 0 : : m_points[activeDomainIdx].y();
753 0 : const bool changed = (!m_hasActivePoint
754 0 : || newPx != m_activePointPx
755 0 : || std::abs(newValue - m_activePointValue) > EPSILON);
756 :
757 0 : m_hasActivePoint = true;
758 0 : m_activePointPx = newPx;
759 0 : m_activePointValue = newValue;
760 :
761 0 : if (changed) {
762 0 : emit activePointChanged();
763 : }
764 : }
765 :
766 0 : QPointF PolylinePlot::normalizedFromDomain(const QPointF& p) const
767 : {
768 0 : if (!hasValidXRange() || !hasValidYRange()) {
769 0 : return clamp01(QPointF(0.0, 0.0));
770 : }
771 :
772 0 : const qreal xN = (p.x() - m_xFrom) / (m_xTo - m_xFrom);
773 0 : qreal yN = clamp01(yNormalizedFromDomain(p.y()));
774 :
775 0 : if (m_yAxisInverse) {
776 0 : yN = 1.0 - yN;
777 : }
778 :
779 0 : return clamp01(QPointF(xN, yN));
780 : }
781 :
782 0 : QPointF PolylinePlot::domainFromNormalized(const QPointF& pN) const
783 : {
784 0 : if (!hasValidXRange() || !hasValidYRange()) {
785 0 : return QPointF(m_xFrom, m_yFrom);
786 : }
787 :
788 0 : const qreal x = m_xFrom + clamp01(pN.x()) * (m_xTo - m_xFrom);
789 :
790 0 : qreal yT = clamp01(pN.y());
791 0 : if (m_yAxisInverse) {
792 0 : yT = 1.0 - yT;
793 : }
794 0 : const qreal y = yDomainFromNormalized(yT);
795 :
796 0 : return QPointF(x, y);
797 : }
798 :
799 0 : QVector<QPointF> PolylinePlot::normalizedFromDomain(const QVector<QPointF>& pts) const
800 : {
801 0 : QVector<QPointF> out;
802 0 : out.reserve(pts.size());
803 0 : for (const auto& p : pts) {
804 0 : out.push_back(normalizedFromDomain(p));
805 : }
806 :
807 0 : return out;
808 0 : }
809 :
810 0 : QVector<QPointF> PolylinePlot::domainFromNormalized(const QVector<QPointF>& ptsN) const
811 : {
812 0 : QVector<QPointF> out;
813 0 : out.reserve(ptsN.size());
814 0 : for (const auto& pN : ptsN) {
815 0 : out.push_back(domainFromNormalized(pN));
816 : }
817 :
818 0 : return out;
819 0 : }
820 :
821 0 : void PolylinePlot::rebuildVisiblePoints()
822 : {
823 0 : m_pointsNVisible.clear();
824 0 : m_visibleToDomainIndex.clear();
825 :
826 0 : const double xRange = (m_xTo - m_xFrom);
827 0 : const double yRange = (m_yTo - m_yFrom);
828 :
829 0 : if (width() <= 0 || height() <= 0 || std::abs(xRange) <= 0 || std::abs(yRange) <= 0) {
830 0 : if (m_points.isEmpty()) {
831 0 : updateBaselineFromDefaultValue();
832 : }
833 0 : update();
834 0 : return;
835 : }
836 :
837 0 : if (m_points.isEmpty()) {
838 0 : updateBaselineFromDefaultValue();
839 0 : update();
840 0 : return;
841 : }
842 :
843 : struct P {
844 : QPointF p;
845 : int idx;
846 : };
847 :
848 0 : QVector<P> sortedPointsWithIndexes;
849 0 : sortedPointsWithIndexes.reserve(m_points.size());
850 0 : for (int i = 0; i < m_points.size(); ++i) {
851 0 : sortedPointsWithIndexes.push_back({ m_points[i], i });
852 : }
853 0 : std::sort(sortedPointsWithIndexes.begin(), sortedPointsWithIndexes.end(),
854 0 : [](const P& a, const P& b) { return a.p.x() < b.p.x(); });
855 :
856 0 : auto normY = [&](double yAbs) {
857 0 : double yn = yNormalizedFromDomain(yAbs);
858 0 : if (m_yAxisInverse) {
859 0 : yn = 1.0 - yn;
860 : }
861 0 : return yn;
862 0 : };
863 :
864 : // interpolate at window edges
865 0 : QVector<QPointF> sortedPointsN;
866 0 : sortedPointsN.reserve(sortedPointsWithIndexes.size());
867 0 : for (const auto& it : sortedPointsWithIndexes) {
868 0 : sortedPointsN.push_back(QPointF(it.p.x(), normY(it.p.y())));
869 : }
870 :
871 : // Synthetic boundary points must be interpolated in rendered Y-space.
872 : // Interpolating in domain Y breaks continuity for split mappings
873 : // (e.g. segments crossing 0 dB split value).
874 0 : const double yAt0N = valueAtX(sortedPointsN, m_xFrom);
875 0 : const double yAt1N = valueAtX(sortedPointsN, m_xTo);
876 :
877 : // NOTE: synthetic boundary points are added just outside the visible range
878 : // so the polyline draws correctly: they allow horizontal segments
879 : // before the first real point and after the last real point. Especially in
880 : // cases where polyline is currently visible only partially the screen.
881 : // These are rendering-only points (no corresponding domain index).
882 :
883 : // left boundary (synthetic)
884 0 : m_pointsNVisible.push_back(QPointF(-0.1, yAt0N));
885 0 : m_visibleToDomainIndex.push_back(INVALID_POINT_IDX);
886 :
887 : // interior real points
888 0 : for (const auto& it : sortedPointsWithIndexes) {
889 0 : const auto& p = it.p;
890 : // NOTE: build visible points with a margin so points directly at the edges
891 : // of container do not flash on re-paint
892 0 : if (p.x() < (m_xFrom - BOUNDARY_MARGIN) || p.x() > (m_xTo + BOUNDARY_MARGIN)) {
893 0 : continue;
894 : }
895 0 : const double xN = (p.x() - m_xFrom) / xRange;
896 0 : m_pointsNVisible.push_back(QPointF(std::clamp(xN, 0.0, 1.0), normY(p.y())));
897 0 : m_visibleToDomainIndex.push_back(it.idx);
898 : }
899 :
900 : // right boundary (synthetic)
901 0 : m_pointsNVisible.push_back(QPointF(1.1, yAt1N));
902 0 : m_visibleToDomainIndex.push_back(INVALID_POINT_IDX);
903 :
904 0 : updateActivePoint();
905 0 : update();
906 0 : }
907 :
908 0 : QVector<QPointF> PolylinePlot::polylinePx() const
909 : {
910 0 : QVector<QPointF> pts;
911 :
912 0 : if (width() <= 0 || height() <= 0) {
913 0 : return pts;
914 : }
915 :
916 : // 0 or 1 point -> horizontal baseline
917 0 : if (m_pointsNVisible.size() < 2) {
918 0 : qreal yN = m_baselineN;
919 0 : if (m_pointsNVisible.size() == 1) {
920 0 : yN = m_pointsNVisible[0].y();
921 : }
922 0 : yN = clamp01(yN);
923 :
924 0 : const qreal y = toPxY(this, yN);
925 0 : pts.push_back(QPointF(0.0, y));
926 0 : pts.push_back(QPointF(width(), y));
927 :
928 0 : return pts;
929 : }
930 :
931 : // 2+ points -> polyline
932 0 : QVector<QPointF> sorted = m_pointsNVisible;
933 0 : std::sort(sorted.begin(), sorted.end(),
934 0 : [](const QPointF& a, const QPointF& b) { return a.x() < b.x(); });
935 :
936 0 : const QPointF firstN = sorted.front();
937 0 : const QPointF lastN = sorted.back();
938 :
939 0 : const qreal firstYpx = toPxY(this, firstN.y());
940 0 : const qreal lastYpx = toPxY(this, lastN.y());
941 :
942 0 : pts.reserve(sorted.size() + 2);
943 :
944 : // left horizontal segment start
945 0 : pts.push_back(QPointF(0.0, firstYpx));
946 :
947 : // actual points
948 0 : for (const auto& pN : sorted) {
949 0 : if (pN.x() < 0.0 || pN.x() > 1.0) {
950 : // ignore synthetic boundary points
951 0 : continue;
952 : }
953 0 : pts.push_back(QPointF(toPxX(this, clamp01(pN.x())),
954 0 : toPxY(this, pN.y())));
955 : }
956 :
957 : // right horizontal segment end
958 0 : pts.push_back(QPointF(width(), lastYpx));
959 :
960 0 : return pts;
961 0 : }
962 :
963 0 : bool PolylinePlot::isNearLinePx(const QPointF& px) const
964 : {
965 0 : const auto pts = polylinePx();
966 0 : if (pts.size() < 2) {
967 0 : return false;
968 : }
969 :
970 0 : qreal best = std::numeric_limits<qreal>::max();
971 0 : for (int i = 0; i < pts.size() - 1; ++i) {
972 0 : best = std::min(best, pointToSegmentDistance(px, pts[i], pts[i + 1]));
973 : }
974 0 : return best <= m_hitRadius;
975 0 : }
976 :
977 0 : int PolylinePlot::pointIndexAtPx(const QPointF& px) const
978 : {
979 : // search in visible points, skip synthetic boundary points
980 0 : for (int i = 0; i < m_pointsNVisible.size(); ++i) {
981 0 : const int domainIdx = (i < m_visibleToDomainIndex.size()) ? m_visibleToDomainIndex[i] : INVALID_POINT_IDX;
982 0 : if (domainIdx < 0) {
983 0 : continue;
984 : }
985 :
986 0 : QPointF pN = m_pointsNVisible[i];
987 0 : const qreal x = toPxX(this, pN.x());
988 0 : const qreal y = toPxY(this, pN.y());
989 0 : const qreal dx = px.x() - x;
990 0 : const qreal dy = px.y() - y;
991 0 : if ((dx * dx + dy * dy) <= (m_hitRadius * m_hitRadius)) {
992 0 : return domainIdx;
993 : }
994 : }
995 :
996 0 : return INVALID_POINT_IDX;
997 : }
998 :
999 0 : GhostPoint PolylinePlot::ghostPointToPolylinePx(const QPointF& px) const
1000 : {
1001 0 : GhostPoint best;
1002 :
1003 0 : const auto pts = polylinePx();
1004 0 : if (pts.size() < 2) {
1005 0 : best.point = px;
1006 0 : best.distToSegment = std::numeric_limits<qreal>::max();
1007 0 : return best;
1008 : }
1009 :
1010 0 : for (int i = 0; i < pts.size() - 1; ++i) {
1011 0 : const auto res = ghostPointToSegmentDistance(px, pts[i], pts[i + 1]);
1012 0 : if (res.distToSegment < best.distToSegment) {
1013 0 : best = res;
1014 : }
1015 : }
1016 :
1017 0 : return best;
1018 0 : }
1019 :
1020 0 : QPointF PolylinePlot::snapToNeighbor(qreal dragPxX, QPointF pDomain) const
1021 : {
1022 0 : if (!m_isSnapEnabled || !m_hasDraggedPointDomain || m_points.size() < 2) {
1023 0 : return pDomain;
1024 : }
1025 :
1026 : // Find the dragged point's actual current index in m_points (which may
1027 : // have shifted after neighbor consumption) without modifying
1028 : // m_pressedPointIndex — the model needs that to stay at the original
1029 : // value so it can restore consumed points when dragging back.
1030 0 : int currentIdx = -1;
1031 0 : double bestDist = std::numeric_limits<double>::max();
1032 0 : for (int i = 0; i < m_points.size(); ++i) {
1033 0 : const double dx = std::abs(m_points[i].x() - m_draggedPointDomain.x());
1034 0 : const double dy = std::abs(m_points[i].y() - m_draggedPointDomain.y());
1035 0 : const double dist = dx + dy;
1036 0 : if (dist < bestDist) {
1037 0 : bestDist = dist;
1038 0 : currentIdx = i;
1039 : }
1040 : }
1041 :
1042 0 : if (currentIdx < 0) {
1043 0 : return pDomain;
1044 : }
1045 :
1046 : // Snap close to — but not exactly onto — a neighbor.
1047 : // Can't have multiple automation points at the exact same timepoint.
1048 : static constexpr double SNAP_EPSILON = 1e-9;
1049 :
1050 0 : if (currentIdx + 1 < m_points.size()) {
1051 0 : const qreal neighborPxX = toPxX(this, normalizedFromDomain(m_points[currentIdx + 1]).x());
1052 0 : if (std::abs(dragPxX - neighborPxX) <= m_snapThresholdPx) {
1053 0 : pDomain.setX(m_points[currentIdx + 1].x() - SNAP_EPSILON);
1054 : }
1055 : }
1056 :
1057 0 : if (currentIdx > 0) {
1058 0 : const qreal neighborPxX = toPxX(this, normalizedFromDomain(m_points[currentIdx - 1]).x());
1059 0 : if (std::abs(dragPxX - neighborPxX) <= m_snapThresholdPx) {
1060 0 : pDomain.setX(m_points[currentIdx - 1].x() + SNAP_EPSILON);
1061 : }
1062 : }
1063 :
1064 0 : return pDomain;
1065 : }
1066 :
1067 0 : void PolylinePlot::updateCursor()
1068 : {
1069 0 : const bool interactive = m_hoveredOnLine || m_pressed || (m_pressedPointIndex >= 0);
1070 :
1071 0 : if (interactive) {
1072 0 : setCursor(Qt::CrossCursor);
1073 : } else {
1074 0 : unsetCursor();
1075 : }
1076 0 : }
1077 :
1078 0 : void PolylinePlot::resetGestureState()
1079 : {
1080 0 : m_pressed = false;
1081 0 : m_pressedOnLine = false;
1082 0 : m_pressedOnPoint = false;
1083 0 : m_pressedPointIndex = INVALID_POINT_IDX;
1084 0 : m_hasDraggedPointDomain = false;
1085 0 : m_hoveredOnLine = false;
1086 0 : m_draggedPointDomain = {};
1087 0 : m_movedSincePress = false;
1088 0 : m_pressPx = QPointF(0.0, 0.0);
1089 :
1090 0 : updateCursor();
1091 0 : updateActivePoint();
1092 0 : update();
1093 0 : }
1094 :
1095 0 : void PolylinePlot::geometryChange(const QRectF& newG, const QRectF& oldG)
1096 : {
1097 0 : QQuickPaintedItem::geometryChange(newG, oldG);
1098 0 : if (newG.size() != oldG.size()) {
1099 0 : rebuildVisiblePoints();
1100 : }
1101 0 : }
1102 :
1103 0 : void PolylinePlot::drawLinesAndFillUnder(QPainter* painter) const
1104 : {
1105 0 : const auto pts = polylinePx();
1106 0 : if (pts.size() < 2) {
1107 0 : return;
1108 : }
1109 :
1110 0 : QPen pen(m_lineColor);
1111 0 : pen.setWidthF(m_lineWidth);
1112 0 : pen.setJoinStyle(Qt::MiterJoin);
1113 0 : painter->setPen(pen);
1114 0 : painter->setBrush(Qt::NoBrush);
1115 :
1116 0 : const qreal baselinePxY = toPxY(this, m_baselineN);
1117 :
1118 0 : for (int i = 0; i < pts.size() - 1; ++i) {
1119 0 : if (i < m_colorsUnderLine.size()) {
1120 : // fill area under line
1121 : const QVector<QPointF> areaUnderLine {
1122 0 : QPointF(pts[i].x(), baselinePxY), // bottom left
1123 0 : pts[i], // top left
1124 0 : pts[i + 1], // top right
1125 0 : QPointF(pts[i + 1].x(), baselinePxY), // bottom right
1126 0 : };
1127 0 : QPainterPath path;
1128 0 : path.addPolygon(QPolygonF(areaUnderLine));
1129 0 : painter->fillPath(path, m_colorsUnderLine.at(i));
1130 0 : }
1131 : // draw line
1132 0 : painter->drawLine(pts[i], pts[i + 1]);
1133 : }
1134 0 : }
1135 :
1136 0 : void PolylinePlot::paintPoint(QPainter* painter, const PolylinePointStyle* style, const QPointF& centre, bool useHoveredStyle) const
1137 : {
1138 0 : IF_ASSERT_FAILED(painter && style) {
1139 0 : return;
1140 : }
1141 :
1142 0 : const qreal centerRadius = useHoveredStyle ? style->centerRadiusHovered() : style->centerRadius();
1143 0 : const QColor centerColor = useHoveredStyle ? style->centerColorHovered() : style->centerColor();
1144 :
1145 0 : const qreal middleRingWidth = useHoveredStyle ? style->middleRingWidthHovered() : style->middleRingWidth();
1146 0 : const QColor middleRingColor = useHoveredStyle ? style->middleRingColorHovered() : style->middleRingColor();
1147 :
1148 0 : const qreal outlineWidth = useHoveredStyle ? style->outlineWidthHovered() : style->outlineWidth();
1149 0 : const QColor outlineColor = useHoveredStyle ? style->outlineColorHovered() : style->outlineColor();
1150 :
1151 0 : if (middleRingWidth > 0.0) {
1152 0 : const qreal middleRadius = centerRadius + middleRingWidth;
1153 0 : const qreal outerRadius = middleRadius + outlineWidth;
1154 :
1155 0 : painter->setPen(Qt::NoPen);
1156 :
1157 0 : painter->setBrush(outlineColor);
1158 0 : painter->drawEllipse(centre, outerRadius, outerRadius);
1159 :
1160 0 : painter->setBrush(middleRingColor);
1161 0 : painter->drawEllipse(centre, middleRadius, middleRadius);
1162 :
1163 0 : painter->setBrush(centerColor);
1164 0 : painter->drawEllipse(centre, centerRadius, centerRadius);
1165 0 : return;
1166 : }
1167 :
1168 0 : painter->setPen(Qt::NoPen);
1169 0 : painter->setBrush(centerColor);
1170 0 : painter->drawEllipse(centre, centerRadius, centerRadius);
1171 :
1172 0 : if (outlineWidth > 0.0) {
1173 0 : QPen pen(outlineColor);
1174 0 : pen.setWidthF(outlineWidth);
1175 0 : painter->setPen(pen);
1176 0 : painter->setBrush(Qt::NoBrush);
1177 0 : painter->drawEllipse(centre, centerRadius, centerRadius);
1178 0 : }
1179 : }
1180 :
1181 0 : void PolylinePlot::paint(QPainter* painter)
1182 : {
1183 0 : if (!painter) {
1184 0 : return;
1185 : }
1186 :
1187 0 : painter->setRenderHint(QPainter::Antialiasing, antialiasing());
1188 :
1189 : // draw background overlay
1190 0 : if (m_drawBackground) {
1191 0 : QColor overlay = uiConfiguration()->currentTheme().extra["black_color"].value<QColor>();
1192 0 : overlay.setAlphaF(0.25);
1193 :
1194 0 : painter->setPen(Qt::NoPen);
1195 0 : painter->setBrush(overlay);
1196 0 : painter->drawRect(boundingRect());
1197 : }
1198 :
1199 : // draw lines and fill area underneath
1200 0 : drawLinesAndFillUnder(painter);
1201 :
1202 : // draw points
1203 0 : IF_ASSERT_FAILED(m_standardPointStyle && m_ghostPointStyle && m_selectedPointStyle) {
1204 0 : return;
1205 : }
1206 :
1207 0 : const int n = m_pointsNVisible.size();
1208 0 : const int hoveredIndex = pointIndexAtPx(m_hoverPx);
1209 0 : for (int i = 0; i < n; ++i) {
1210 0 : QPointF pN = m_pointsNVisible[i];
1211 0 : const QPointF centre(toPxX(this, pN.x()), toPxY(this, pN.y()));
1212 :
1213 0 : const int domainIdx = (i < m_visibleToDomainIndex.size()) ? m_visibleToDomainIndex[i] : INVALID_POINT_IDX;
1214 0 : const bool isSelected = m_selectedPointsEnabled && domainIdx >= 0 && muse::contains(m_selectedPointsIndices, domainIdx);
1215 0 : const bool isHovered = !(m_pressed && isSelected) && domainIdx >= 0 && domainIdx == hoveredIndex;
1216 :
1217 0 : const PolylinePointStyle* style = isSelected ? m_selectedPointStyle : m_standardPointStyle;
1218 0 : paintPoint(painter, style, centre, /*useHoveredStyle*/ isHovered);
1219 : }
1220 :
1221 : // draw hover ghost point
1222 0 : if (m_ghostPointsEnabled && m_hoveredOnLine && m_pressedPointIndex < 0) {
1223 0 : QPointF hp = m_hoverGhostPx;
1224 :
1225 0 : if (m_pointsNVisible.size() < 2) {
1226 0 : const qreal yN = (m_pointsNVisible.size() == 1) ? m_pointsNVisible[0].y() : m_baselineN;
1227 0 : hp.setY(toPxY(this, yN));
1228 : }
1229 :
1230 0 : if (pointIndexAtPx(hp) < 0 && isNearLinePx(hp)) {
1231 0 : paintPoint(painter, m_ghostPointStyle, hp, /*useHoveredStyle*/ false);
1232 : }
1233 : }
1234 : }
1235 :
1236 0 : void PolylinePlot::hoverMoveEvent(QHoverEvent* e)
1237 : {
1238 : // NOTE: even if mouse is still, Qt produces hoverMoveEvents constantly
1239 0 : if (m_hoverPx == e->position()) {
1240 : // IMPORTANT: let through hover events (otherwise double-click
1241 : // clip selection may be broken)
1242 0 : e->ignore();
1243 0 : return;
1244 : }
1245 :
1246 0 : m_hoverPx = e->position();
1247 :
1248 0 : const bool nearPoint = (pointIndexAtPx(m_hoverPx) >= 0);
1249 :
1250 0 : bool nearLine = false;
1251 0 : if (m_ghostPointsEnabled) {
1252 0 : const auto proj = ghostPointToPolylinePx(m_hoverPx);
1253 0 : nearLine = (proj.distToSegment <= m_hitRadius);
1254 :
1255 0 : if (m_pointsNVisible.size() >= 2) {
1256 0 : m_hoverGhostPx = proj.point;
1257 : } else {
1258 0 : m_hoverGhostPx = m_hoverPx;
1259 : }
1260 : } else {
1261 : // nearLine is still required for updateCursor (m_hoveredOnLine) and to accept events, but
1262 : // if ghost points are disabled we can skip the m_hoverGhostPx logic...
1263 0 : nearLine = isNearLinePx(m_hoverPx);
1264 : }
1265 :
1266 0 : m_hoveredOnLine = (nearPoint || nearLine);
1267 0 : updateCursor();
1268 :
1269 0 : updateActivePoint();
1270 0 : update();
1271 :
1272 : // IMPORTANT: only consume hover when automation is actually interactive.
1273 : // Otherwise let underlying ClipItem hover area keep containsMouse=true,
1274 : // so itemHovered stays correct for selection/double-click logic.
1275 0 : if (nearPoint || nearLine || m_pressed || m_pressedPointIndex >= 0) {
1276 0 : e->accept();
1277 : } else {
1278 0 : e->ignore();
1279 : }
1280 : }
1281 :
1282 0 : void PolylinePlot::hoverLeaveEvent(QHoverEvent* e)
1283 : {
1284 : Q_UNUSED(e);
1285 0 : m_hoveredOnLine = false;
1286 0 : updateCursor();
1287 0 : updateActivePoint();
1288 0 : update();
1289 0 : }
1290 :
1291 0 : void PolylinePlot::mousePressEvent(QMouseEvent* e)
1292 : {
1293 0 : if (e->button() != Qt::LeftButton) {
1294 0 : e->ignore();
1295 0 : return;
1296 : }
1297 :
1298 0 : if (!m_selectedPointsIndices.empty()) {
1299 0 : m_selectedPointsIndices.clear();
1300 : }
1301 :
1302 0 : const int pointIndex = pointIndexAtPx(e->position());
1303 0 : const bool onPoint = pointIndex >= 0;
1304 0 : const bool onLine = isNearLinePx(e->position());
1305 :
1306 : // NOTE: allow clicks on the points and lines only
1307 0 : if (!onPoint && !onLine) {
1308 0 : e->ignore();
1309 0 : return;
1310 : }
1311 :
1312 0 : resetGestureState();
1313 :
1314 0 : e->accept();
1315 0 : updateCursor();
1316 :
1317 0 : m_pressed = true;
1318 0 : m_pressPx = e->position();
1319 :
1320 0 : if (onPoint) {
1321 0 : m_pressedOnPoint = true;
1322 0 : m_pressedPointIndex = pointIndex;
1323 0 : if (m_pressedPointIndex >= 0 && m_pressedPointIndex < m_points.size()) {
1324 0 : if (m_selectedPointsEnabled) {
1325 0 : m_selectedPointsIndices.insert(m_pressedPointIndex);
1326 : }
1327 0 : m_draggedPointDomain = m_points[m_pressedPointIndex];
1328 0 : m_hasDraggedPointDomain = true;
1329 : }
1330 0 : updateActivePoint();
1331 0 : return;
1332 : }
1333 :
1334 0 : if (onLine) {
1335 0 : m_pressedOnLine = true;
1336 0 : m_pressedPointIndex = PENDING_POINT_IDX;
1337 0 : m_pressedOnPoint = false;
1338 :
1339 0 : if (width() > 0 && height() > 0) {
1340 0 : const auto ghostPoint = ghostPointToPolylinePx(e->position());
1341 :
1342 0 : QPointF pN(clamp01(ghostPoint.point.x() / width()),
1343 0 : 1.0 - clamp01(ghostPoint.point.y() / height()));
1344 :
1345 0 : const QPointF pDomain = domainFromNormalized(pN);
1346 0 : emit pointAdded(pDomain.x(), pDomain.y(), /*completed*/ false);
1347 : }
1348 :
1349 0 : updateActivePoint();
1350 0 : return;
1351 : }
1352 : }
1353 :
1354 0 : void PolylinePlot::mouseMoveEvent(QMouseEvent* e)
1355 : {
1356 0 : if (!m_pressed) {
1357 0 : e->ignore();
1358 0 : return;
1359 : }
1360 :
1361 0 : e->accept();
1362 :
1363 0 : const QPointF pos = e->position();
1364 0 : if (!m_movedSincePress && (pos - m_pressPx).manhattanLength() > MOVE_THRESHOLD) {
1365 0 : m_movedSincePress = true;
1366 : }
1367 :
1368 : // drag point (2+ points only)
1369 0 : if (m_pressedPointIndex >= 0) {
1370 0 : if (width() <= 0 || height() <= 0) {
1371 0 : return;
1372 : }
1373 :
1374 0 : QPointF pN(pos.x() / width(), 1.0 - (pos.y() / height()));
1375 0 : pN = clamp01(pN);
1376 :
1377 0 : const QPointF pDomain = snapToNeighbor(pos.x(), domainFromNormalized(pN));
1378 :
1379 0 : m_draggedPointDomain = pDomain;
1380 0 : m_hasDraggedPointDomain = true;
1381 0 : emit pointMoved(m_pressedPointIndex, pDomain.x(), pDomain.y(), /*completed*/ false);
1382 0 : updateActivePoint();
1383 :
1384 0 : return;
1385 : }
1386 : }
1387 :
1388 0 : void PolylinePlot::mouseReleaseEvent(QMouseEvent* e)
1389 : {
1390 0 : if (e->button() != Qt::LeftButton || !m_pressed) {
1391 0 : e->ignore();
1392 0 : return;
1393 : }
1394 0 : e->accept();
1395 :
1396 0 : const bool isClick = !m_movedSincePress;
1397 0 : const QPointF rel = e->position();
1398 :
1399 : // commit point drag
1400 0 : if (!isClick && m_pressedPointIndex >= 0) {
1401 0 : QPointF pN(rel.x() / width(), 1.0 - (rel.y() / height()));
1402 0 : pN = clamp01(pN);
1403 :
1404 0 : const QPointF pDomain = snapToNeighbor(rel.x(), domainFromNormalized(pN));
1405 :
1406 0 : m_draggedPointDomain = pDomain;
1407 0 : m_hasDraggedPointDomain = true;
1408 0 : emit pointMoved(m_pressedPointIndex, pDomain.x(), pDomain.y(), /*completed*/ true);
1409 0 : emit interactionFinished();
1410 0 : resetGestureState();
1411 0 : return;
1412 : }
1413 :
1414 : // click-without-drag on line: finalize the point that was added during press
1415 0 : if (isClick && m_pressedOnLine && m_pressedPointIndex >= 0 && m_pressedPointIndex < m_points.size()) {
1416 0 : const QPointF pDomain = m_points[m_pressedPointIndex];
1417 0 : emit pointAdded(pDomain.x(), pDomain.y(), /*completed*/ true);
1418 : }
1419 :
1420 0 : emit interactionFinished();
1421 0 : resetGestureState();
1422 : }
1423 :
1424 0 : void PolylinePlot::mouseDoubleClickEvent(QMouseEvent* e)
1425 : {
1426 0 : if (e->button() != Qt::LeftButton) {
1427 0 : e->ignore();
1428 0 : return;
1429 : }
1430 :
1431 0 : const int idx = pointIndexAtPx(e->position());
1432 0 : if (idx < 0) {
1433 0 : e->ignore();
1434 0 : return;
1435 : }
1436 :
1437 0 : e->accept();
1438 :
1439 0 : emit pointRemoved(idx, /*completed*/ true);
1440 0 : emit interactionFinished();
1441 :
1442 0 : resetGestureState();
1443 : }
|