Vuo 2.4.4
Loading...
Searching...
No Matches
VuoPortPopover.cc
Go to the documentation of this file.
1
10#include "VuoPortPopover.hh"
12#include "VuoCompilerGraph.hh"
16#include "VuoRendererFonts.hh"
17#include "VuoNodeClass.hh"
18#include "VuoEditor.hh"
20#include "VuoHeap.h"
21#include "VuoPopover.hh"
22#include "VuoComposition.hh"
23
24
25#ifdef __APPLE__
26#include <objc/objc-runtime.h>
27#include <objc/runtime.h>
28#endif
29
30const int VuoPortPopover::maxPopoverContentWidth = 512;
31const int VuoPortPopover::maxPopoverImageWidth = 256;
32const int VuoPortPopover::maxPopoverImageHeight = 256;
33const qreal VuoPortPopover::minTextUpdateInterval = 100; // The minimum time (in ms) between updates to the popover text, including automatic refreshes.
34const int VuoPortPopover::eventHistoryMaxSize = 20; // The maximum number of recent events to be used in calculating the event frequency.
35const int VuoPortPopover::noEventObserved = -1; // Indicates that we're watching for events, but haven't seen any yet.
36const int VuoPortPopover::noDisplayableEventTime = -2; // Indicates that we're not watching for or displaying event information.
37const string VuoPortPopover::noDataValueObserved = ""; // Indicates that we're watching for data values, but haven't seen any yet.
38const string VuoPortPopover::noDisplayableDataValue = "Unknown"; // Indicates that we're not watching for or displaying data values.
39
43VuoPortPopover::VuoPortPopover(VuoPort *port, VuoEditorComposition *composition, QWidget *parent) :
44 QTextBrowser(parent)
45{
46 // Text content
47 this->portID = composition->getIdentifierForStaticPort(port);
48 this->composition = composition;
49 this->cachedDataValue = noDisplayableDataValue;
50 this->timeOfLastEvent = noDisplayableEventTime;
51 this->timeOfLastUpdate = noDisplayableEventTime;
52 this->eventCount = 0;
53 this->droppedEventCount = 0;
54 this->isDetached = false;
55
56 setReadOnly(true);
57 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
58 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
59
60 setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); // not Qt::TextBrowserInteraction since it makes text selectable
61 setOpenExternalLinks(true);
62
63 imageResize = VuoImageResize_make();
64 VuoRetain(imageResize);
65
66 this->popoverTextQueue = dispatch_queue_create("org.vuo.editor.port", NULL);
67
68 this->refreshTextTimer = new QTimer(this);
69 this->refreshTextTimer->setObjectName("VuoPortPopover::refreshTextTimer");
70 refreshTextTimerFiredSinceLastReset = false;
71 connect(refreshTextTimer, &QTimer::timeout, this, &VuoPortPopover::updateTextAndResize);
72
73 setCompositionRunning(false, false);
74
76 {
77 VuoCompilerTriggerPort *triggerPort = static_cast<VuoCompilerTriggerPort *>(port->getCompiler());
78 size_t allPortsReached = composition->getBase()->getCompiler()->getCachedGraph()->getPublishedOutputPortsDownstream(triggerPort).size();
79 size_t triggerPortsReached = composition->getBase()->getCompiler()->getCachedGraph()->getPublishedOutputTriggersDownstream(triggerPort).size();
80 allEventsBlocked = (triggerPortsReached == 0 && allPortsReached > 0);
81 someEventsBlocked = (triggerPortsReached > 0 && allPortsReached > triggerPortsReached);
82
83 if (allEventsBlocked || someEventsBlocked)
84 {
85 QToolButton *helpButton = new QToolButton(this);
86 helpButton->setIcon(QIcon(":/Icons/question-circle.svg"));
87 helpButton->setStyleSheet("QToolButton { border: none; }");
88 helpButton->setCursor(Qt::PointingHandCursor);
89 connect(helpButton, &QToolButton::clicked, this, &VuoPortPopover::helpButtonClicked);
90
91 // Place the button in the bottom-right corner of the popover.
92 QVBoxLayout *innerLayout = new QVBoxLayout(this);
93 innerLayout->setContentsMargins(0, 0, 4, 4);
94 innerLayout->addWidget(helpButton, 0, Qt::AlignBottom | Qt::AlignRight);
95 this->setLayout(innerLayout);
96 }
97 }
98 else
99 allEventsBlocked = someEventsBlocked = false;
100
101 // Style
102 VuoEditor *editor = (VuoEditor *)qApp;
103 connect(editor, &VuoEditor::darkInterfaceToggled, this, &VuoPortPopover::updateStyle);
105 updateStyle();
107
108 this->dragInProgress = false;
109}
110
115{
116 VuoRelease(imageResize);
117 dispatch_release(popoverTextQueue);
118}
119
127{
128 dispatch_sync(popoverTextQueue, ^{
129 mostRecentImage.clear();
130 this->cachedDataValue = value.toUtf8().constData();
131 this->updateTextThreadUnsafe();
132 resetRefreshTextInterval();
133 });
134}
135
147void VuoPortPopover::updateLastEventTimeAndDataValue(bool event, bool data, QString value)
148{
149 dispatch_sync(popoverTextQueue, ^{
150 qint64 timeBefore = this->timeOfLastUpdate;
151 qint64 timeNow = QDateTime::currentMSecsSinceEpoch();
152 this->timeOfLastUpdate = timeNow;
153
154 if (event)
155 {
156 this->timeOfLastEvent = timeNow;
157 this->eventHistory.enqueue(timeNow);
158 while (this->eventHistory.size() > eventHistoryMaxSize)
159 this->eventHistory.dequeue();
160
161 ++eventCount;
162 }
163
164 if (data)
165 {
166 this->cachedDataValue = value.toUtf8().constData();
167 }
168
169 if (timeBefore + minTextUpdateInterval < timeNow)
170 {
171 this->updateTextThreadUnsafe(true);
172 resetRefreshTextInterval();
173 }
174 });
175}
176
181{
182 dispatch_sync(popoverTextQueue, ^{
183 ++droppedEventCount;
184
185 this->updateTextThreadUnsafe(true);
186 resetRefreshTextInterval();
187 });
188}
189
194void VuoPortPopover::setCompositionRunning(bool running, bool resetDataValue)
195{
196 dispatch_sync(popoverTextQueue, ^{
197 this->compositionRunning = running;
198
199 if (running)
200 {
201 if (resetDataValue)
202 this->cachedDataValue = noDataValueObserved;
203
204 this->timeOfLastEvent = noEventObserved;
205 this->timeOfLastUpdate = noEventObserved;
206
207 while (!eventHistory.isEmpty())
208 eventHistory.dequeue();
209
210 this->eventCount = 0;
211 this->droppedEventCount = 0;
212
213 // Refresh the popover text periodically to keep the reported time
214 // since the last event up-to-date.
215 refreshTextTimer->setInterval(minTextUpdateInterval);
216 refreshTextTimer->start();
217 }
218
219 else
220 {
221 refreshTextTimer->stop();
222 }
223
224 // @todo https://b33p.net/kosada/node/6755 : Possibly need to re-size to accommodate longer text.
225 updateTextThreadUnsafe();
226 });
227}
228
236{
237 dispatch_sync(popoverTextQueue, ^{
238 updateTextAndResizeThreadUnsafe();
239 refreshTextTimerFiredSinceLastReset = true;
240 });
241}
242
247QString VuoPortPopover::generateImageCode()
248{
249 VuoPort *port = composition->getPortWithStaticIdentifier(portID);
250 VuoType *type = static_cast<VuoCompilerPortClass *>(port->getClass()->getCompiler())->getDataVuoType();
251 if (!type || (type->getModuleKey() != "VuoImage" && type->getModuleKey() != "VuoVideoFrame"))
252 return "";
253
254 if (! compositionRunning)
255 return mostRecentImage;
256
257 json_object *imageJson = composition->getPortValueInRunningComposition(port);
258 if (!imageJson)
259 return mostRecentImage;
260
261 VuoImage image = NULL;
262 json_object *o;
263 if (json_object_object_get_ex(imageJson, "ioSurface", &o)) // VuoImage_getInterprocessJson returns an object with an 'ioSurface' key.
264 image = VuoImage_makeFromJson(imageJson);
265 else if (json_object_object_get_ex(imageJson, "image", &o)) // VuoVideoFrame_getInterprocessJson returns an object with an 'image' key, which contains an 'ioSurface' key.
266 image = VuoImage_makeFromJson(o);
267 else
268 VUserLog("Warning: Unknown interprocess JSON format.");
269 json_object_put(imageJson);
270 if (!image)
271 return mostRecentImage;
272
273 VuoRetain(image);
274
275 int maxImageWidth = maxPopoverImageWidth;
276 int maxImageHeight = maxPopoverImageHeight;
277
278 int devicePixelRatio = window()->windowHandle()->screen()->devicePixelRatio();
279 maxImageWidth *= devicePixelRatio;
280 maxImageHeight *= devicePixelRatio;
281
282 if (image->pixelsWide > maxImageWidth
283 || image->pixelsHigh > maxImageHeight)
284 {
285 VuoImage resizedImage = VuoImageResize_resize(image, imageResize, VuoSizingMode_Proportional, maxImageWidth, maxImageHeight);
286 if (resizedImage)
287 {
288 VuoRetain(resizedImage);
289 VuoRelease(image);
290 image = resizedImage;
291 }
292 }
293
294 const unsigned char *buffer = VuoImage_getBuffer(image, GL_RGBA);
295 unsigned long int bufferLength = image->pixelsWide * image->pixelsHigh * 4;
296 unsigned char *bufferClamped = (unsigned char *)malloc(bufferLength);
297
298 // The VuoImage might contain out-of-range premultiplied color values.
299 // Clamp them, since QImage might wrap (especially when blending onto
300 // the white part of the checkerboard background), which looks bad.
301 for (unsigned long int i = 0; i < bufferLength; i += 4)
302 {
303 unsigned char a = buffer[i+3];
304 bufferClamped[i ] = MIN(a,buffer[i ]);
305 bufferClamped[i+1] = MIN(a,buffer[i+1]);
306 bufferClamped[i+2] = MIN(a,buffer[i+2]);
307 bufferClamped[i+3] = a;
308 }
309
310 QImage qi(bufferClamped, image->pixelsWide, image->pixelsHigh, QImage::Format_RGBA8888_Premultiplied, free, bufferClamped);
311 VuoRelease(image);
312 qi.setDevicePixelRatio(devicePixelRatio);
313 qi = qi.mirrored(false, true);
314
315 document()->addResource(QTextDocument::ImageResource, QUrl("vuo-port-popover://foreground.png"), QVariant(qi));
316 mostRecentImage = "<div><img style='background-image: url(vuo-port-popover://background.png);' src='vuo-port-popover://foreground.png' /></div>";
317 return mostRecentImage;
318}
319
326void VuoPortPopover::updateTextAndResizeThreadUnsafe()
327{
328 // Exponentially increase the timeout interval of the popover
329 // refresh timer as time passes since the most recent event or data.
330 double secondsSinceLastUpdate = (QDateTime::currentMSecsSinceEpoch() - this->timeOfLastUpdate)/1000.;
331 int updatedTextRefreshInterval = (secondsSinceLastUpdate <= 1? 0.1 :
332 (secondsSinceLastUpdate < 20? 1 :
333 (secondsSinceLastUpdate < 40? 2 :
334 (secondsSinceLastUpdate < 120? 6 :
335 (secondsSinceLastUpdate < 180? 9 :
336 (secondsSinceLastUpdate < 600? 30 :
337 INT_MAX/1000))))))*1000;
338
339 if (updatedTextRefreshInterval != refreshTextTimer->interval())
340 this->refreshTextTimer->setInterval(updatedTextRefreshInterval);
341
342 setHtml(generatePortPopoverText());
343
344 // Calculate the size needed to display the text without wrapping,
345 // limited to about half the width of a typical screen.
346 document()->setPageSize(QSizeF(maxPopoverContentWidth, 10000));
347 document()->setPageSize(QSizeF(ceil(document()->idealWidth()), 10000));
348 QSize documentSize = document()->documentLayout()->documentSize().toSize();
349
350 // Never shrink the popover.
351 documentSize = documentSize.expandedTo(size());
352
353 this->setFixedSize(documentSize);
354}
355
361void VuoPortPopover::updateTextThreadUnsafe(bool includeEventIndicator)
362{
363 this->setText(generatePortPopoverText(includeEventIndicator));
364}
365
371void VuoPortPopover::resetRefreshTextInterval()
372{
373 if (refreshTextTimerFiredSinceLastReset)
374 {
375 refreshTextTimerFiredSinceLastReset = false;
376 this->refreshTextTimer->setInterval(minTextUpdateInterval);
377 }
378
379 QThread::yieldCurrentThread();
380}
381
389QString VuoPortPopover::generatePortPopoverText(bool includeEventIndicator)
390{
391 VuoEditor *editor = (VuoEditor *)qApp;
392 VuoPort *port = composition->getPortWithStaticIdentifier(portID);
393 if (!port)
394 return QString();
395
396 json_object *details = nullptr;
398 if (portClass)
399 details = (portClass->getDataClass()? portClass->getDataClass()->getDetails() : nullptr);
400
401 bool isCodeEditor = false;
402 json_object *codeEditorValue = nullptr;
403 if (details && json_object_object_get_ex(details, "isCodeEditor", &codeEditorValue))
404 isCodeEditor = json_object_get_boolean(codeEditorValue);
405
406 bool isDark = editor->isInterfaceDark();
407 QString textColor = isDark ? "#cacaca" : "#000000";
408 QString subtleTextColor = isDark ? "#808080" : "#808080";
409 QString subtlerTextColor = isDark ? "#505050" : "#bbbbbb";
410 QString codeTextColor = isDark ? "#383838" : "#ececec";
411
412 qint64 timeOfLastEventSnapshot = this->timeOfLastEvent;
413 QQueue<qint64> eventHistorySnapshot = this->eventHistory;
414 string cachedDataValueSnapshot = this->cachedDataValue;
415 bool compositionRunningSnapshot = this->compositionRunning;
416
417 string portName = port->getRenderer()->getPortNameToRenderWhenDisplayed();
418 VuoNode *parentNode = port->getRenderer()->getUnderlyingParentNode()->getBase();
419 string nodeName = parentNode->getTitle();
420 string nodeClass = parentNode->getNodeClass()->getClassName();
421 VuoType *dataType = port->getRenderer()->getDataType();
422 QString dataTypeDescription = composition->formatTypeNameForDisplay(dataType);
423 bool displayValue = (dataType && (cachedDataValueSnapshot != noDisplayableDataValue) && !isCodeEditor);
424
425 //: Appears in port popovers.
426 //: Refers to whether any events passed through this port in a running composition while the popover was open.
427 const string noEvent = ("(" + tr("none observed") + ")").toStdString();
428
429 //: Appears in port popovers.
430 //: Refers to how many events passed through this port in a running composition while the popover was open.
431 const string unknownFrequency = "(" + tr("%1 observed", "", eventCount).arg(eventCount).toStdString() + ")";
432
433 QString lastEventTime;
434 string lastEventFrequency = "";
435
436 bool displayLastEventTime = compositionRunningSnapshot;
437 bool displayEventFrequency = (timeOfLastEventSnapshot != noDisplayableEventTime);
438
439 // Time since last event
440 if (displayLastEventTime && (timeOfLastEventSnapshot != noEventObserved))
441 {
442 qint64 timeNow = QDateTime::currentMSecsSinceEpoch();
443 double secondsSinceLastEvent = (timeNow - timeOfLastEventSnapshot)/1000.;
444 int roundedSecondsSinceLastEvent = (int)(secondsSinceLastEvent + 0.5);
445
446 lastEventTime = secondsSinceLastEvent <= 1
447 ? tr("just now")
448 : (secondsSinceLastEvent < 20
449 ? tr("%1 second(s) ago", "", roundedSecondsSinceLastEvent).arg(roundedSecondsSinceLastEvent)
450 : (secondsSinceLastEvent < 40
451 ? tr("about half a minute ago")
452 : (secondsSinceLastEvent < 120
453 ? tr("about a minute ago")
454 : (secondsSinceLastEvent < 180
455 ? tr("a couple minutes ago")
456 : (secondsSinceLastEvent < 600
457 ? tr("several minutes ago")
458 : tr("more than 10 minutes ago"))))));
459 }
460
461 // Event frequency
462 if (displayEventFrequency)
463 {
464 if (timeOfLastEventSnapshot == noEventObserved)
465 lastEventFrequency = noEvent;
466
467 else if (eventHistorySnapshot.size() > 2)
468 {
469 double recentEventIntervalMean = getEventIntervalMean(eventHistorySnapshot);
470 double recentEventIntervalStdDev = getEventIntervalStdDev(eventHistorySnapshot);
471
472 // Calculate the coefficient of variation (CV) in time intervals between events.
473 // Don't display frequencies for events whose intervals are fluctuating wildly.
474 double recentEventIntervalCV = recentEventIntervalStdDev/recentEventIntervalMean;
475 const double maxCVForFrequencyDisplay = 2.0;
476 if (recentEventIntervalCV <= maxCVForFrequencyDisplay)
477 {
478 double recentEventFrequency = 1./recentEventIntervalMean;
479
480 // Use an appropriate time unit.
481 //: Appears in port popovers.
482 QString unit = tr("per second");
483 double roundedEventFrequency = ((int)(10*recentEventFrequency+0.5))/10.;
484
485 if (roundedEventFrequency == 0)
486 {
487 //: Appears in port popovers.
488 unit = tr("per minute");
489 roundedEventFrequency = ((int)(10*60*recentEventFrequency+0.5))/10.;
490 }
491
492 if (roundedEventFrequency == 0)
493 {
494 //: Appears in port popovers.
495 unit = tr("per hour");
496 roundedEventFrequency = ((int)(10*60*60*recentEventFrequency+0.5))/10.;
497 }
498
499 lastEventFrequency += "(~";
500 lastEventFrequency += QLocale::system().toString(roundedEventFrequency, 'f', 1).toStdString();
501 lastEventFrequency += " ";
502 lastEventFrequency += unit.toStdString();
503 lastEventFrequency += ")";
504 }
505
506 else
507 lastEventFrequency = unknownFrequency;
508 }
509
510 else
511 lastEventFrequency = unknownFrequency;
512 }
513
514 // Only indicate unknown frequencies after the composition has stopped.
515 if ((lastEventFrequency == unknownFrequency) && compositionRunningSnapshot)
516 lastEventFrequency = "";
517
518 if (!lastEventTime.isEmpty() && !lastEventFrequency.empty())
519 lastEventFrequency = " " + lastEventFrequency;
520
521 if (includeEventIndicator && compositionRunningSnapshot)
522 lastEventFrequency += " ยท";
523 else
524 // Leave room for the event indicator when it's hidden,
525 // so the popover doesn't have to resize when it appears.
526 lastEventFrequency += " &nbsp;";
527
528 // Event throttling
529 QString eventThrottlingDescription;
531 {
533 //: Appears in port popovers.
534 eventThrottlingDescription = tr("enqueue events");
535 else if (! compositionRunningSnapshot)
536 //: Appears in port popovers.
537 eventThrottlingDescription = tr("drop events");
538 else
539 {
540 unsigned int totalEventCount = droppedEventCount + eventCount;
541 unsigned int percentDropped = round( (float)droppedEventCount / (float)totalEventCount * 100 );
542 //: Appears in port popovers.
543 //: Refers to the number and percentage of events dropped in the running composition while the port popover was open.
544 //: Example: "42 events dropped (3%)"
545 eventThrottlingDescription = tr("%1 event(s) dropped (%2%)", "", droppedEventCount).arg(droppedEventCount).arg(percentDropped);
546 }
547 }
548
549 // Help when trigger's event stream overlaps published input's event stream in subcomposition
550 QString formattedTriggerBlockedHelp;
551 if (allEventsBlocked || someEventsBlocked)
552 formattedTriggerBlockedHelp = "<p>"
553 + (allEventsBlocked
554 ? tr("Events from this trigger are blocked from exiting this composition.")
555 : tr("Some events from this trigger are blocked from exiting this composition."))
556 // Leave room for questionmark-circle.svg.
557 + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</p>";
558
559 // Formatting
560 QString formattedPortName = (isDetached?
561 QString("<b><font size=+2 color='%3'>%1: %2</font></b>").arg(nodeName.c_str(), portName.c_str(), textColor) :
562 QString("<b><font size=+2 color='%2'>%1</font></b>").arg(portName.c_str(), textColor));
563 QString formattedDataTypeDescription = "<tr><th>" + tr("Data type") + ": </th><td>" + dataTypeDescription +"</td></tr>";
564 QString formattedThrottlingDescription = "<tr><th>" + tr("Event throttling") + ": </th><td>" + eventThrottlingDescription + "</td></tr>";
565 QString formattedLastEventLine = "<tr><th>" + tr("Last event") + ": </th>"
566 + "<td" + (lastEventFrequency == noEvent ? " class='subtler'" : "") + ">"
567 + lastEventTime + QString::fromStdString(lastEventFrequency) + "</td></tr>";
568
569 // Special formatting for named enum types
570 {
571 json_object *menuItemsValue = NULL;
572 if (details && dataType && dataType->getModuleKey() == "VuoInteger" && json_object_object_get_ex(details, "menuItems", &menuItemsValue))
573 {
574 int len = json_object_array_length(menuItemsValue);
575 for (int i = 0; i < len; ++i)
576 {
577 json_object *menuItem = json_object_array_get_idx(menuItemsValue, i);
578 if (json_object_is_type(menuItem, json_type_object))
579 {
580 json_object *value = NULL;
581 if (json_object_object_get_ex(menuItem, "value", &value))
582 if (json_object_is_type(value, json_type_int ) && atol(cachedDataValueSnapshot.c_str()) == json_object_get_int64(value))
583 {
584 json_object *name = NULL;
585 if (json_object_object_get_ex(menuItem, "name", &name))
586 {
587 cachedDataValueSnapshot += ": ";
588 cachedDataValueSnapshot += json_object_get_string(name);
589 break;
590 }
591 }
592 }
593 }
594 }
595 }
596
597 //: Appears in port popovers.
598 //: Refers to the port's current value.
599 QString formattedDataValue = "<tr><th>" + tr("Value")
600 + ": </th><td>" + generateImageCode() + QString::fromStdString(cachedDataValueSnapshot) + "</td></tr>";
601
602 QString popoverText = VUO_QSTRINGIFY(
603 <style>
604 code {
605 font-family: 'Monaco';
606 font-size: 12px;
607 background-color: %1;
608 white-space: pre-wrap;
609 }
610 table {
611 font-size: 13px;
612 }
613 table th {
614 font-weight: normal;
615 text-align: right;
616 color: %2;
617 white-space: pre; // Don't wrap table headers.
618 }
619 table td {
620 font-weight: bold;
621 color: %2;
622 }
623 .subtler {
624 color: %3;
625 }
626 p {
627 font-size: 13px;
628 font-weight: normal;
629 color: %2;
630 }
631 </style>)
632 .arg(codeTextColor)
633 .arg(subtleTextColor)
634 .arg(subtlerTextColor);
635 popoverText.append(formattedPortName);
636 popoverText.append("<table>");
637 popoverText.append(formattedDataTypeDescription);
638
640 popoverText.append(formattedThrottlingDescription);
641
642 if ((displayLastEventTime || displayEventFrequency) && (!lastEventTime.isEmpty() || !lastEventFrequency.empty()))
643 popoverText.append(formattedLastEventLine);
644
645 if (displayValue)
646 popoverText.append(formattedDataValue);
647
648 popoverText.append("</table>");
649
650 popoverText.append(formattedTriggerBlockedHelp);
651
652 return popoverText;
653}
654
659{
660 return isDetached;
661}
662
667{
668 this->isDetached = true;
669
670 // The following will no longer be necessary once the popover is never
671 // assigned a parent to begin with; see https://b33p.net/node/5211 .
672 if (parentWidget())
673 {
674 QPoint newGlobalPos = parentWidget()->mapToGlobal(pos());
675 setParent(NULL);
676 move(newGlobalPos);
677 }
678
679 updateStyle();
681 show();
682
683 emit popoverDetachedFromPort(portID);
684}
685
686
690void VuoPortPopover::updateStyle()
691{
692 setAlignment(Qt::AlignTop);
693 setFont(VuoRendererFonts::getSharedFonts()->portPopoverFont());
694 setAutoFillBackground(true);
695 document()->setDocumentMargin(5);
696
697 VuoEditor *editor = (VuoEditor *)qApp;
698 bool isDark = editor->isInterfaceDark();
699 QString borderColor = isDark ? "#505050" : "#d1d1d1";
700 QString backgroundColor = isDark ? "#282828" : "#f9f9f9";
701
702 setStyleSheet("border: none;");
703
704 if (! isDetached)
705 {
706 Qt::WindowFlags flags = windowFlags();
707 flags |= Qt::FramelessWindowHint;
708 setWindowFlags(flags);
709
710
711 // Disabled until https://bugreports.qt-project.org/browse/QTBUG-40687 or https://b33p.net/kosada/node/5211 .
712// QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);
713// shadow->setBlurRadius(10);
714// shadow->setColor(QColor(160, 160, 160, 128));
715// shadow->setOffset(2);
716// this->setGraphicsEffect(shadow);
717
718 setAttribute(Qt::WA_TranslucentBackground);
719 setAutoFillBackground(false);
720
721 viewport()->setStyleSheet(QString(
722 "border: 1px solid %1;"
723 "border-radius: 4px;" // Rounded corners
724 "background-color: %2;"
725 )
726 .arg(borderColor)
727 .arg(backgroundColor)
728 );
729 }
730
731 else
732 {
733 Qt::WindowFlags flags = windowFlags();
734 flags |= Qt::Dialog; // This causes Qt to create an NSPanel instead of an NSWindow.
735 flags |= Qt::Tool;
736 flags &= ~Qt::FramelessWindowHint;
737 flags &= ~Qt::WindowMinimizeButtonHint;
738 setWindowFlags(flags);
739 setFixedSize(size());
740 setAttribute(Qt::WA_DeleteOnClose, false);
741
742#if __APPLE__
743 id window = (id)VuoPopover::getWindowForPopover(this);
744
745 unsigned long styleMask = 0;
746 styleMask |= 1 << 0; // NSWindowStyleMaskTitled
747 styleMask |= 1 << 1; // NSWindowStyleMaskClosable
748 styleMask |= 1 << 3; // NSWindowStyleMaskResizable
749 styleMask |= 1 << 4; // NSWindowStyleMaskUtilityWindow
750 if (isDark)
751 styleMask |= 1 << 13; // NSWindowStyleMaskHUDWindow
752 ((void (*)(id, SEL, unsigned long))objc_msgSend)(window, sel_getUid("setStyleMask:"), styleMask);
753
754 // Continue to show the panel even when the Vuo Editor isn't the focused application.
755 ((void (*)(id, SEL, BOOL))objc_msgSend)(window, sel_getUid("setHidesOnDeactivate:"), NO);
756
757 // Disable the maximize button (since disabling flag Qt::WindowMaximizeButtonHint doesn't do it).
758 {
759 int NSWindowZoomButton = 2;
760 id zoomButton = ((id (*)(id, SEL, unsigned long))objc_msgSend)(window, sel_getUid("standardWindowButton:"), NSWindowZoomButton);
761 if (zoomButton)
762 ((void (*)(id, SEL, BOOL))objc_msgSend)(zoomButton, sel_getUid("setEnabled:"), NO);
763 }
764#endif
765
766 // @todo: This doesn't seem to have any effect:
767 this->setGraphicsEffect(NULL);
768
769 viewport()->setStyleSheet(QString(
770 "border: none;"
771 "background-color: %1;"
772 )
773 .arg(backgroundColor)
774 );
775 }
776
777 QPalette p;
778 QColor bulletColor = isDark ? "#a0a0a0" : "#404040";
779 p.setColor(QPalette::Normal, QPalette::Text, bulletColor);
780 p.setColor(QPalette::Normal, QPalette::WindowText, bulletColor);
781 setPalette(p);
782
783
784 // Make a checkerboard background for VuoImage popovers.
785 QImage checkerboard(maxPopoverImageWidth, maxPopoverImageHeight, QImage::Format_Grayscale8);
786 checkerboard.fill(isDark ? "#232323" : "#ffffff");
787 QPainter painter(&checkerboard);
788 QColor c(isDark ? "#2c2c2c" : "#eeeeee");
789 for (int y = 0; y < maxPopoverImageHeight; y += 32)
790 for (int x = 0; x < maxPopoverImageWidth; x += 32)
791 if (x%64 != y%64)
792 painter.fillRect(x, y, 32, 32, c);
793 document()->addResource(QTextDocument::ImageResource, QUrl("vuo-port-popover://background.png"), QVariant(checkerboard));
794}
795
802{
803 if (!getDetached())
804 return;
805
807}
808
814{
815 if (!getDetached())
816 return;
817
819}
820
824void VuoPortPopover::mousePressEvent(QMouseEvent *event)
825{
826 if (event->button() == Qt::LeftButton)
827 {
828 if (! isDetached)
829 detach();
830
831 dragInProgress = true;
832 positionBeforeDrag = event->globalPos();
833 }
834
835 QTextBrowser::mousePressEvent(event);
836}
837
841void VuoPortPopover::mouseMoveEvent(QMouseEvent *event)
842{
843 if ((event->buttons() & Qt::LeftButton) && dragInProgress)
844 {
845 QPoint delta = event->globalPos() - positionBeforeDrag;
846 move(x() + delta.x(), y() + delta.y());
847 positionBeforeDrag = event->globalPos();
848 }
849
850 QTextBrowser::mouseMoveEvent(event);
851}
852
856void VuoPortPopover::mouseReleaseEvent(QMouseEvent *event)
857{
858 if (event->button() == Qt::LeftButton)
859 dragInProgress = false;
860
861 QTextBrowser::mouseReleaseEvent(event);
862}
863
867void VuoPortPopover::closeEvent(QCloseEvent *event)
868{
869 emit popoverClosedForPort(portID);
870 QTextBrowser::closeEvent(event);
871}
872
876void VuoPortPopover::resizeEvent(QResizeEvent *event)
877{
878 emit popoverResized();
879 QTextBrowser::resizeEvent(event);
880}
881
890double VuoPortPopover::getEventIntervalMean(QQueue<qint64> timestamps)
891{
892 if (timestamps.size() <= 1)
893 return -1;
894
895 double secBetweenFirstAndLastEvents = (timestamps.last() - timestamps.first())/1000.;
896 int numEventIntervals = timestamps.size()-1;
897
898 return (secBetweenFirstAndLastEvents/(1.0*numEventIntervals));
899}
900
909double VuoPortPopover::getEventIntervalStdDev(QQueue<qint64> timestamps)
910{
911 int numIntervals = timestamps.size() - 1;
912 if (numIntervals < 2)
913 return -1;
914
915 double meanIntervalInSec = getEventIntervalMean(timestamps);
916 double diffSquaredSum = 0;
917 for (int i = 1; i < timestamps.size(); ++i)
918 {
919 double intervalInSec = (timestamps[i] - timestamps[i-1])/1000.;
920 diffSquaredSum += pow(abs(intervalInSec-meanIntervalInSec), 2);
921 }
922
923 return pow(diffSquaredSum/(1.0*(numIntervals-1)), 0.5);
924}
925
926void VuoPortPopover::helpButtonClicked()
927{
928 QDesktopServices::openUrl(QUrl("vuo-help:how-events-travel-through-a-subcomposition.html"));
929}