26#include <objc/objc-runtime.h>
27#include <objc/runtime.h>
30const int VuoPortPopover::maxPopoverContentWidth = 512;
31const int VuoPortPopover::maxPopoverImageWidth = 256;
32const int VuoPortPopover::maxPopoverImageHeight = 256;
33const qreal VuoPortPopover::minTextUpdateInterval = 100;
34const int VuoPortPopover::eventHistoryMaxSize = 20;
35const int VuoPortPopover::noEventObserved = -1;
36const int VuoPortPopover::noDisplayableEventTime = -2;
37const string VuoPortPopover::noDataValueObserved =
"";
38const string VuoPortPopover::noDisplayableDataValue =
"Unknown";
48 this->composition = composition;
49 this->cachedDataValue = noDisplayableDataValue;
50 this->timeOfLastEvent = noDisplayableEventTime;
51 this->timeOfLastUpdate = noDisplayableEventTime;
53 this->droppedEventCount = 0;
54 this->isDetached =
false;
57 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
58 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
60 setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
61 setOpenExternalLinks(
true);
66 this->popoverTextQueue = dispatch_queue_create(
"org.vuo.editor.port", NULL);
68 this->refreshTextTimer =
new QTimer(
this);
69 this->refreshTextTimer->setObjectName(
"VuoPortPopover::refreshTextTimer");
70 refreshTextTimerFiredSinceLastReset =
false;
80 allEventsBlocked = (triggerPortsReached == 0 && allPortsReached > 0);
81 someEventsBlocked = (triggerPortsReached > 0 && allPortsReached > triggerPortsReached);
83 if (allEventsBlocked || someEventsBlocked)
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);
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);
99 allEventsBlocked = someEventsBlocked =
false;
108 this->dragInProgress =
false;
117 dispatch_release(popoverTextQueue);
128 dispatch_sync(popoverTextQueue, ^{
129 mostRecentImage.clear();
130 this->cachedDataValue = value.toUtf8().constData();
131 this->updateTextThreadUnsafe();
132 resetRefreshTextInterval();
149 dispatch_sync(popoverTextQueue, ^{
150 qint64 timeBefore = this->timeOfLastUpdate;
151 qint64 timeNow = QDateTime::currentMSecsSinceEpoch();
152 this->timeOfLastUpdate = timeNow;
156 this->timeOfLastEvent = timeNow;
157 this->eventHistory.enqueue(timeNow);
158 while (this->eventHistory.size() > eventHistoryMaxSize)
159 this->eventHistory.dequeue();
166 this->cachedDataValue = value.toUtf8().constData();
169 if (timeBefore + minTextUpdateInterval < timeNow)
171 this->updateTextThreadUnsafe(
true);
172 resetRefreshTextInterval();
182 dispatch_sync(popoverTextQueue, ^{
185 this->updateTextThreadUnsafe(
true);
186 resetRefreshTextInterval();
196 dispatch_sync(popoverTextQueue, ^{
197 this->compositionRunning = running;
202 this->cachedDataValue = noDataValueObserved;
204 this->timeOfLastEvent = noEventObserved;
205 this->timeOfLastUpdate = noEventObserved;
207 while (!eventHistory.isEmpty())
208 eventHistory.dequeue();
210 this->eventCount = 0;
211 this->droppedEventCount = 0;
215 refreshTextTimer->setInterval(minTextUpdateInterval);
216 refreshTextTimer->start();
221 refreshTextTimer->stop();
225 updateTextThreadUnsafe();
237 dispatch_sync(popoverTextQueue, ^{
238 updateTextAndResizeThreadUnsafe();
239 refreshTextTimerFiredSinceLastReset =
true;
247QString VuoPortPopover::generateImageCode()
254 if (! compositionRunning)
255 return mostRecentImage;
259 return mostRecentImage;
263 if (json_object_object_get_ex(imageJson,
"ioSurface", &o))
265 else if (json_object_object_get_ex(imageJson,
"image", &o))
268 VUserLog(
"Warning: Unknown interprocess JSON format.");
269 json_object_put(imageJson);
271 return mostRecentImage;
275 int maxImageWidth = maxPopoverImageWidth;
276 int maxImageHeight = maxPopoverImageHeight;
278 int devicePixelRatio = window()->windowHandle()->screen()->devicePixelRatio();
279 maxImageWidth *= devicePixelRatio;
280 maxImageHeight *= devicePixelRatio;
282 if (image->pixelsWide > maxImageWidth
283 || image->pixelsHigh > maxImageHeight)
290 image = resizedImage;
295 unsigned long int bufferLength = image->pixelsWide * image->pixelsHigh * 4;
296 unsigned char *bufferClamped = (
unsigned char *)malloc(bufferLength);
301 for (
unsigned long int i = 0; i < bufferLength; i += 4)
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;
310 QImage qi(bufferClamped, image->pixelsWide, image->pixelsHigh, QImage::Format_RGBA8888_Premultiplied, free, bufferClamped);
312 qi.setDevicePixelRatio(devicePixelRatio);
313 qi = qi.mirrored(
false,
true);
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;
326void VuoPortPopover::updateTextAndResizeThreadUnsafe()
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;
339 if (updatedTextRefreshInterval != refreshTextTimer->interval())
340 this->refreshTextTimer->setInterval(updatedTextRefreshInterval);
342 setHtml(generatePortPopoverText());
346 document()->setPageSize(QSizeF(maxPopoverContentWidth, 10000));
347 document()->setPageSize(QSizeF(ceil(document()->idealWidth()), 10000));
348 QSize documentSize = document()->documentLayout()->documentSize().toSize();
351 documentSize = documentSize.expandedTo(size());
353 this->setFixedSize(documentSize);
361void VuoPortPopover::updateTextThreadUnsafe(
bool includeEventIndicator)
363 this->setText(generatePortPopoverText(includeEventIndicator));
371void VuoPortPopover::resetRefreshTextInterval()
373 if (refreshTextTimerFiredSinceLastReset)
375 refreshTextTimerFiredSinceLastReset =
false;
376 this->refreshTextTimer->setInterval(minTextUpdateInterval);
379 QThread::yieldCurrentThread();
389QString VuoPortPopover::generatePortPopoverText(
bool includeEventIndicator)
396 json_object *details =
nullptr;
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);
407 QString textColor = isDark ?
"#cacaca" :
"#000000";
408 QString subtleTextColor = isDark ?
"#808080" :
"#808080";
409 QString subtlerTextColor = isDark ?
"#505050" :
"#bbbbbb";
410 QString codeTextColor = isDark ?
"#383838" :
"#ececec";
412 qint64 timeOfLastEventSnapshot = this->timeOfLastEvent;
413 QQueue<qint64> eventHistorySnapshot = this->eventHistory;
414 string cachedDataValueSnapshot = this->cachedDataValue;
415 bool compositionRunningSnapshot = this->compositionRunning;
419 string nodeName = parentNode->
getTitle();
423 bool displayValue = (dataType && (cachedDataValueSnapshot != noDisplayableDataValue) && !isCodeEditor);
427 const string noEvent = (
"(" + tr(
"none observed") +
")").toStdString();
431 const string unknownFrequency =
"(" + tr(
"%1 observed",
"", eventCount).arg(eventCount).toStdString() +
")";
433 QString lastEventTime;
434 string lastEventFrequency =
"";
436 bool displayLastEventTime = compositionRunningSnapshot;
437 bool displayEventFrequency = (timeOfLastEventSnapshot != noDisplayableEventTime);
440 if (displayLastEventTime && (timeOfLastEventSnapshot != noEventObserved))
442 qint64 timeNow = QDateTime::currentMSecsSinceEpoch();
443 double secondsSinceLastEvent = (timeNow - timeOfLastEventSnapshot)/1000.;
444 int roundedSecondsSinceLastEvent = (int)(secondsSinceLastEvent + 0.5);
446 lastEventTime = secondsSinceLastEvent <= 1
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"))))));
462 if (displayEventFrequency)
464 if (timeOfLastEventSnapshot == noEventObserved)
465 lastEventFrequency = noEvent;
467 else if (eventHistorySnapshot.size() > 2)
469 double recentEventIntervalMean = getEventIntervalMean(eventHistorySnapshot);
470 double recentEventIntervalStdDev = getEventIntervalStdDev(eventHistorySnapshot);
474 double recentEventIntervalCV = recentEventIntervalStdDev/recentEventIntervalMean;
475 const double maxCVForFrequencyDisplay = 2.0;
476 if (recentEventIntervalCV <= maxCVForFrequencyDisplay)
478 double recentEventFrequency = 1./recentEventIntervalMean;
482 QString unit = tr(
"per second");
483 double roundedEventFrequency = ((int)(10*recentEventFrequency+0.5))/10.;
485 if (roundedEventFrequency == 0)
488 unit = tr(
"per minute");
489 roundedEventFrequency = ((int)(10*60*recentEventFrequency+0.5))/10.;
492 if (roundedEventFrequency == 0)
495 unit = tr(
"per hour");
496 roundedEventFrequency = ((int)(10*60*60*recentEventFrequency+0.5))/10.;
499 lastEventFrequency +=
"(~";
500 lastEventFrequency += QLocale::system().toString(roundedEventFrequency,
'f', 1).toStdString();
501 lastEventFrequency +=
" ";
502 lastEventFrequency += unit.toStdString();
503 lastEventFrequency +=
")";
507 lastEventFrequency = unknownFrequency;
511 lastEventFrequency = unknownFrequency;
515 if ((lastEventFrequency == unknownFrequency) && compositionRunningSnapshot)
516 lastEventFrequency =
"";
518 if (!lastEventTime.isEmpty() && !lastEventFrequency.empty())
519 lastEventFrequency =
" " + lastEventFrequency;
521 if (includeEventIndicator && compositionRunningSnapshot)
522 lastEventFrequency +=
" ยท";
526 lastEventFrequency +=
" ";
529 QString eventThrottlingDescription;
534 eventThrottlingDescription = tr(
"enqueue events");
535 else if (! compositionRunningSnapshot)
537 eventThrottlingDescription = tr(
"drop events");
540 unsigned int totalEventCount = droppedEventCount + eventCount;
541 unsigned int percentDropped = round( (
float)droppedEventCount / (
float)totalEventCount * 100 );
545 eventThrottlingDescription = tr(
"%1 event(s) dropped (%2%)",
"", droppedEventCount).arg(droppedEventCount).arg(percentDropped);
550 QString formattedTriggerBlockedHelp;
551 if (allEventsBlocked || someEventsBlocked)
552 formattedTriggerBlockedHelp =
"<p>"
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."))
557 +
" </p>";
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>";
571 json_object *menuItemsValue = NULL;
572 if (details && dataType && dataType->
getModuleKey() ==
"VuoInteger" && json_object_object_get_ex(details,
"menuItems", &menuItemsValue))
574 int len = json_object_array_length(menuItemsValue);
575 for (
int i = 0; i < len; ++i)
577 json_object *menuItem = json_object_array_get_idx(menuItemsValue, i);
578 if (json_object_is_type(menuItem, json_type_object))
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))
584 json_object *name = NULL;
585 if (json_object_object_get_ex(menuItem,
"name", &name))
587 cachedDataValueSnapshot +=
": ";
588 cachedDataValueSnapshot += json_object_get_string(name);
599 QString formattedDataValue =
"<tr><th>" + tr(
"Value")
600 +
": </th><td>" + generateImageCode() + QString::fromStdString(cachedDataValueSnapshot) +
"</td></tr>";
602 QString popoverText = VUO_QSTRINGIFY(
605 font-family:
'Monaco';
607 background-color: %1;
608 white-space: pre-wrap;
633 .arg(subtleTextColor)
634 .arg(subtlerTextColor);
635 popoverText.append(formattedPortName);
636 popoverText.append(
"<table>");
637 popoverText.append(formattedDataTypeDescription);
640 popoverText.append(formattedThrottlingDescription);
642 if ((displayLastEventTime || displayEventFrequency) && (!lastEventTime.isEmpty() || !lastEventFrequency.empty()))
643 popoverText.append(formattedLastEventLine);
646 popoverText.append(formattedDataValue);
648 popoverText.append(
"</table>");
650 popoverText.append(formattedTriggerBlockedHelp);
668 this->isDetached =
true;
674 QPoint newGlobalPos = parentWidget()->mapToGlobal(pos());
690void VuoPortPopover::updateStyle()
692 setAlignment(Qt::AlignTop);
694 setAutoFillBackground(
true);
695 document()->setDocumentMargin(5);
699 QString borderColor = isDark ?
"#505050" :
"#d1d1d1";
700 QString backgroundColor = isDark ?
"#282828" :
"#f9f9f9";
702 setStyleSheet(
"border: none;");
706 Qt::WindowFlags flags = windowFlags();
707 flags |= Qt::FramelessWindowHint;
708 setWindowFlags(flags);
718 setAttribute(Qt::WA_TranslucentBackground);
719 setAutoFillBackground(
false);
721 viewport()->setStyleSheet(QString(
722 "border: 1px solid %1;"
723 "border-radius: 4px;"
724 "background-color: %2;"
727 .arg(backgroundColor)
733 Qt::WindowFlags flags = windowFlags();
736 flags &= ~Qt::FramelessWindowHint;
737 flags &= ~Qt::WindowMinimizeButtonHint;
738 setWindowFlags(flags);
739 setFixedSize(size());
740 setAttribute(Qt::WA_DeleteOnClose,
false);
745 unsigned long styleMask = 0;
751 styleMask |= 1 << 13;
752 ((void (*)(id, SEL,
unsigned long))objc_msgSend)(window, sel_getUid(
"setStyleMask:"), styleMask);
755 ((void (*)(id, SEL,
BOOL))objc_msgSend)(window, sel_getUid(
"setHidesOnDeactivate:"), NO);
759 int NSWindowZoomButton = 2;
760 id zoomButton = ((id (*)(id, SEL,
unsigned long))objc_msgSend)(window, sel_getUid(
"standardWindowButton:"), NSWindowZoomButton);
762 ((void (*)(id, SEL,
BOOL))objc_msgSend)(zoomButton, sel_getUid(
"setEnabled:"), NO);
767 this->setGraphicsEffect(NULL);
769 viewport()->setStyleSheet(QString(
771 "background-color: %1;"
773 .arg(backgroundColor)
778 QColor bulletColor = isDark ?
"#a0a0a0" :
"#404040";
779 p.setColor(QPalette::Normal, QPalette::Text, bulletColor);
780 p.setColor(QPalette::Normal, QPalette::WindowText, bulletColor);
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)
792 painter.fillRect(x, y, 32, 32, c);
793 document()->addResource(QTextDocument::ImageResource, QUrl(
"vuo-port-popover://background.png"), QVariant(checkerboard));
826 if (event->button() == Qt::LeftButton)
831 dragInProgress =
true;
832 positionBeforeDrag =
event->globalPos();
835 QTextBrowser::mousePressEvent(event);
843 if ((event->buttons() & Qt::LeftButton) && dragInProgress)
845 QPoint delta =
event->globalPos() - positionBeforeDrag;
846 move(x() + delta.x(), y() + delta.y());
847 positionBeforeDrag =
event->globalPos();
850 QTextBrowser::mouseMoveEvent(event);
858 if (event->button() == Qt::LeftButton)
859 dragInProgress =
false;
861 QTextBrowser::mouseReleaseEvent(event);
870 QTextBrowser::closeEvent(event);
879 QTextBrowser::resizeEvent(event);
890double VuoPortPopover::getEventIntervalMean(QQueue<qint64> timestamps)
892 if (timestamps.size() <= 1)
895 double secBetweenFirstAndLastEvents = (timestamps.last() - timestamps.first())/1000.;
896 int numEventIntervals = timestamps.size()-1;
898 return (secBetweenFirstAndLastEvents/(1.0*numEventIntervals));
909double VuoPortPopover::getEventIntervalStdDev(QQueue<qint64> timestamps)
911 int numIntervals = timestamps.size() - 1;
912 if (numIntervals < 2)
915 double meanIntervalInSec = getEventIntervalMean(timestamps);
916 double diffSquaredSum = 0;
917 for (
int i = 1; i < timestamps.size(); ++i)
919 double intervalInSec = (timestamps[i] - timestamps[i-1])/1000.;
920 diffSquaredSum += pow(abs(intervalInSec-meanIntervalInSec), 2);
923 return pow(diffSquaredSum/(1.0*(numIntervals-1)), 0.5);
926void VuoPortPopover::helpButtonClicked()
928 QDesktopServices::openUrl(QUrl(
"vuo-help:how-events-travel-through-a-subcomposition.html"));