1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the demonstration applications of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include "arthurwidgets.h"
52#include "hoverpoints.h"
53
54#include <algorithm>
55
56#if QT_CONFIG(opengl)
57#include <QtOpenGL/QOpenGLWindow>
58#endif
59
60#define printf
61
62HoverPoints::HoverPoints(QWidget *widget, PointShape shape)
63 : QObject(widget)
64{
65 m_widget = widget;
66 widget->installEventFilter(this);
67 widget->setAttribute(Qt::WA_AcceptTouchEvents);
68
69 m_connectionType = CurveConnection;
70 m_sortType = NoSort;
71 m_shape = shape;
72 m_pointPen = QPen(QColor(255, 255, 255, 191), 1);
73 m_connectionPen = QPen(QColor(255, 255, 255, 127), 2);
74 m_pointBrush = QBrush(QColor(191, 191, 191, 127));
75 m_pointSize = QSize(11, 11);
76 m_currentIndex = -1;
77 m_editable = true;
78 m_enabled = true;
79
80 connect(this, &HoverPoints::pointsChanged,
81 m_widget, QOverload<>::of(&QWidget::update));
82}
83
84
85void HoverPoints::setEnabled(bool enabled)
86{
87 if (m_enabled != enabled) {
88 m_enabled = enabled;
89 m_widget->update();
90 }
91}
92
93
94bool HoverPoints::eventFilter(QObject *object, QEvent *event)
95{
96 if (object == m_widget && m_enabled) {
97 switch (event->type()) {
98
99 case QEvent::MouseButtonPress:
100 {
101 if (!m_fingerPointMapping.isEmpty())
102 return true;
103 QMouseEvent *me = (QMouseEvent *) event;
104
105 QPointF clickPos = me->position().toPoint();
106 int index = -1;
107 for (int i=0; i<m_points.size(); ++i) {
108 QPainterPath path;
109 if (m_shape == CircleShape)
110 path.addEllipse(pointBoundingRect(i));
111 else
112 path.addRect(pointBoundingRect(i));
113
114 if (path.contains(clickPos)) {
115 index = i;
116 break;
117 }
118 }
119
120 if (me->button() == Qt::LeftButton) {
121 if (index == -1) {
122 if (!m_editable)
123 return false;
124 int pos = 0;
125 // Insert sort for x or y
126 if (m_sortType == XSort) {
127 for (int i=0; i<m_points.size(); ++i)
128 if (m_points.at(i).x() > clickPos.x()) {
129 pos = i;
130 break;
131 }
132 } else if (m_sortType == YSort) {
133 for (int i=0; i<m_points.size(); ++i)
134 if (m_points.at(i).y() > clickPos.y()) {
135 pos = i;
136 break;
137 }
138 }
139
140 m_points.insert(pos, clickPos);
141 m_locks.insert(pos, 0);
142 m_currentIndex = pos;
143 firePointChange();
144 } else {
145 m_currentIndex = index;
146 }
147 return true;
148
149 } else if (me->button() == Qt::RightButton) {
150 if (index >= 0 && m_editable) {
151 if (m_locks[index] == 0) {
152 m_locks.remove(index);
153 m_points.remove(index);
154 }
155 firePointChange();
156 return true;
157 }
158 }
159
160 }
161 break;
162
163 case QEvent::MouseButtonRelease:
164 if (!m_fingerPointMapping.isEmpty())
165 return true;
166 m_currentIndex = -1;
167 break;
168
169 case QEvent::MouseMove:
170 if (!m_fingerPointMapping.isEmpty())
171 return true;
172 if (m_currentIndex >= 0)
173 movePoint(m_currentIndex, ((QMouseEvent *)event)->position().toPoint());
174 break;
175 case QEvent::TouchBegin:
176 case QEvent::TouchUpdate:
177 {
178 const QTouchEvent *const touchEvent = static_cast<const QTouchEvent*>(event);
179 const QList<QTouchEvent::TouchPoint> points = touchEvent->touchPoints();
180 const qreal pointSize = qMax(m_pointSize.width(), m_pointSize.height());
181 for (const QTouchEvent::TouchPoint &touchPoint : points) {
182 const int id = touchPoint.id();
183 switch (touchPoint.state()) {
184 case Qt::TouchPointPressed:
185 {
186 // find the point, move it
187 const auto mappedPoints = m_fingerPointMapping.values();
188 QSet<int> activePoints = QSet<int>(mappedPoints.begin(), mappedPoints.end());
189 int activePoint = -1;
190 qreal distance = -1;
191 const int pointsCount = m_points.size();
192 const int activePointCount = activePoints.size();
193 if (pointsCount == 2 && activePointCount == 1) { // only two points
194 activePoint = activePoints.contains(0) ? 1 : 0;
195 } else {
196 for (int i=0; i<pointsCount; ++i) {
197 if (activePoints.contains(i))
198 continue;
199
200 qreal d = QLineF(touchPoint.position(), m_points.at(i)).length();
201 if ((distance < 0 && d < 12 * pointSize) || d < distance) {
202 distance = d;
203 activePoint = i;
204 }
205
206 }
207 }
208 if (activePoint != -1) {
209 m_fingerPointMapping.insert(touchPoint.id(), activePoint);
210 movePoint(activePoint, touchPoint.position());
211 }
212 }
213 break;
214 case Qt::TouchPointReleased:
215 {
216 // move the point and release
217 QHash<int,int>::iterator it = m_fingerPointMapping.find(id);
218 movePoint(it.value(), touchPoint.position());
219 m_fingerPointMapping.erase(it);
220 }
221 break;
222 case Qt::TouchPointMoved:
223 {
224 // move the point
225 const int pointIdx = m_fingerPointMapping.value(id, -1);
226 if (pointIdx >= 0) // do we track this point?
227 movePoint(pointIdx, touchPoint.position());
228 }
229 break;
230 default:
231 break;
232 }
233 }
234 if (m_fingerPointMapping.isEmpty()) {
235 event->ignore();
236 return false;
237 } else {
238 return true;
239 }
240 }
241 break;
242 case QEvent::TouchEnd:
243 if (m_fingerPointMapping.isEmpty()) {
244 event->ignore();
245 return false;
246 }
247 return true;
248 break;
249
250 case QEvent::Resize:
251 {
252 QResizeEvent *e = (QResizeEvent *) event;
253 if (e->oldSize().width() == 0 || e->oldSize().height() == 0)
254 break;
255 qreal stretch_x = e->size().width() / qreal(e->oldSize().width());
256 qreal stretch_y = e->size().height() / qreal(e->oldSize().height());
257 for (int i=0; i<m_points.size(); ++i) {
258 QPointF p = m_points[i];
259 movePoint(i, QPointF(p.x() * stretch_x, p.y() * stretch_y), false);
260 }
261
262 firePointChange();
263 break;
264 }
265
266 case QEvent::Paint:
267 {
268 QWidget *that_widget = m_widget;
269 m_widget = nullptr;
270 QCoreApplication::sendEvent(object, event);
271 m_widget = that_widget;
272 paintPoints();
273 return true;
274 }
275 default:
276 break;
277 }
278 }
279
280 return false;
281}
282
283
284void HoverPoints::paintPoints()
285{
286 QPainter p;
287#if QT_CONFIG(opengl)
288 ArthurFrame *af = qobject_cast<ArthurFrame *>(m_widget);
289 if (af && af->usesOpenGL() && af->glWindow()->isValid()) {
290 af->glWindow()->makeCurrent();
291 p.begin(af->glWindow());
292 } else {
293 p.begin(m_widget);
294 }
295#else
296 p.begin(m_widget);
297#endif
298
299 p.setRenderHint(QPainter::Antialiasing);
300
301 if (m_connectionPen.style() != Qt::NoPen && m_connectionType != NoConnection) {
302 p.setPen(m_connectionPen);
303
304 if (m_connectionType == CurveConnection) {
305 QPainterPath path;
306 path.moveTo(m_points.at(0));
307 for (int i=1; i<m_points.size(); ++i) {
308 QPointF p1 = m_points.at(i-1);
309 QPointF p2 = m_points.at(i);
310 qreal distance = p2.x() - p1.x();
311
312 path.cubicTo(p1.x() + distance / 2, p1.y(),
313 p1.x() + distance / 2, p2.y(),
314 p2.x(), p2.y());
315 }
316 p.drawPath(path);
317 } else {
318 p.drawPolyline(m_points);
319 }
320 }
321
322 p.setPen(m_pointPen);
323 p.setBrush(m_pointBrush);
324
325 for (int i=0; i<m_points.size(); ++i) {
326 QRectF bounds = pointBoundingRect(i);
327 if (m_shape == CircleShape)
328 p.drawEllipse(bounds);
329 else
330 p.drawRect(bounds);
331 }
332}
333
334static QPointF bound_point(const QPointF &point, const QRectF &bounds, int lock)
335{
336 QPointF p = point;
337
338 qreal left = bounds.left();
339 qreal right = bounds.right();
340 qreal top = bounds.top();
341 qreal bottom = bounds.bottom();
342
343 if (p.x() < left || (lock & HoverPoints::LockToLeft)) p.setX(left);
344 else if (p.x() > right || (lock & HoverPoints::LockToRight)) p.setX(right);
345
346 if (p.y() < top || (lock & HoverPoints::LockToTop)) p.setY(top);
347 else if (p.y() > bottom || (lock & HoverPoints::LockToBottom)) p.setY(bottom);
348
349 return p;
350}
351
352void HoverPoints::setPoints(const QPolygonF &points)
353{
354 if (points.size() != m_points.size())
355 m_fingerPointMapping.clear();
356 m_points.clear();
357 for (int i=0; i<points.size(); ++i)
358 m_points << bound_point(points.at(i), boundingRect(), 0);
359
360 m_locks.clear();
361 if (m_points.size() > 0) {
362 m_locks.resize(m_points.size());
363
364 m_locks.fill(0);
365 }
366}
367
368
369void HoverPoints::movePoint(int index, const QPointF &point, bool emitUpdate)
370{
371 m_points[index] = bound_point(point, boundingRect(), m_locks.at(index));
372 if (emitUpdate)
373 firePointChange();
374}
375
376
377inline static bool x_less_than(const QPointF &p1, const QPointF &p2)
378{
379 return p1.x() < p2.x();
380}
381
382
383inline static bool y_less_than(const QPointF &p1, const QPointF &p2)
384{
385 return p1.y() < p2.y();
386}
387
388void HoverPoints::firePointChange()
389{
390// printf("HoverPoints::firePointChange(), current=%d\n", m_currentIndex);
391
392 if (m_sortType != NoSort) {
393
394 QPointF oldCurrent;
395 if (m_currentIndex != -1) {
396 oldCurrent = m_points[m_currentIndex];
397 }
398
399 if (m_sortType == XSort)
400 std::sort(m_points.begin(), m_points.end(), x_less_than);
401 else if (m_sortType == YSort)
402 std::sort(m_points.begin(), m_points.end(), y_less_than);
403
404 // Compensate for changed order...
405 if (m_currentIndex != -1) {
406 for (int i=0; i<m_points.size(); ++i) {
407 if (m_points[i] == oldCurrent) {
408 m_currentIndex = i;
409 break;
410 }
411 }
412 }
413
414// printf(" - firePointChange(), current=%d\n", m_currentIndex);
415
416 }
417
418// for (int i=0; i<m_points.size(); ++i) {
419// printf(" - point(%2d)=[%.2f, %.2f], lock=%d\n",
420// i, m_points.at(i).x(), m_points.at(i).y(), m_locks.at(i));
421// }
422
423 emit pointsChanged(m_points);
424}
425