##// END OF EJS Templates
Fix a bug with event edition from visu after a graph is closed
trabillard -
r1378:171bcfdb6657
parent child
Show More
@@ -1,1053 +1,1057
1 1 #include "Visualization/VisualizationGraphWidget.h"
2 2 #include "Visualization/IVisualizationWidgetVisitor.h"
3 3 #include "Visualization/VisualizationCursorItem.h"
4 4 #include "Visualization/VisualizationDefs.h"
5 5 #include "Visualization/VisualizationGraphHelper.h"
6 6 #include "Visualization/VisualizationGraphRenderingDelegate.h"
7 7 #include "Visualization/VisualizationMultiZoneSelectionDialog.h"
8 8 #include "Visualization/VisualizationSelectionZoneItem.h"
9 9 #include "Visualization/VisualizationSelectionZoneManager.h"
10 10 #include "Visualization/VisualizationWidget.h"
11 11 #include "Visualization/VisualizationZoneWidget.h"
12 12 #include "ui_VisualizationGraphWidget.h"
13 13
14 14 #include <Actions/ActionsGuiController.h>
15 15 #include <Common/MimeTypesDef.h>
16 16 #include <Data/ArrayData.h>
17 17 #include <Data/IDataSeries.h>
18 18 #include <Data/SpectrogramSeries.h>
19 19 #include <DragAndDrop/DragDropGuiController.h>
20 20 #include <Settings/SqpSettingsDefs.h>
21 21 #include <SqpApplication.h>
22 22 #include <Time/TimeController.h>
23 23 #include <Variable/Variable.h>
24 24 #include <Variable/VariableController.h>
25 25
26 26 #include <unordered_map>
27 27
28 28 Q_LOGGING_CATEGORY(LOG_VisualizationGraphWidget, "VisualizationGraphWidget")
29 29
30 30 namespace {
31 31
32 32 /// Key pressed to enable drag&drop in all modes
33 33 const auto DRAG_DROP_MODIFIER = Qt::AltModifier;
34 34
35 35 /// Key pressed to enable zoom on horizontal axis
36 36 const auto HORIZONTAL_ZOOM_MODIFIER = Qt::ControlModifier;
37 37
38 38 /// Key pressed to enable zoom on vertical axis
39 39 const auto VERTICAL_ZOOM_MODIFIER = Qt::ShiftModifier;
40 40
41 41 /// Speed of a step of a wheel event for a pan, in percentage of the axis range
42 42 const auto PAN_SPEED = 5;
43 43
44 44 /// Key pressed to enable a calibration pan
45 45 const auto VERTICAL_PAN_MODIFIER = Qt::AltModifier;
46 46
47 47 /// Key pressed to enable multi selection of selection zones
48 48 const auto MULTI_ZONE_SELECTION_MODIFIER = Qt::ControlModifier;
49 49
50 50 /// Minimum size for the zoom box, in percentage of the axis range
51 51 const auto ZOOM_BOX_MIN_SIZE = 0.8;
52 52
53 53 /// Format of the dates appearing in the label of a cursor
54 54 const auto CURSOR_LABELS_DATETIME_FORMAT = QStringLiteral("yyyy/MM/dd\nhh:mm:ss:zzz");
55 55
56 56 } // namespace
57 57
58 58 struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate {
59 59
60 60 explicit VisualizationGraphWidgetPrivate(const QString &name)
61 61 : m_Name{name},
62 62 m_Flags{GraphFlag::EnableAll},
63 63 m_IsCalibration{false},
64 64 m_RenderingDelegate{nullptr}
65 65 {
66 66 }
67 67
68 68 void updateData(PlottablesMap &plottables, std::shared_ptr<Variable> variable,
69 69 const SqpRange &range)
70 70 {
71 71 VisualizationGraphHelper::updateData(plottables, variable, range);
72 72
73 73 // Prevents that data has changed to update rendering
74 74 m_RenderingDelegate->onPlotUpdated();
75 75 }
76 76
77 77 QString m_Name;
78 78 // 1 variable -> n qcpplot
79 79 std::map<std::shared_ptr<Variable>, PlottablesMap> m_VariableToPlotMultiMap;
80 80 GraphFlags m_Flags;
81 81 bool m_IsCalibration;
82 82 /// Delegate used to attach rendering features to the plot
83 83 std::unique_ptr<VisualizationGraphRenderingDelegate> m_RenderingDelegate;
84 84
85 85 QCPItemRect *m_DrawingZoomRect = nullptr;
86 86 QStack<QPair<QCPRange, QCPRange> > m_ZoomStack;
87 87
88 88 std::unique_ptr<VisualizationCursorItem> m_HorizontalCursor = nullptr;
89 89 std::unique_ptr<VisualizationCursorItem> m_VerticalCursor = nullptr;
90 90
91 91 VisualizationSelectionZoneItem *m_DrawingZone = nullptr;
92 92 VisualizationSelectionZoneItem *m_HoveredZone = nullptr;
93 93 QVector<VisualizationSelectionZoneItem *> m_SelectionZones;
94 94
95 95 bool m_HasMovedMouse = false; // Indicates if the mouse moved in a releaseMouse even
96 96
97 97 bool m_VariableAutoRangeOnInit = true;
98 98
99 99 void startDrawingRect(const QPoint &pos, QCustomPlot &plot)
100 100 {
101 101 removeDrawingRect(plot);
102 102
103 103 auto axisPos = posToAxisPos(pos, plot);
104 104
105 105 m_DrawingZoomRect = new QCPItemRect{&plot};
106 106 QPen p;
107 107 p.setWidth(2);
108 108 m_DrawingZoomRect->setPen(p);
109 109
110 110 m_DrawingZoomRect->topLeft->setCoords(axisPos);
111 111 m_DrawingZoomRect->bottomRight->setCoords(axisPos);
112 112 }
113 113
114 114 void removeDrawingRect(QCustomPlot &plot)
115 115 {
116 116 if (m_DrawingZoomRect) {
117 117 plot.removeItem(m_DrawingZoomRect); // the item is deleted by QCustomPlot
118 118 m_DrawingZoomRect = nullptr;
119 119 plot.replot(QCustomPlot::rpQueuedReplot);
120 120 }
121 121 }
122 122
123 123 void startDrawingZone(const QPoint &pos, VisualizationGraphWidget *graph)
124 124 {
125 125 endDrawingZone(graph);
126 126
127 127 auto axisPos = posToAxisPos(pos, graph->plot());
128 128
129 129 m_DrawingZone = new VisualizationSelectionZoneItem{&graph->plot()};
130 130 m_DrawingZone->setRange(axisPos.x(), axisPos.x());
131 131 m_DrawingZone->setEditionEnabled(false);
132 132 }
133 133
134 134 void endDrawingZone(VisualizationGraphWidget *graph)
135 135 {
136 136 if (m_DrawingZone) {
137 137 auto drawingZoneRange = m_DrawingZone->range();
138 138 if (qAbs(drawingZoneRange.m_TEnd - drawingZoneRange.m_TStart) > 0) {
139 139 m_DrawingZone->setEditionEnabled(true);
140 140 addSelectionZone(m_DrawingZone);
141 141 }
142 142 else {
143 143 graph->plot().removeItem(m_DrawingZone); // the item is deleted by QCustomPlot
144 144 }
145 145
146 146 graph->plot().replot(QCustomPlot::rpQueuedReplot);
147 147 m_DrawingZone = nullptr;
148 148 }
149 149 }
150 150
151 151 void setSelectionZonesEditionEnabled(bool value)
152 152 {
153 153 for (auto s : m_SelectionZones) {
154 154 s->setEditionEnabled(value);
155 155 }
156 156 }
157 157
158 158 void addSelectionZone(VisualizationSelectionZoneItem *zone) { m_SelectionZones << zone; }
159 159
160 160 VisualizationSelectionZoneItem *selectionZoneAt(const QPoint &pos,
161 161 const QCustomPlot &plot) const
162 162 {
163 163 VisualizationSelectionZoneItem *selectionZoneItemUnderCursor = nullptr;
164 164 auto minDistanceToZone = -1;
165 165 for (auto zone : m_SelectionZones) {
166 166 auto distanceToZone = zone->selectTest(pos, false);
167 167 if ((minDistanceToZone < 0 || distanceToZone <= minDistanceToZone)
168 168 && distanceToZone >= 0 && distanceToZone < plot.selectionTolerance()) {
169 169 selectionZoneItemUnderCursor = zone;
170 170 }
171 171 }
172 172
173 173 return selectionZoneItemUnderCursor;
174 174 }
175 175
176 176 QVector<VisualizationSelectionZoneItem *> selectionZonesAt(const QPoint &pos,
177 177 const QCustomPlot &plot) const
178 178 {
179 179 QVector<VisualizationSelectionZoneItem *> zones;
180 180 for (auto zone : m_SelectionZones) {
181 181 auto distanceToZone = zone->selectTest(pos, false);
182 182 if (distanceToZone >= 0 && distanceToZone < plot.selectionTolerance()) {
183 183 zones << zone;
184 184 }
185 185 }
186 186
187 187 return zones;
188 188 }
189 189
190 190 void moveSelectionZoneOnTop(VisualizationSelectionZoneItem *zone, QCustomPlot &plot)
191 191 {
192 192 if (!m_SelectionZones.isEmpty() && m_SelectionZones.last() != zone) {
193 193 zone->moveToTop();
194 194 m_SelectionZones.removeAll(zone);
195 195 m_SelectionZones.append(zone);
196 196 }
197 197 }
198 198
199 199 QPointF posToAxisPos(const QPoint &pos, QCustomPlot &plot) const
200 200 {
201 201 auto axisX = plot.axisRect()->axis(QCPAxis::atBottom);
202 202 auto axisY = plot.axisRect()->axis(QCPAxis::atLeft);
203 203 return QPointF{axisX->pixelToCoord(pos.x()), axisY->pixelToCoord(pos.y())};
204 204 }
205 205
206 206 bool pointIsInAxisRect(const QPointF &axisPoint, QCustomPlot &plot) const
207 207 {
208 208 auto axisX = plot.axisRect()->axis(QCPAxis::atBottom);
209 209 auto axisY = plot.axisRect()->axis(QCPAxis::atLeft);
210 210 return axisX->range().contains(axisPoint.x()) && axisY->range().contains(axisPoint.y());
211 211 }
212 212 };
213 213
214 214 VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget *parent)
215 215 : VisualizationDragWidget{parent},
216 216 ui{new Ui::VisualizationGraphWidget},
217 217 impl{spimpl::make_unique_impl<VisualizationGraphWidgetPrivate>(name)}
218 218 {
219 219 ui->setupUi(this);
220 220
221 221 // 'Close' options : widget is deleted when closed
222 222 setAttribute(Qt::WA_DeleteOnClose);
223 223
224 224 // Set qcpplot properties :
225 225 // - zoom is enabled
226 226 // - Mouse wheel on qcpplot is intercepted to determine the zoom orientation
227 227 ui->widget->setInteractions(QCP::iRangeZoom);
228 228 ui->widget->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
229 229
230 230 // The delegate must be initialized after the ui as it uses the plot
231 231 impl->m_RenderingDelegate = std::make_unique<VisualizationGraphRenderingDelegate>(*this);
232 232
233 233 // Init the cursors
234 234 impl->m_HorizontalCursor = std::make_unique<VisualizationCursorItem>(&plot());
235 235 impl->m_HorizontalCursor->setOrientation(Qt::Horizontal);
236 236 impl->m_VerticalCursor = std::make_unique<VisualizationCursorItem>(&plot());
237 237 impl->m_VerticalCursor->setOrientation(Qt::Vertical);
238 238
239 239 connect(ui->widget, &QCustomPlot::mousePress, this, &VisualizationGraphWidget::onMousePress);
240 240 connect(ui->widget, &QCustomPlot::mouseRelease, this,
241 241 &VisualizationGraphWidget::onMouseRelease);
242 242 connect(ui->widget, &QCustomPlot::mouseMove, this, &VisualizationGraphWidget::onMouseMove);
243 243 connect(ui->widget, &QCustomPlot::mouseWheel, this, &VisualizationGraphWidget::onMouseWheel);
244 244 connect(ui->widget, &QCustomPlot::mouseDoubleClick, this,
245 245 &VisualizationGraphWidget::onMouseDoubleClick);
246 246 connect(ui->widget->xAxis, static_cast<void (QCPAxis::*)(const QCPRange &, const QCPRange &)>(
247 247 &QCPAxis::rangeChanged),
248 248 this, &VisualizationGraphWidget::onRangeChanged, Qt::DirectConnection);
249 249
250 250 // Activates menu when right clicking on the graph
251 251 ui->widget->setContextMenuPolicy(Qt::CustomContextMenu);
252 252 connect(ui->widget, &QCustomPlot::customContextMenuRequested, this,
253 253 &VisualizationGraphWidget::onGraphMenuRequested);
254 254
255 255 connect(this, &VisualizationGraphWidget::requestDataLoading, &sqpApp->variableController(),
256 256 &VariableController::onRequestDataLoading);
257 257
258 258 connect(&sqpApp->variableController(), &VariableController::updateVarDisplaying, this,
259 259 &VisualizationGraphWidget::onUpdateVarDisplaying);
260 260
261 261 // Necessary for all platform since Qt::AA_EnableHighDpiScaling is enable.
262 262 plot().setPlottingHint(QCP::phFastPolylines, true);
263 263 }
264 264
265 265
266 266 VisualizationGraphWidget::~VisualizationGraphWidget()
267 267 {
268 268 delete ui;
269 269 }
270 270
271 271 VisualizationZoneWidget *VisualizationGraphWidget::parentZoneWidget() const noexcept
272 272 {
273 273 auto parent = parentWidget();
274 274 while (parent != nullptr && !qobject_cast<VisualizationZoneWidget *>(parent)) {
275 275 parent = parent->parentWidget();
276 276 }
277 277
278 278 return qobject_cast<VisualizationZoneWidget *>(parent);
279 279 }
280 280
281 281 VisualizationWidget *VisualizationGraphWidget::parentVisualizationWidget() const
282 282 {
283 283 auto parent = parentWidget();
284 284 while (parent != nullptr && !qobject_cast<VisualizationWidget *>(parent)) {
285 285 parent = parent->parentWidget();
286 286 }
287 287
288 288 return qobject_cast<VisualizationWidget *>(parent);
289 289 }
290 290
291 291 void VisualizationGraphWidget::setFlags(GraphFlags flags)
292 292 {
293 293 impl->m_Flags = std::move(flags);
294 294 }
295 295
296 296 void VisualizationGraphWidget::addVariable(std::shared_ptr<Variable> variable, SqpRange range)
297 297 {
298 298 /// Lambda used to set graph's units and range according to the variable passed in parameter
299 299 auto loadRange = [this](std::shared_ptr<Variable> variable, const SqpRange &range) {
300 300 impl->m_RenderingDelegate->setAxesUnits(*variable);
301 301
302 302 this->setFlags(GraphFlag::DisableAll);
303 303 setGraphRange(range);
304 304 this->setFlags(GraphFlag::EnableAll);
305 305 emit requestDataLoading({variable}, range, false);
306 306 };
307 307
308 308 connect(variable.get(), SIGNAL(updated()), this, SLOT(onDataCacheVariableUpdated()));
309 309
310 310 // Calls update of graph's range and units when the data of the variable have been initialized.
311 311 // Note: we use QueuedConnection here as the update event must be called in the UI thread
312 312 connect(variable.get(), &Variable::dataInitialized, this,
313 313 [ varW = std::weak_ptr<Variable>{variable}, range, loadRange, this ]() {
314 314 if (auto var = varW.lock()) {
315 315 // If the variable is the first added in the graph, we load its range
316 316 auto firstVariableInGraph = range == INVALID_RANGE;
317 317 auto loadedRange = graphRange();
318 318 if (impl->m_VariableAutoRangeOnInit) {
319 319 loadedRange = firstVariableInGraph ? var->range() : range;
320 320 }
321 321 loadRange(var, loadedRange);
322 322 setYRange(var);
323 323 }
324 324 },
325 325 Qt::QueuedConnection);
326 326
327 327 // Uses delegate to create the qcpplot components according to the variable
328 328 auto createdPlottables = VisualizationGraphHelper::create(variable, *ui->widget);
329 329
330 330 // Sets graph properties
331 331 impl->m_RenderingDelegate->setGraphProperties(*variable, createdPlottables);
332 332
333 333 impl->m_VariableToPlotMultiMap.insert({variable, std::move(createdPlottables)});
334 334
335 335 // If the variable already has its data loaded, load its units and its range in the graph
336 336 if (variable->dataSeries() != nullptr) {
337 337 loadRange(variable, range);
338 338 }
339 339
340 340 emit variableAdded(variable);
341 341 }
342 342
343 343 void VisualizationGraphWidget::removeVariable(std::shared_ptr<Variable> variable) noexcept
344 344 {
345 345 // Each component associated to the variable :
346 346 // - is removed from qcpplot (which deletes it)
347 347 // - is no longer referenced in the map
348 348 auto variableIt = impl->m_VariableToPlotMultiMap.find(variable);
349 349 if (variableIt != impl->m_VariableToPlotMultiMap.cend()) {
350 350 emit variableAboutToBeRemoved(variable);
351 351
352 352 auto &plottablesMap = variableIt->second;
353 353
354 354 for (auto plottableIt = plottablesMap.cbegin(), plottableEnd = plottablesMap.cend();
355 355 plottableIt != plottableEnd;) {
356 356 ui->widget->removePlottable(plottableIt->second);
357 357 plottableIt = plottablesMap.erase(plottableIt);
358 358 }
359 359
360 360 impl->m_VariableToPlotMultiMap.erase(variableIt);
361 361 }
362 362
363 363 // Updates graph
364 364 ui->widget->replot();
365 365 }
366 366
367 367 QList<std::shared_ptr<Variable> > VisualizationGraphWidget::variables() const
368 368 {
369 369 auto variables = QList<std::shared_ptr<Variable> >{};
370 370 for (auto it = std::cbegin(impl->m_VariableToPlotMultiMap);
371 371 it != std::cend(impl->m_VariableToPlotMultiMap); ++it) {
372 372 variables << it->first;
373 373 }
374 374
375 375 return variables;
376 376 }
377 377
378 378 void VisualizationGraphWidget::setYRange(std::shared_ptr<Variable> variable)
379 379 {
380 380 if (!variable) {
381 381 qCCritical(LOG_VisualizationGraphWidget()) << "Can't set y-axis range: variable is null";
382 382 return;
383 383 }
384 384
385 385 VisualizationGraphHelper::setYAxisRange(variable, *ui->widget);
386 386 }
387 387
388 388 SqpRange VisualizationGraphWidget::graphRange() const noexcept
389 389 {
390 390 auto graphRange = ui->widget->xAxis->range();
391 391 return SqpRange{graphRange.lower, graphRange.upper};
392 392 }
393 393
394 394 void VisualizationGraphWidget::setGraphRange(const SqpRange &range, bool calibration)
395 395 {
396 396 qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange START");
397 397
398 398 if (calibration) {
399 399 impl->m_IsCalibration = true;
400 400 }
401 401
402 402 ui->widget->xAxis->setRange(range.m_TStart, range.m_TEnd);
403 403 ui->widget->replot();
404 404
405 405 if (calibration) {
406 406 impl->m_IsCalibration = false;
407 407 }
408 408
409 409 qCDebug(LOG_VisualizationGraphWidget()) << tr("VisualizationGraphWidget::setGraphRange END");
410 410 }
411 411
412 412 void VisualizationGraphWidget::setAutoRangeOnVariableInitialization(bool value)
413 413 {
414 414 impl->m_VariableAutoRangeOnInit = value;
415 415 }
416 416
417 417 QVector<SqpRange> VisualizationGraphWidget::selectionZoneRanges() const
418 418 {
419 419 QVector<SqpRange> ranges;
420 420 for (auto zone : impl->m_SelectionZones) {
421 421 ranges << zone->range();
422 422 }
423 423
424 424 return ranges;
425 425 }
426 426
427 427 void VisualizationGraphWidget::addSelectionZones(const QVector<SqpRange> &ranges)
428 428 {
429 429 for (const auto &range : ranges) {
430 430 // note: ownership is transfered to QCustomPlot
431 431 auto zone = new VisualizationSelectionZoneItem(&plot());
432 432 zone->setRange(range.m_TStart, range.m_TEnd);
433 433 impl->addSelectionZone(zone);
434 434 }
435 435
436 436 plot().replot(QCustomPlot::rpQueuedReplot);
437 437 }
438 438
439 439 VisualizationSelectionZoneItem *VisualizationGraphWidget::addSelectionZone(const QString &name,
440 440 const SqpRange &range)
441 441 {
442 442 // note: ownership is transfered to QCustomPlot
443 443 auto zone = new VisualizationSelectionZoneItem(&plot());
444 444 zone->setName(name);
445 445 zone->setRange(range.m_TStart, range.m_TEnd);
446 446 impl->addSelectionZone(zone);
447 447
448 448 plot().replot(QCustomPlot::rpQueuedReplot);
449 449
450 450 return zone;
451 451 }
452 452
453 453 void VisualizationGraphWidget::removeSelectionZone(VisualizationSelectionZoneItem *selectionZone)
454 454 {
455 455 parentVisualizationWidget()->selectionZoneManager().setSelected(selectionZone, false);
456 456
457 457 if (impl->m_HoveredZone == selectionZone) {
458 458 impl->m_HoveredZone = nullptr;
459 459 setCursor(Qt::ArrowCursor);
460 460 }
461 461
462 462 impl->m_SelectionZones.removeAll(selectionZone);
463 463 plot().removeItem(selectionZone);
464 464 plot().replot(QCustomPlot::rpQueuedReplot);
465 465 }
466 466
467 467 void VisualizationGraphWidget::undoZoom()
468 468 {
469 469 auto zoom = impl->m_ZoomStack.pop();
470 470 auto axisX = plot().axisRect()->axis(QCPAxis::atBottom);
471 471 auto axisY = plot().axisRect()->axis(QCPAxis::atLeft);
472 472
473 473 axisX->setRange(zoom.first);
474 474 axisY->setRange(zoom.second);
475 475
476 476 plot().replot(QCustomPlot::rpQueuedReplot);
477 477 }
478 478
479 479 void VisualizationGraphWidget::accept(IVisualizationWidgetVisitor *visitor)
480 480 {
481 481 if (visitor) {
482 482 visitor->visit(this);
483 483 }
484 484 else {
485 485 qCCritical(LOG_VisualizationGraphWidget())
486 486 << tr("Can't visit widget : the visitor is null");
487 487 }
488 488 }
489 489
490 490 bool VisualizationGraphWidget::canDrop(const Variable &variable) const
491 491 {
492 492 auto isSpectrogram = [](const auto &variable) {
493 493 return std::dynamic_pointer_cast<SpectrogramSeries>(variable.dataSeries()) != nullptr;
494 494 };
495 495
496 496 // - A spectrogram series can't be dropped on graph with existing plottables
497 497 // - No data series can be dropped on graph with existing spectrogram series
498 498 return isSpectrogram(variable)
499 499 ? impl->m_VariableToPlotMultiMap.empty()
500 500 : std::none_of(
501 501 impl->m_VariableToPlotMultiMap.cbegin(), impl->m_VariableToPlotMultiMap.cend(),
502 502 [isSpectrogram](const auto &entry) { return isSpectrogram(*entry.first); });
503 503 }
504 504
505 505 bool VisualizationGraphWidget::contains(const Variable &variable) const
506 506 {
507 507 // Finds the variable among the keys of the map
508 508 auto variablePtr = &variable;
509 509 auto findVariable
510 510 = [variablePtr](const auto &entry) { return variablePtr == entry.first.get(); };
511 511
512 512 auto end = impl->m_VariableToPlotMultiMap.cend();
513 513 auto it = std::find_if(impl->m_VariableToPlotMultiMap.cbegin(), end, findVariable);
514 514 return it != end;
515 515 }
516 516
517 517 QString VisualizationGraphWidget::name() const
518 518 {
519 519 return impl->m_Name;
520 520 }
521 521
522 522 QMimeData *VisualizationGraphWidget::mimeData(const QPoint &position) const
523 523 {
524 524 auto mimeData = new QMimeData;
525 525
526 526 auto selectionZoneItemUnderCursor = impl->selectionZoneAt(position, plot());
527 527 if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones
528 528 && selectionZoneItemUnderCursor) {
529 529 mimeData->setData(MIME_TYPE_TIME_RANGE, TimeController::mimeDataForTimeRange(
530 530 selectionZoneItemUnderCursor->range()));
531 531 mimeData->setData(MIME_TYPE_SELECTION_ZONE, TimeController::mimeDataForTimeRange(
532 532 selectionZoneItemUnderCursor->range()));
533 533 }
534 534 else {
535 535 mimeData->setData(MIME_TYPE_GRAPH, QByteArray{});
536 536
537 537 auto timeRangeData = TimeController::mimeDataForTimeRange(graphRange());
538 538 mimeData->setData(MIME_TYPE_TIME_RANGE, timeRangeData);
539 539 }
540 540
541 541 return mimeData;
542 542 }
543 543
544 544 QPixmap VisualizationGraphWidget::customDragPixmap(const QPoint &dragPosition)
545 545 {
546 546 auto selectionZoneItemUnderCursor = impl->selectionZoneAt(dragPosition, plot());
547 547 if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones
548 548 && selectionZoneItemUnderCursor) {
549 549
550 550 auto zoneTopLeft = selectionZoneItemUnderCursor->topLeft->pixelPosition();
551 551 auto zoneBottomRight = selectionZoneItemUnderCursor->bottomRight->pixelPosition();
552 552
553 553 auto zoneSize = QSizeF{qAbs(zoneBottomRight.x() - zoneTopLeft.x()),
554 554 qAbs(zoneBottomRight.y() - zoneTopLeft.y())}
555 555 .toSize();
556 556
557 557 auto pixmap = QPixmap(zoneSize);
558 558 render(&pixmap, QPoint(), QRegion{QRect{zoneTopLeft.toPoint(), zoneSize}});
559 559
560 560 return pixmap;
561 561 }
562 562
563 563 return QPixmap();
564 564 }
565 565
566 566 bool VisualizationGraphWidget::isDragAllowed() const
567 567 {
568 568 return true;
569 569 }
570 570
571 571 void VisualizationGraphWidget::highlightForMerge(bool highlighted)
572 572 {
573 573 if (highlighted) {
574 574 plot().setBackground(QBrush(QColor("#BBD5EE")));
575 575 }
576 576 else {
577 577 plot().setBackground(QBrush(Qt::white));
578 578 }
579 579
580 580 plot().update();
581 581 }
582 582
583 583 void VisualizationGraphWidget::addVerticalCursor(double time)
584 584 {
585 585 impl->m_VerticalCursor->setPosition(time);
586 586 impl->m_VerticalCursor->setVisible(true);
587 587
588 588 auto text
589 589 = DateUtils::dateTime(time).toString(CURSOR_LABELS_DATETIME_FORMAT).replace(' ', '\n');
590 590 impl->m_VerticalCursor->setLabelText(text);
591 591 }
592 592
593 593 void VisualizationGraphWidget::addVerticalCursorAtViewportPosition(double position)
594 594 {
595 595 impl->m_VerticalCursor->setAbsolutePosition(position);
596 596 impl->m_VerticalCursor->setVisible(true);
597 597
598 598 auto axis = plot().axisRect()->axis(QCPAxis::atBottom);
599 599 auto text
600 600 = DateUtils::dateTime(axis->pixelToCoord(position)).toString(CURSOR_LABELS_DATETIME_FORMAT);
601 601 impl->m_VerticalCursor->setLabelText(text);
602 602 }
603 603
604 604 void VisualizationGraphWidget::removeVerticalCursor()
605 605 {
606 606 impl->m_VerticalCursor->setVisible(false);
607 607 plot().replot(QCustomPlot::rpQueuedReplot);
608 608 }
609 609
610 610 void VisualizationGraphWidget::addHorizontalCursor(double value)
611 611 {
612 612 impl->m_HorizontalCursor->setPosition(value);
613 613 impl->m_HorizontalCursor->setVisible(true);
614 614 impl->m_HorizontalCursor->setLabelText(QString::number(value));
615 615 }
616 616
617 617 void VisualizationGraphWidget::addHorizontalCursorAtViewportPosition(double position)
618 618 {
619 619 impl->m_HorizontalCursor->setAbsolutePosition(position);
620 620 impl->m_HorizontalCursor->setVisible(true);
621 621
622 622 auto axis = plot().axisRect()->axis(QCPAxis::atLeft);
623 623 impl->m_HorizontalCursor->setLabelText(QString::number(axis->pixelToCoord(position)));
624 624 }
625 625
626 626 void VisualizationGraphWidget::removeHorizontalCursor()
627 627 {
628 628 impl->m_HorizontalCursor->setVisible(false);
629 629 plot().replot(QCustomPlot::rpQueuedReplot);
630 630 }
631 631
632 632 void VisualizationGraphWidget::closeEvent(QCloseEvent *event)
633 633 {
634 634 Q_UNUSED(event);
635 635
636 for (auto i : impl->m_SelectionZones) {
637 parentVisualizationWidget()->selectionZoneManager().setSelected(i, false);
638 }
639
636 640 // Prevents that all variables will be removed from graph when it will be closed
637 641 for (auto &variableEntry : impl->m_VariableToPlotMultiMap) {
638 642 emit variableAboutToBeRemoved(variableEntry.first);
639 643 }
640 644 }
641 645
642 646 void VisualizationGraphWidget::enterEvent(QEvent *event)
643 647 {
644 648 Q_UNUSED(event);
645 649 impl->m_RenderingDelegate->showGraphOverlay(true);
646 650 }
647 651
648 652 void VisualizationGraphWidget::leaveEvent(QEvent *event)
649 653 {
650 654 Q_UNUSED(event);
651 655 impl->m_RenderingDelegate->showGraphOverlay(false);
652 656
653 657 if (auto parentZone = parentZoneWidget()) {
654 658 parentZone->notifyMouseLeaveGraph(this);
655 659 }
656 660 else {
657 661 qCWarning(LOG_VisualizationGraphWidget()) << "leaveEvent: No parent zone widget";
658 662 }
659 663
660 664 if (impl->m_HoveredZone) {
661 665 impl->m_HoveredZone->setHovered(false);
662 666 impl->m_HoveredZone = nullptr;
663 667 }
664 668 }
665 669
666 670 QCustomPlot &VisualizationGraphWidget::plot() const noexcept
667 671 {
668 672 return *ui->widget;
669 673 }
670 674
671 675 void VisualizationGraphWidget::onGraphMenuRequested(const QPoint &pos) noexcept
672 676 {
673 677 QMenu graphMenu{};
674 678
675 679 // Iterates on variables (unique keys)
676 680 for (auto it = impl->m_VariableToPlotMultiMap.cbegin(),
677 681 end = impl->m_VariableToPlotMultiMap.cend();
678 682 it != end; it = impl->m_VariableToPlotMultiMap.upper_bound(it->first)) {
679 683 // 'Remove variable' action
680 684 graphMenu.addAction(tr("Remove variable %1").arg(it->first->name()),
681 685 [ this, var = it->first ]() { removeVariable(var); });
682 686 }
683 687
684 688 if (!impl->m_ZoomStack.isEmpty()) {
685 689 if (!graphMenu.isEmpty()) {
686 690 graphMenu.addSeparator();
687 691 }
688 692
689 693 graphMenu.addAction(tr("Undo Zoom"), [this]() { undoZoom(); });
690 694 }
691 695
692 696 // Selection Zone Actions
693 697 auto selectionZoneItem = impl->selectionZoneAt(pos, plot());
694 698 if (selectionZoneItem) {
695 699 auto selectedItems = parentVisualizationWidget()->selectionZoneManager().selectedItems();
696 700 selectedItems.removeAll(selectionZoneItem);
697 701 selectedItems.prepend(selectionZoneItem); // Put the current selection zone first
698 702
699 703 auto zoneActions = sqpApp->actionsGuiController().selectionZoneActions();
700 704 if (!zoneActions.isEmpty() && !graphMenu.isEmpty()) {
701 705 graphMenu.addSeparator();
702 706 }
703 707
704 708 QHash<QString, QMenu *> subMenus;
705 709 QHash<QString, bool> subMenusEnabled;
706 710
707 711 for (auto zoneAction : zoneActions) {
708 712
709 713 auto isEnabled = zoneAction->isEnabled(selectedItems);
710 714
711 715 auto menu = &graphMenu;
712 716 for (auto subMenuName : zoneAction->subMenuList()) {
713 717 if (!subMenus.contains(subMenuName)) {
714 718 menu = menu->addMenu(subMenuName);
715 719 subMenus[subMenuName] = menu;
716 720 subMenusEnabled[subMenuName] = isEnabled;
717 721 }
718 722 else {
719 723 menu = subMenus.value(subMenuName);
720 724 if (isEnabled) {
721 725 // The sub menu is enabled if at least one of its actions is enabled
722 726 subMenusEnabled[subMenuName] = true;
723 727 }
724 728 }
725 729 }
726 730
727 731 auto action = menu->addAction(zoneAction->name());
728 732 action->setEnabled(isEnabled);
729 733 action->setShortcut(zoneAction->displayedShortcut());
730 734 QObject::connect(action, &QAction::triggered,
731 735 [zoneAction, selectedItems]() { zoneAction->execute(selectedItems); });
732 736 }
733 737
734 738 for (auto it = subMenus.cbegin(); it != subMenus.cend(); ++it) {
735 739 it.value()->setEnabled(subMenusEnabled[it.key()]);
736 740 }
737 741 }
738 742
739 743 if (!graphMenu.isEmpty()) {
740 744 graphMenu.exec(QCursor::pos());
741 745 }
742 746 }
743 747
744 748 void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange &t2)
745 749 {
746 750 qCDebug(LOG_VisualizationGraphWidget()) << tr("TORM: VisualizationGraphWidget::onRangeChanged")
747 751 << QThread::currentThread()->objectName() << "DoAcqui"
748 752 << impl->m_Flags.testFlag(GraphFlag::EnableAcquisition);
749 753
750 754 auto graphRange = SqpRange{t1.lower, t1.upper};
751 755 auto oldGraphRange = SqpRange{t2.lower, t2.upper};
752 756
753 757 if (impl->m_Flags.testFlag(GraphFlag::EnableAcquisition)) {
754 758 QVector<std::shared_ptr<Variable> > variableUnderGraphVector;
755 759
756 760 for (auto it = impl->m_VariableToPlotMultiMap.begin(),
757 761 end = impl->m_VariableToPlotMultiMap.end();
758 762 it != end; it = impl->m_VariableToPlotMultiMap.upper_bound(it->first)) {
759 763 variableUnderGraphVector.push_back(it->first);
760 764 }
761 765 emit requestDataLoading(std::move(variableUnderGraphVector), graphRange,
762 766 !impl->m_IsCalibration);
763 767 }
764 768
765 769 if (impl->m_Flags.testFlag(GraphFlag::EnableSynchronization) && !impl->m_IsCalibration) {
766 770 qCDebug(LOG_VisualizationGraphWidget())
767 771 << tr("TORM: VisualizationGraphWidget::Synchronize notify !!")
768 772 << QThread::currentThread()->objectName() << graphRange << oldGraphRange;
769 773 emit synchronize(graphRange, oldGraphRange);
770 774 }
771 775
772 776 auto pos = mapFromGlobal(QCursor::pos());
773 777 auto axisPos = impl->posToAxisPos(pos, plot());
774 778 if (auto parentZone = parentZoneWidget()) {
775 779 if (impl->pointIsInAxisRect(axisPos, plot())) {
776 780 parentZone->notifyMouseMoveInGraph(pos, axisPos, this);
777 781 }
778 782 else {
779 783 parentZone->notifyMouseLeaveGraph(this);
780 784 }
781 785 }
782 786 else {
783 787 qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget";
784 788 }
785 789
786 790 // Quits calibration
787 791 impl->m_IsCalibration = false;
788 792 }
789 793
790 794 void VisualizationGraphWidget::onMouseDoubleClick(QMouseEvent *event) noexcept
791 795 {
792 796 impl->m_RenderingDelegate->onMouseDoubleClick(event);
793 797 }
794 798
795 799 void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept
796 800 {
797 801 // Handles plot rendering when mouse is moving
798 802 impl->m_RenderingDelegate->onMouseMove(event);
799 803
800 804 auto axisPos = impl->posToAxisPos(event->pos(), plot());
801 805
802 806 // Zoom box and zone drawing
803 807 if (impl->m_DrawingZoomRect) {
804 808 impl->m_DrawingZoomRect->bottomRight->setCoords(axisPos);
805 809 }
806 810 else if (impl->m_DrawingZone) {
807 811 impl->m_DrawingZone->setEnd(axisPos.x());
808 812 }
809 813
810 814 // Cursor
811 815 if (auto parentZone = parentZoneWidget()) {
812 816 if (impl->pointIsInAxisRect(axisPos, plot())) {
813 817 parentZone->notifyMouseMoveInGraph(event->pos(), axisPos, this);
814 818 }
815 819 else {
816 820 parentZone->notifyMouseLeaveGraph(this);
817 821 }
818 822 }
819 823 else {
820 824 qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget";
821 825 }
822 826
823 827 // Search for the selection zone under the mouse
824 828 auto selectionZoneItemUnderCursor = impl->selectionZoneAt(event->pos(), plot());
825 829 if (selectionZoneItemUnderCursor && !impl->m_DrawingZone
826 830 && sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones) {
827 831
828 832 // Sets the appropriate cursor shape
829 833 auto cursorShape = selectionZoneItemUnderCursor->curshorShapeForPosition(event->pos());
830 834 setCursor(cursorShape);
831 835
832 836 // Manages the hovered zone
833 837 if (selectionZoneItemUnderCursor != impl->m_HoveredZone) {
834 838 if (impl->m_HoveredZone) {
835 839 impl->m_HoveredZone->setHovered(false);
836 840 }
837 841 selectionZoneItemUnderCursor->setHovered(true);
838 842 impl->m_HoveredZone = selectionZoneItemUnderCursor;
839 843 plot().replot(QCustomPlot::rpQueuedReplot);
840 844 }
841 845 }
842 846 else {
843 847 // There is no zone under the mouse or the interaction mode is not "selection zones"
844 848 if (impl->m_HoveredZone) {
845 849 impl->m_HoveredZone->setHovered(false);
846 850 impl->m_HoveredZone = nullptr;
847 851 }
848 852
849 853 setCursor(Qt::ArrowCursor);
850 854 }
851 855
852 856 impl->m_HasMovedMouse = true;
853 857 VisualizationDragWidget::mouseMoveEvent(event);
854 858 }
855 859
856 860 void VisualizationGraphWidget::onMouseWheel(QWheelEvent *event) noexcept
857 861 {
858 862 auto value = event->angleDelta().x() + event->angleDelta().y();
859 863 if (value != 0) {
860 864
861 865 auto direction = value > 0 ? 1.0 : -1.0;
862 866 auto isZoomX = event->modifiers().testFlag(HORIZONTAL_ZOOM_MODIFIER);
863 867 auto isZoomY = event->modifiers().testFlag(VERTICAL_ZOOM_MODIFIER);
864 868 impl->m_IsCalibration = event->modifiers().testFlag(VERTICAL_PAN_MODIFIER);
865 869
866 870 auto zoomOrientations = QFlags<Qt::Orientation>{};
867 871 zoomOrientations.setFlag(Qt::Horizontal, isZoomX);
868 872 zoomOrientations.setFlag(Qt::Vertical, isZoomY);
869 873
870 874 ui->widget->axisRect()->setRangeZoom(zoomOrientations);
871 875
872 876 if (!isZoomX && !isZoomY) {
873 877 auto axis = plot().axisRect()->axis(QCPAxis::atBottom);
874 878 auto diff = direction * (axis->range().size() * (PAN_SPEED / 100.0));
875 879
876 880 axis->setRange(axis->range() + diff);
877 881
878 882 if (plot().noAntialiasingOnDrag()) {
879 883 plot().setNotAntialiasedElements(QCP::aeAll);
880 884 }
881 885
882 886 plot().replot(QCustomPlot::rpQueuedReplot);
883 887 }
884 888 }
885 889 }
886 890
887 891 void VisualizationGraphWidget::onMousePress(QMouseEvent *event) noexcept
888 892 {
889 893 auto isDragDropClick = event->modifiers().testFlag(DRAG_DROP_MODIFIER);
890 894 auto isSelectionZoneMode
891 895 = sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones;
892 896 auto isLeftClick = event->buttons().testFlag(Qt::LeftButton);
893 897
894 898 if (!isDragDropClick && isLeftClick) {
895 899 if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::ZoomBox) {
896 900 // Starts a zoom box
897 901 impl->startDrawingRect(event->pos(), plot());
898 902 }
899 903 else if (isSelectionZoneMode && impl->m_DrawingZone == nullptr) {
900 904 // Starts a new selection zone
901 905 auto zoneAtPos = impl->selectionZoneAt(event->pos(), plot());
902 906 if (!zoneAtPos) {
903 907 impl->startDrawingZone(event->pos(), this);
904 908 }
905 909 }
906 910 }
907 911
908 912 // Allows mouse panning only in default mode
909 913 plot().setInteraction(QCP::iRangeDrag, sqpApp->plotsInteractionMode()
910 914 == SqpApplication::PlotsInteractionMode::None
911 915 && !isDragDropClick);
912 916
913 917 // Allows zone edition only in selection zone mode without drag&drop
914 918 impl->setSelectionZonesEditionEnabled(isSelectionZoneMode && !isDragDropClick);
915 919
916 920 // Selection / Deselection
917 921 if (isSelectionZoneMode) {
918 922 auto isMultiSelectionClick = event->modifiers().testFlag(MULTI_ZONE_SELECTION_MODIFIER);
919 923 auto selectionZoneItemUnderCursor = impl->selectionZoneAt(event->pos(), plot());
920 924
921 925
922 926 if (selectionZoneItemUnderCursor && !selectionZoneItemUnderCursor->selected()
923 927 && !isMultiSelectionClick) {
924 928 parentVisualizationWidget()->selectionZoneManager().select(
925 929 {selectionZoneItemUnderCursor});
926 930 }
927 931 else if (!selectionZoneItemUnderCursor && !isMultiSelectionClick && isLeftClick) {
928 932 parentVisualizationWidget()->selectionZoneManager().clearSelection();
929 933 }
930 934 else {
931 935 // No selection change
932 936 }
933 937
934 938 if (selectionZoneItemUnderCursor && isLeftClick) {
935 939 selectionZoneItemUnderCursor->setAssociatedEditedZones(
936 940 parentVisualizationWidget()->selectionZoneManager().selectedItems());
937 941 }
938 942 }
939 943
940 944
941 945 impl->m_HasMovedMouse = false;
942 946 VisualizationDragWidget::mousePressEvent(event);
943 947 }
944 948
945 949 void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept
946 950 {
947 951 if (impl->m_DrawingZoomRect) {
948 952
949 953 auto axisX = plot().axisRect()->axis(QCPAxis::atBottom);
950 954 auto axisY = plot().axisRect()->axis(QCPAxis::atLeft);
951 955
952 956 auto newAxisXRange = QCPRange{impl->m_DrawingZoomRect->topLeft->coords().x(),
953 957 impl->m_DrawingZoomRect->bottomRight->coords().x()};
954 958
955 959 auto newAxisYRange = QCPRange{impl->m_DrawingZoomRect->topLeft->coords().y(),
956 960 impl->m_DrawingZoomRect->bottomRight->coords().y()};
957 961
958 962 impl->removeDrawingRect(plot());
959 963
960 964 if (newAxisXRange.size() > axisX->range().size() * (ZOOM_BOX_MIN_SIZE / 100.0)
961 965 && newAxisYRange.size() > axisY->range().size() * (ZOOM_BOX_MIN_SIZE / 100.0)) {
962 966 impl->m_ZoomStack.push(qMakePair(axisX->range(), axisY->range()));
963 967 axisX->setRange(newAxisXRange);
964 968 axisY->setRange(newAxisYRange);
965 969
966 970 plot().replot(QCustomPlot::rpQueuedReplot);
967 971 }
968 972 }
969 973
970 974 impl->endDrawingZone(this);
971 975
972 976 // Selection / Deselection
973 977 auto isSelectionZoneMode
974 978 = sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones;
975 979 if (isSelectionZoneMode) {
976 980 auto isMultiSelectionClick = event->modifiers().testFlag(MULTI_ZONE_SELECTION_MODIFIER);
977 981 auto selectionZoneItemUnderCursor = impl->selectionZoneAt(event->pos(), plot());
978 982 if (selectionZoneItemUnderCursor && event->button() == Qt::LeftButton
979 983 && !impl->m_HasMovedMouse) {
980 984
981 985 auto zonesUnderCursor = impl->selectionZonesAt(event->pos(), plot());
982 986 if (zonesUnderCursor.count() > 1) {
983 987 // There are multiple zones under the mouse.
984 988 // Performs the selection with a selection dialog.
985 989 VisualizationMultiZoneSelectionDialog dialog{this};
986 990 dialog.setZones(zonesUnderCursor);
987 991 dialog.move(mapToGlobal(event->pos() - QPoint(dialog.width() / 2, 20)));
988 992 dialog.activateWindow();
989 993 dialog.raise();
990 994 if (dialog.exec() == QDialog::Accepted) {
991 995 auto selection = dialog.selectedZones();
992 996
993 997 if (!isMultiSelectionClick) {
994 998 parentVisualizationWidget()->selectionZoneManager().clearSelection();
995 999 }
996 1000
997 1001 for (auto it = selection.cbegin(); it != selection.cend(); ++it) {
998 1002 auto zone = it.key();
999 1003 auto isSelected = it.value();
1000 1004 parentVisualizationWidget()->selectionZoneManager().setSelected(zone,
1001 1005 isSelected);
1002 1006
1003 1007 if (isSelected) {
1004 1008 // Puts the zone on top of the stack so it can be moved or resized
1005 1009 impl->moveSelectionZoneOnTop(zone, plot());
1006 1010 }
1007 1011 }
1008 1012 }
1009 1013 }
1010 1014 else {
1011 1015 if (!isMultiSelectionClick) {
1012 1016 parentVisualizationWidget()->selectionZoneManager().select(
1013 1017 {selectionZoneItemUnderCursor});
1014 1018 impl->moveSelectionZoneOnTop(selectionZoneItemUnderCursor, plot());
1015 1019 }
1016 1020 else {
1017 1021 parentVisualizationWidget()->selectionZoneManager().setSelected(
1018 1022 selectionZoneItemUnderCursor, !selectionZoneItemUnderCursor->selected()
1019 1023 || event->button() == Qt::RightButton);
1020 1024 }
1021 1025 }
1022 1026 }
1023 1027 else {
1024 1028 // No selection change
1025 1029 }
1026 1030 }
1027 1031 }
1028 1032
1029 1033 void VisualizationGraphWidget::onDataCacheVariableUpdated()
1030 1034 {
1031 1035 auto graphRange = ui->widget->xAxis->range();
1032 1036 auto dateTime = SqpRange{graphRange.lower, graphRange.upper};
1033 1037
1034 1038 for (auto &variableEntry : impl->m_VariableToPlotMultiMap) {
1035 1039 auto variable = variableEntry.first;
1036 1040 qCDebug(LOG_VisualizationGraphWidget())
1037 1041 << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated S" << variable->range();
1038 1042 qCDebug(LOG_VisualizationGraphWidget())
1039 1043 << "TORM: VisualizationGraphWidget::onDataCacheVariableUpdated E" << dateTime;
1040 1044 if (dateTime.contains(variable->range()) || dateTime.intersect(variable->range())) {
1041 1045 impl->updateData(variableEntry.second, variable, variable->range());
1042 1046 }
1043 1047 }
1044 1048 }
1045 1049
1046 1050 void VisualizationGraphWidget::onUpdateVarDisplaying(std::shared_ptr<Variable> variable,
1047 1051 const SqpRange &range)
1048 1052 {
1049 1053 auto it = impl->m_VariableToPlotMultiMap.find(variable);
1050 1054 if (it != impl->m_VariableToPlotMultiMap.end()) {
1051 1055 impl->updateData(it->second, variable, range);
1052 1056 }
1053 1057 }
General Comments 0
You need to be logged in to leave comments. Login now