Files
renderdoc/qrenderdoc/Code/QRDUtils.cpp
T
baldurk ebaefc82a9 Normalise and make python/public interface more consistent
* We enforce a naming scheme more strongly - types, member functions,
  and enum values must be UpperCaseCamel, and member variables must be
  lowerCaseCamel. No underscores allowed.
* eventId not eventID or EID, and Id preferred to ID in general. Also
  for resourceId.
* Removed some lingering hungarian m_Foo naming.
* Some pipeline state structs that are almost identical between the
  different APIs are pulled out into common structs. Where something
  doesn't make sense (e.g. viewport enable for vulkan) it will just be
  set to a sensible default (in that case always true).
* Changed scissors to be x/y & width/height instead of sometimes
  left/top/right/bottom
* Abbreviations are discouraged, e.g. operation not op, function not
  func.
2017-12-22 13:02:36 +00:00

1684 lines
48 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2016-2017 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "QRDUtils.h"
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QElapsedTimer>
#include <QElapsedTimer>
#include <QFileSystemModel>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QKeyEvent>
#include <QLabel>
#include <QMenu>
#include <QMetaMethod>
#include <QMouseEvent>
#include <QPainter>
#include <QProcess>
#include <QProgressBar>
#include <QProgressDialog>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QStandardPaths>
#include <QTextBlock>
#include <QTextDocument>
#include <QtMath>
#include "Widgets/Extended/RDTreeWidget.h"
// normally this is in the renderdoc core library, but it's needed for the 'unknown enum' path,
// so we implement it here using QString. It's inefficient, but this is a very uncommon path -
// either for invalid values or for when a new enum is added and the code isn't updated
template <>
std::string DoStringise(const uint32_t &el)
{
return QString::number(el).toStdString();
}
// this one we do by hand as it requires formatting
template <>
std::string DoStringise(const ResourceId &el)
{
uint64_t num;
memcpy(&num, &el, sizeof(num));
return lit("resourceid::%1").arg(num).toStdString();
}
struct RichResourceText
{
QVector<QVariant> fragments;
// cached formatted document. We use cacheId to check if it needs to be updated
QTextDocument doc;
int cacheId = 0;
// a plain-text version of the document, suitable for e.g. copy-paste
QString text;
// the ideal width for the document
int idealWidth = 0;
// cache the context once we've obtained it.
ICaptureContext *ctxptr = NULL;
void cacheDocument(const QWidget *widget)
{
if(!ctxptr)
ctxptr = getCaptureContext(widget);
if(!ctxptr)
return;
ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
int refCache = ctx.ResourceNameCacheID();
if(cacheId == refCache)
return;
cacheId = refCache;
// use a table to ensure images don't screw up the baseline for text. DON'T JUDGE ME.
QString html = lit("<table><tr>");
int i = 0;
QVector<int> fragmentIndexFromBlockIndex;
// there's an empty block at the start.
fragmentIndexFromBlockIndex.push_back(-1);
text.clear();
for(const QVariant &v : fragments)
{
if(v.userType() == qMetaTypeId<ResourceId>())
{
QString resname = ctx.GetResourceName(v.value<ResourceId>());
html += lit("<td><b>%1</b></td><td><img src=':/link.png'></td>").arg(resname);
text += resname;
// these generate two blocks (one for each cell)
fragmentIndexFromBlockIndex.push_back(i);
fragmentIndexFromBlockIndex.push_back(i);
}
else
{
html += lit("<td>%1</td>").arg(v.toString());
text += v.toString();
// this only generates one block
fragmentIndexFromBlockIndex.push_back(i);
}
i++;
}
// there's another empty block at the end
fragmentIndexFromBlockIndex.push_back(-1);
html += lit("</tr></table>");
doc.setDocumentMargin(0);
doc.setHtml(html);
if(doc.blockCount() != fragmentIndexFromBlockIndex.count())
{
qCritical() << "Block count is not what's expected!" << doc.blockCount()
<< fragmentIndexFromBlockIndex.count();
for(i = 0; i < doc.blockCount(); i++)
doc.findBlockByNumber(i).setUserState(-1);
return;
}
for(i = 0; i < doc.blockCount(); i++)
doc.findBlockByNumber(i).setUserState(fragmentIndexFromBlockIndex[i]);
qreal old = doc.textWidth();
doc.setTextWidth(-1);
idealWidth = doc.idealWidth();
doc.setTextWidth(old);
}
};
Q_DECLARE_METATYPE(RichResourceTextPtr);
QString ResIdTextToString(RichResourceTextPtr ptr)
{
return ptr->text;
}
void RegisterMetatypeConversions()
{
QMetaType::registerConverter<RichResourceTextPtr, QString>(&ResIdTextToString);
}
void RichResourceTextInitialise(QVariant &var)
{
static QRegularExpression re(lit("(resourceid::)([0-9]*)"));
if(var.userType() == qMetaTypeId<ResourceId>() || re.match(var.toString()).hasMatch())
{
QString text;
if(var.userType() == qMetaTypeId<ResourceId>())
text = ToQStr(var.value<ResourceId>());
else
text = var.toString();
RichResourceTextPtr linkedText(new RichResourceText);
// use regexp to split up into fragments of text and resourceid. The resourceid is then
// formatted on the fly in RichResourceText::cacheDocument
QRegularExpressionMatch match = re.match(text);
while(match.hasMatch())
{
qulonglong idnum = match.captured(2).toULongLong();
ResourceId id;
memcpy(&id, &idnum, sizeof(id));
// push any text that preceeded the ResourceId.
if(match.capturedStart(1) > 0)
linkedText->fragments.push_back(text.left(match.capturedStart(1)));
text.remove(0, match.capturedEnd(2));
linkedText->fragments.push_back(id);
match = re.match(text);
}
if(!text.isEmpty())
linkedText->fragments.push_back(text);
linkedText->doc.setHtml(text);
var = QVariant::fromValue(linkedText);
}
}
bool RichResourceTextCheck(const QVariant &var)
{
return var.userType() == qMetaTypeId<RichResourceTextPtr>();
}
void RichResourceTextPaint(const QWidget *owner, QPainter *painter, QRect rect, QFont font,
QPalette palette, bool mouseOver, QPoint mousePos, const QVariant &var)
{
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
linkedText->cacheDocument(owner);
painter->translate(rect.left(), rect.top());
linkedText->doc.setTextWidth(10000);
linkedText->doc.setDefaultFont(font);
// vertical align to the centre, if there's spare room.
int diff = rect.height() - linkedText->doc.size().height();
if(diff > 0)
painter->translate(0, diff / 2);
linkedText->doc.drawContents(painter, QRectF(0, 0, rect.width(), rect.height()));
if(mouseOver)
{
painter->setPen(QPen(palette.brush(QPalette::WindowText), 1.0));
QAbstractTextDocumentLayout *layout = linkedText->doc.documentLayout();
QPoint p = mousePos - rect.topLeft();
if(diff > 0)
p -= QPoint(0, diff / 2);
int pos = layout->hitTest(p, Qt::FuzzyHit);
if(pos >= 0)
{
QTextBlock block = linkedText->doc.findBlock(pos);
int frag = block.userState();
if(frag >= 0)
{
QVariant v = linkedText->fragments[frag];
if(v.userType() == qMetaTypeId<ResourceId>() && v.value<ResourceId>() != ResourceId())
{
layout->blockBoundingRect(block);
QRectF blockrect = layout->blockBoundingRect(block);
if(block.previous().userState() == frag)
{
blockrect = blockrect.united(layout->blockBoundingRect(block.previous()));
}
if(block.next().userState() == frag)
{
blockrect = blockrect.united(layout->blockBoundingRect(block.next()));
}
blockrect.translate(0.0, -2.0);
painter->drawLine(blockrect.bottomLeft(), blockrect.bottomRight());
}
}
}
}
}
int RichResourceTextWidthHint(const QWidget *owner, const QVariant &var)
{
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
linkedText->cacheDocument(owner);
return linkedText->idealWidth;
}
bool RichResourceTextMouseEvent(const QWidget *owner, const QVariant &var, QRect rect,
QMouseEvent *event)
{
// only process clicks or moves
if(event->type() != QEvent::MouseButtonRelease && event->type() != QEvent::MouseMove)
return false;
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
linkedText->cacheDocument(owner);
QAbstractTextDocumentLayout *layout = linkedText->doc.documentLayout();
QPoint p = event->pos() - rect.topLeft();
// vertical align to the centre, if there's spare room.
int diff = rect.height() - linkedText->doc.size().height();
if(diff > 0)
p -= QPoint(0, diff / 2);
int pos = layout->hitTest(p, Qt::FuzzyHit);
if(pos >= 0)
{
QTextBlock block = linkedText->doc.findBlock(pos);
int frag = block.userState();
if(frag >= 0)
{
QVariant v = linkedText->fragments[frag];
if(v.userType() == qMetaTypeId<ResourceId>())
{
// empty resource ids are not clickable or hover-highlighted.
ResourceId res = v.value<ResourceId>();
if(res == ResourceId())
return false;
if(event->type() == QEvent::MouseButtonRelease && linkedText->ctxptr)
{
ICaptureContext &ctx = *(ICaptureContext *)linkedText->ctxptr;
if(!ctx.HasResourceInspector())
ctx.ShowResourceInspector();
ctx.GetResourceInspector()->Inspect(v.value<ResourceId>());
ctx.RaiseDockWindow(ctx.GetResourceInspector()->Widget());
}
return true;
}
}
}
return false;
}
#include "renderdoc_tostr.inl"
QString ToQStr(const ResourceUsage usage, const GraphicsAPI apitype)
{
if(IsD3D(apitype))
{
switch(usage)
{
case ResourceUsage::VertexBuffer: return lit("Vertex Buffer");
case ResourceUsage::IndexBuffer: return lit("Index Buffer");
case ResourceUsage::VS_Constants: return lit("VS - Constant Buffer");
case ResourceUsage::GS_Constants: return lit("GS - Constant Buffer");
case ResourceUsage::HS_Constants: return lit("HS - Constant Buffer");
case ResourceUsage::DS_Constants: return lit("DS - Constant Buffer");
case ResourceUsage::CS_Constants: return lit("CS - Constant Buffer");
case ResourceUsage::PS_Constants: return lit("PS - Constant Buffer");
case ResourceUsage::All_Constants: return lit("All - Constant Buffer");
case ResourceUsage::StreamOut: return lit("Stream Out");
case ResourceUsage::VS_Resource: return lit("VS - Resource");
case ResourceUsage::GS_Resource: return lit("GS - Resource");
case ResourceUsage::HS_Resource: return lit("HS - Resource");
case ResourceUsage::DS_Resource: return lit("DS - Resource");
case ResourceUsage::CS_Resource: return lit("CS - Resource");
case ResourceUsage::PS_Resource: return lit("PS - Resource");
case ResourceUsage::All_Resource: return lit("All - Resource");
case ResourceUsage::VS_RWResource: return lit("VS - UAV");
case ResourceUsage::HS_RWResource: return lit("HS - UAV");
case ResourceUsage::DS_RWResource: return lit("DS - UAV");
case ResourceUsage::GS_RWResource: return lit("GS - UAV");
case ResourceUsage::PS_RWResource: return lit("PS - UAV");
case ResourceUsage::CS_RWResource: return lit("CS - UAV");
case ResourceUsage::All_RWResource: return lit("All - UAV");
case ResourceUsage::InputTarget: return lit("Color Input");
case ResourceUsage::ColorTarget: return lit("Rendertarget");
case ResourceUsage::DepthStencilTarget: return lit("Depthstencil");
case ResourceUsage::Indirect: return lit("Indirect argument");
case ResourceUsage::Clear: return lit("Clear");
case ResourceUsage::GenMips: return lit("Generate Mips");
case ResourceUsage::Resolve: return lit("Resolve");
case ResourceUsage::ResolveSrc: return lit("Resolve - Source");
case ResourceUsage::ResolveDst: return lit("Resolve - Dest");
case ResourceUsage::Copy: return lit("Copy");
case ResourceUsage::CopySrc: return lit("Copy - Source");
case ResourceUsage::CopyDst: return lit("Copy - Dest");
case ResourceUsage::Barrier: return lit("Barrier");
default: break;
}
}
else if(apitype == GraphicsAPI::OpenGL || apitype == GraphicsAPI::Vulkan)
{
const bool vk = (apitype == GraphicsAPI::Vulkan);
switch(usage)
{
case ResourceUsage::VertexBuffer: return lit("Vertex Buffer");
case ResourceUsage::IndexBuffer: return lit("Index Buffer");
case ResourceUsage::VS_Constants: return lit("VS - Uniform Buffer");
case ResourceUsage::GS_Constants: return lit("GS - Uniform Buffer");
case ResourceUsage::HS_Constants: return lit("HS - Uniform Buffer");
case ResourceUsage::DS_Constants: return lit("DS - Uniform Buffer");
case ResourceUsage::CS_Constants: return lit("CS - Uniform Buffer");
case ResourceUsage::PS_Constants: return lit("PS - Uniform Buffer");
case ResourceUsage::All_Constants: return lit("All - Uniform Buffer");
case ResourceUsage::StreamOut: return lit("Transform Feedback");
case ResourceUsage::VS_Resource: return lit("VS - Texture");
case ResourceUsage::GS_Resource: return lit("GS - Texture");
case ResourceUsage::HS_Resource: return lit("HS - Texture");
case ResourceUsage::DS_Resource: return lit("DS - Texture");
case ResourceUsage::CS_Resource: return lit("CS - Texture");
case ResourceUsage::PS_Resource: return lit("PS - Texture");
case ResourceUsage::All_Resource: return lit("All - Texture");
case ResourceUsage::VS_RWResource: return lit("VS - Image/SSBO");
case ResourceUsage::HS_RWResource: return lit("HS - Image/SSBO");
case ResourceUsage::DS_RWResource: return lit("DS - Image/SSBO");
case ResourceUsage::GS_RWResource: return lit("GS - Image/SSBO");
case ResourceUsage::PS_RWResource: return lit("PS - Image/SSBO");
case ResourceUsage::CS_RWResource: return lit("CS - Image/SSBO");
case ResourceUsage::All_RWResource: return lit("All - Image/SSBO");
case ResourceUsage::InputTarget: return lit("FBO Input");
case ResourceUsage::ColorTarget: return lit("FBO Color");
case ResourceUsage::DepthStencilTarget: return lit("FBO Depthstencil");
case ResourceUsage::Indirect: return lit("Indirect argument");
case ResourceUsage::Clear: return lit("Clear");
case ResourceUsage::GenMips: return lit("Generate Mips");
case ResourceUsage::Resolve: return vk ? lit("Resolve") : lit("Framebuffer blit");
case ResourceUsage::ResolveSrc:
return vk ? lit("Resolve - Source") : lit("Framebuffer blit - Source");
case ResourceUsage::ResolveDst:
return vk ? lit("Resolve - Dest") : lit("Framebuffer blit - Dest");
case ResourceUsage::Copy: return lit("Copy");
case ResourceUsage::CopySrc: return lit("Copy - Source");
case ResourceUsage::CopyDst: return lit("Copy - Dest");
case ResourceUsage::Barrier: return lit("Barrier");
default: break;
}
}
return lit("Unknown");
}
QString ToQStr(const ShaderStage stage, const GraphicsAPI apitype)
{
if(IsD3D(apitype))
{
switch(stage)
{
case ShaderStage::Vertex: return lit("Vertex");
case ShaderStage::Hull: return lit("Hull");
case ShaderStage::Domain: return lit("Domain");
case ShaderStage::Geometry: return lit("Geometry");
case ShaderStage::Pixel: return lit("Pixel");
case ShaderStage::Compute: return lit("Compute");
default: break;
}
}
else if(apitype == GraphicsAPI::OpenGL || apitype == GraphicsAPI::Vulkan)
{
switch(stage)
{
case ShaderStage::Vertex: return lit("Vertex");
case ShaderStage::Tess_Control: return lit("Tess. Control");
case ShaderStage::Tess_Eval: return lit("Tess. Eval");
case ShaderStage::Geometry: return lit("Geometry");
case ShaderStage::Fragment: return lit("Fragment");
case ShaderStage::Compute: return lit("Compute");
default: break;
}
}
return lit("Unknown");
}
QString TypeString(const SigParameter &sig)
{
QString ret = lit("");
if(sig.compType == CompType::Float)
ret += lit("float");
else if(sig.compType == CompType::UInt || sig.compType == CompType::UScaled)
ret += lit("uint");
else if(sig.compType == CompType::SInt || sig.compType == CompType::SScaled)
ret += lit("int");
else if(sig.compType == CompType::UNorm)
ret += lit("unorm float");
else if(sig.compType == CompType::SNorm)
ret += lit("snorm float");
else if(sig.compType == CompType::Depth)
ret += lit("float");
if(sig.compCount > 1)
ret += QString::number(sig.compCount);
return ret;
}
QString D3DSemanticString(const SigParameter &sig)
{
if(sig.systemValue == ShaderBuiltin::Undefined)
return sig.semanticIdxName;
QString sysValues[ENUM_ARRAY_SIZE(ShaderBuiltin)] = {
lit("SV_Undefined"),
lit("SV_Position"),
lit("Unsupported (PointSize)"),
lit("SV_ClipDistance"),
lit("SV_CullDistance"),
lit("SV_RenderTargetIndex"),
lit("SV_ViewportIndex"),
lit("SV_VertexID"),
lit("SV_PrimitiveID"),
lit("SV_InstanceID"),
lit("Unsupported (DispatchSize)"),
lit("SV_DispatchThreadID"),
lit("SV_GroupID"),
lit("SV_GroupIndex"),
lit("SV_GroupThreadID"),
lit("SV_GSInstanceID"),
lit("SV_OutputControlPointID"),
lit("SV_DomainLocation"),
lit("SV_IsFrontFace"),
lit("SV_Coverage"),
lit("Unsupported (SamplePosition)"),
lit("SV_SampleIndex"),
lit("Unsupported (PatchNumVertices)"),
lit("SV_TessFactor"),
lit("SV_InsideTessFactor"),
lit("SV_Target"),
lit("SV_Depth"),
lit("SV_DepthGreaterEqual"),
lit("SV_DepthLessEqual"),
};
QString ret = sysValues[size_t(sig.systemValue)];
// need to include the index if it's a system value semantic that's numbered
if(sig.systemValue == ShaderBuiltin::ColorOutput ||
sig.systemValue == ShaderBuiltin::CullDistance || sig.systemValue == ShaderBuiltin::ClipDistance)
ret += QString::number(sig.semanticIndex);
return ret;
}
QString GetComponentString(byte mask)
{
QString ret;
if((mask & 0x1) > 0)
ret += lit("R");
if((mask & 0x2) > 0)
ret += lit("G");
if((mask & 0x4) > 0)
ret += lit("B");
if((mask & 0x8) > 0)
ret += lit("A");
return ret;
}
void CombineUsageEvents(ICaptureContext &ctx, const rdcarray<EventUsage> &usage,
std::function<void(uint32_t startEID, uint32_t endEID, ResourceUsage use)> callback)
{
uint32_t start = 0;
uint32_t end = 0;
ResourceUsage us = ResourceUsage::IndexBuffer;
for(const EventUsage u : usage)
{
if(start == 0)
{
start = end = u.eventId;
us = u.usage;
continue;
}
const DrawcallDescription *draw = ctx.GetDrawcall(u.eventId);
bool distinct = false;
// if the usage is different from the last, add a new entry,
// or if the previous draw link is broken.
if(u.usage != us || draw == NULL || draw->previous == 0)
{
distinct = true;
}
else
{
// otherwise search back through real draws, to see if the
// last event was where we were - otherwise it's a new
// distinct set of drawcalls and should have a separate
// entry in the context menu
const DrawcallDescription *prev = ctx.GetDrawcall(draw->previous);
while(prev != NULL && prev->eventId > end)
{
if(!(prev->flags & (DrawFlags::Dispatch | DrawFlags::Drawcall | DrawFlags::CmdList)))
{
prev = ctx.GetDrawcall(prev->previous);
}
else
{
distinct = true;
break;
}
if(prev == NULL)
distinct = true;
}
}
if(distinct)
{
callback(start, end, us);
start = end = u.eventId;
us = u.usage;
}
end = u.eventId;
}
if(start != 0)
callback(start, end, us);
}
void addStructuredObjects(RDTreeWidgetItem *parent, const StructuredObjectList &objs,
bool parentIsArray)
{
for(const SDObject *obj : objs)
{
if(obj->type.flags & SDTypeFlags::Hidden)
continue;
QVariant param;
if(parentIsArray)
param = QFormatStr("[%1]").arg(parent->childCount());
else
param = obj->name;
RDTreeWidgetItem *item = new RDTreeWidgetItem({param, QString()});
// we don't identify via the type name as many types could be serialised as a ResourceId -
// e.g. ID3D11Resource* or ID3D11Buffer* which would be the actual typename. We want to preserve
// that for the best raw structured data representation instead of flattening those out to just
// "ResourceId", and we also don't want to store two types ('fake' and 'real'), so instead we
// check the custom string.
if((obj->type.flags & SDTypeFlags::HasCustomString) &&
!strncmp(obj->data.str.c_str(), "ResourceId", 10))
{
ResourceId id;
static_assert(sizeof(id) == sizeof(obj->data.basic.u), "ResourceId is no longer uint64_t!");
memcpy(&id, &obj->data.basic.u, sizeof(id));
param = id;
}
else if(obj->type.flags & SDTypeFlags::NullString)
{
param = lit("NULL");
}
else if(obj->type.flags & SDTypeFlags::HasCustomString)
{
param = obj->data.str;
}
else
{
switch(obj->type.basetype)
{
case SDBasic::Chunk:
case SDBasic::Struct:
param = QFormatStr("%1()").arg(obj->type.name);
addStructuredObjects(item, obj->data.children, false);
break;
case SDBasic::Array:
param = QFormatStr("%1[]").arg(obj->type.name);
addStructuredObjects(item, obj->data.children, true);
break;
case SDBasic::Null: param = lit("NULL"); break;
case SDBasic::Buffer: param = lit("(%1 bytes)").arg(obj->type.byteSize); break;
case SDBasic::String: param = obj->data.str; break;
case SDBasic::Enum:
case SDBasic::UnsignedInteger: param = Formatter::Format(obj->data.basic.u); break;
case SDBasic::SignedInteger: param = Formatter::Format(obj->data.basic.i); break;
case SDBasic::Float: param = Formatter::Format(obj->data.basic.d); break;
case SDBasic::Boolean: param = (obj->data.basic.b ? lit("True") : lit("False")); break;
case SDBasic::Character: param = QString(QLatin1Char(obj->data.basic.c)); break;
}
}
item->setText(1, param);
parent->addChild(item);
}
}
bool SaveToJSON(QVariantMap &data, QIODevice &f, const char *magicIdentifier, uint32_t magicVersion)
{
// marker that this data is valid
if(magicIdentifier)
data[QString::fromLatin1(magicIdentifier)] = magicVersion;
QJsonDocument doc = QJsonDocument::fromVariant(data);
if(doc.isEmpty() || doc.isNull())
{
qCritical() << "Failed to convert data to JSON document";
return false;
}
QByteArray jsontext = doc.toJson(QJsonDocument::Indented);
qint64 ret = f.write(jsontext);
if(ret != jsontext.size())
{
qCritical() << "Failed to write JSON data: " << ret << " " << f.errorString();
return false;
}
return true;
}
bool LoadFromJSON(QVariantMap &data, QIODevice &f, const char *magicIdentifier, uint32_t magicVersion)
{
QByteArray json = f.readAll();
if(json.isEmpty())
{
qCritical() << "Read invalid empty JSON data from file " << f.errorString();
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(json);
if(doc.isEmpty() || doc.isNull())
{
qCritical() << "Failed to convert file to JSON document";
return false;
}
data = doc.toVariant().toMap();
QString ident = QString::fromLatin1(magicIdentifier);
if(data.isEmpty() || !data.contains(ident))
{
qCritical() << "Converted config data is invalid or unrecognised";
return false;
}
if(data[ident].toUInt() != magicVersion)
{
qCritical() << "Converted config data is not the right version";
return false;
}
return true;
}
QString VariantToJSON(const QVariantMap &data)
{
return QString::fromUtf8(QJsonDocument::fromVariant(data).toJson(QJsonDocument::Indented));
}
QVariantMap JSONToVariant(const QString &json)
{
return QJsonDocument::fromJson(json.toUtf8()).toVariant().toMap();
}
int GUIInvoke::methodIndex = -1;
void GUIInvoke::init()
{
GUIInvoke *invoke = new GUIInvoke();
methodIndex = invoke->metaObject()->indexOfMethod(QMetaObject::normalizedSignature("doInvoke()"));
}
void GUIInvoke::call(const std::function<void()> &f)
{
if(onUIThread())
{
f();
return;
}
defer(f);
}
void GUIInvoke::defer(const std::function<void()> &f)
{
GUIInvoke *invoke = new GUIInvoke(f);
invoke->moveToThread(qApp->thread());
invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::QueuedConnection);
}
void GUIInvoke::blockcall(const std::function<void()> &f)
{
if(onUIThread())
{
f();
return;
}
GUIInvoke *invoke = new GUIInvoke(f);
invoke->moveToThread(qApp->thread());
invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::BlockingQueuedConnection);
}
bool GUIInvoke::onUIThread()
{
return qApp->thread() == QThread::currentThread();
}
const QMessageBox::StandardButtons RDDialog::YesNoCancel =
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
void RDDialog::show(QMenu *menu, QPoint pos)
{
// menus aren't always visible immediately, so we need to listen for aboutToHide to exit the event
// loop. As a safety precaution because I don't trust the damn signals, if we loop for over a
// second then we'll quit as soon as the menu is not visible
volatile bool menuHiding = false;
auto connection =
QObject::connect(menu, &QMenu::aboutToHide, [&menuHiding]() { menuHiding = true; });
menu->setWindowModality(Qt::ApplicationModal);
menu->popup(pos);
QElapsedTimer elapsed;
elapsed.start();
QEventLoop loop;
for(;;)
{
// stop processing once aboutToHide has been signalled
if(menuHiding)
break;
// stop processing if 1s has passed and the menu isn't visible anymore.
if(elapsed.hasExpired(1000) && !menu->isVisible())
break;
loop.processEvents(QEventLoop::WaitForMoreEvents);
QCoreApplication::sendPostedEvents();
}
QObject::disconnect(connection);
}
int RDDialog::show(QDialog *dialog)
{
dialog->setWindowModality(Qt::ApplicationModal);
dialog->show();
QEventLoop loop;
while(dialog->isVisible())
{
loop.processEvents(QEventLoop::WaitForMoreEvents);
QCoreApplication::sendPostedEvents();
}
return dialog->result();
}
QMessageBox::StandardButton RDDialog::messageBox(QMessageBox::Icon icon, QWidget *parent,
const QString &title, const QString &text,
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
QMessageBox::StandardButton ret = defaultButton;
// if we're already on the right thread, this boils down to a function call
GUIInvoke::blockcall([&]() {
QMessageBox mb(icon, title, text, buttons, parent);
mb.setDefaultButton(defaultButton);
show(&mb);
ret = mb.standardButton(mb.clickedButton());
});
return ret;
}
QMessageBox::StandardButton RDDialog::messageBoxChecked(QMessageBox::Icon icon, QWidget *parent,
const QString &title, const QString &text,
QCheckBox *checkBox, bool &checked,
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
QMessageBox::StandardButton ret = defaultButton;
// if we're already on the right thread, this boils down to a function call
GUIInvoke::blockcall([&]() {
QMessageBox mb(icon, title, text, buttons, parent);
mb.setDefaultButton(defaultButton);
mb.setCheckBox(checkBox);
show(&mb);
checked = mb.checkBox()->isChecked();
ret = mb.standardButton(mb.clickedButton());
});
return ret;
}
QString RDDialog::getExistingDirectory(QWidget *parent, const QString &caption, const QString &dir,
QFileDialog::Options options)
{
QFileDialog fd(parent, caption, dir, QString());
fd.setAcceptMode(QFileDialog::AcceptOpen);
fd.setFileMode(QFileDialog::DirectoryOnly);
fd.setOptions(options);
show(&fd);
if(fd.result() == QFileDialog::Accepted)
{
QStringList files = fd.selectedFiles();
if(!files.isEmpty())
return files[0];
}
return QString();
}
QString RDDialog::getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
const QString &filter, QString *selectedFilter,
QFileDialog::Options options)
{
QFileDialog fd(parent, caption, dir, filter);
fd.setFileMode(QFileDialog::ExistingFile);
fd.setAcceptMode(QFileDialog::AcceptOpen);
fd.setOptions(options);
show(&fd);
if(fd.result() == QFileDialog::Accepted)
{
if(selectedFilter)
*selectedFilter = fd.selectedNameFilter();
QStringList files = fd.selectedFiles();
if(!files.isEmpty())
return files[0];
}
return QString();
}
QString RDDialog::getExecutableFileName(QWidget *parent, const QString &caption, const QString &dir,
QFileDialog::Options options)
{
QString filter;
#if defined(Q_OS_WIN32)
// can't filter by executable bit on windows, but we have extensions
filter = QApplication::translate("RDDialog", "Executables (*.exe);;All Files (*)");
#endif
QFileDialog fd(parent, caption, dir, filter);
fd.setOptions(options);
fd.setAcceptMode(QFileDialog::AcceptOpen);
fd.setFileMode(QFileDialog::ExistingFile);
{
QFileFilterModel *fileProxy = new QFileFilterModel(parent);
fileProxy->setRequirePermissions(QDir::Executable);
fd.setProxyModel(fileProxy);
}
show(&fd);
if(fd.result() == QFileDialog::Accepted)
{
QStringList files = fd.selectedFiles();
if(!files.isEmpty())
return files[0];
}
return QString();
}
static QStringList getDefaultSuffixesFromFilter(const QString &filter)
{
// capture the first suffix found and discard the rest
static const QRegularExpression regex(lit("\\*\\.([\\w.]+).*"));
QStringList suffixes;
for(const QString &s : filter.split(lit(";;")))
{
suffixes << regex.match(s).captured(1);
}
return suffixes;
}
QString RDDialog::getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
const QString &filter, QString *selectedFilter,
QFileDialog::Options options)
{
QFileDialog fd(parent, caption, dir, filter);
fd.setAcceptMode(QFileDialog::AcceptSave);
fd.setOptions(options);
const QStringList &defaultSuffixes = getDefaultSuffixesFromFilter(filter);
if(!defaultSuffixes.isEmpty())
fd.setDefaultSuffix(defaultSuffixes.first());
QObject::connect(&fd, &QFileDialog::filterSelected, [&](const QString &filter) {
int i = fd.nameFilters().indexOf(filter);
fd.setDefaultSuffix(defaultSuffixes.value(i));
});
show(&fd);
if(fd.result() == QFileDialog::Accepted)
{
if(selectedFilter)
*selectedFilter = fd.selectedNameFilter();
QStringList files = fd.selectedFiles();
if(!files.isEmpty())
return files[0];
}
return QString();
}
bool QFileFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
QFileSystemModel *fs = qobject_cast<QFileSystemModel *>(sourceModel());
if(!fs)
{
qCritical() << "Expected a QFileSystemModel as the source model!";
return true;
}
if(fs->isDir(idx))
return true;
QFile::Permissions permissions =
(QFile::Permissions)sourceModel()->data(idx, QFileSystemModel::FilePermissions).toInt();
if((m_requireMask & QDir::Readable) && !(permissions & QFile::ReadUser))
return false;
if((m_requireMask & QDir::Writable) && !(permissions & QFile::WriteUser))
return false;
if((m_requireMask & QDir::Executable) && !(permissions & QFile::ExeUser))
return false;
if((m_excludeMask & QDir::Readable) && (permissions & QFile::ReadUser))
return false;
if((m_excludeMask & QDir::Writable) && (permissions & QFile::WriteUser))
return false;
if((m_excludeMask & QDir::Executable) && (permissions & QFile::ExeUser))
return false;
return true;
}
void addGridLines(QGridLayout *grid, QColor gridColor)
{
QString style =
QFormatStr("border: solid #%1%2%3; border-bottom-width: 1px; border-right-width: 1px;")
.arg(gridColor.red(), 2, 16, QLatin1Char('0'))
.arg(gridColor.green(), 2, 16, QLatin1Char('0'))
.arg(gridColor.blue(), 2, 16, QLatin1Char('0'));
for(int y = 0; y < grid->rowCount(); y++)
{
for(int x = 0; x < grid->columnCount(); x++)
{
QLayoutItem *item = grid->itemAtPosition(y, x);
if(item == NULL)
continue;
QWidget *w = item->widget();
if(w == NULL)
continue;
QString cellStyle = style;
if(x == 0)
cellStyle += lit("border-left-width: 1px;");
if(y == 0)
cellStyle += lit("border-top-width: 1px;");
w->setStyleSheet(cellStyle);
}
}
}
int Formatter::m_minFigures = 2, Formatter::m_maxFigures = 5, Formatter::m_expNegCutoff = 5,
Formatter::m_expPosCutoff = 7;
double Formatter::m_expNegValue = 0.00001; // 10^(-5)
double Formatter::m_expPosValue = 10000000.0; // 10^7
QFont *Formatter::m_Font = NULL;
QColor Formatter::m_DarkChecker, Formatter::m_LightChecker;
void Formatter::setParams(const PersistantConfig &config)
{
m_minFigures = qMax(0, config.Formatter_MinFigures);
m_maxFigures = qMax(2, config.Formatter_MaxFigures);
m_expNegCutoff = qMax(0, config.Formatter_NegExp);
m_expPosCutoff = qMax(0, config.Formatter_PosExp);
m_expNegValue = qPow(10.0, -config.Formatter_NegExp);
m_expPosValue = qPow(10.0, config.Formatter_PosExp);
if(!m_Font)
m_Font = new QFont();
*m_Font =
config.Font_PreferMonospaced ? QFontDatabase::systemFont(QFontDatabase::FixedFont) : QFont();
m_DarkChecker = QApplication::palette().color(QPalette::Mid);
m_LightChecker = m_DarkChecker.lighter(150);
RENDERDOC_SetColors(m_DarkChecker, m_LightChecker, IsDarkTheme());
}
void Formatter::shutdown()
{
delete m_Font;
}
QString Formatter::Format(double f, bool)
{
if(f != 0.0 && (qAbs(f) < m_expNegValue || qAbs(f) > m_expPosValue))
return QFormatStr("%1").arg(f, -m_minFigures, 'E', m_maxFigures);
QString ret = QFormatStr("%1").arg(f, 0, 'f', m_maxFigures);
// trim excess trailing 0s
int decimal = ret.lastIndexOf(QLatin1Char('.'));
if(decimal > 0)
{
decimal += m_minFigures;
const int len = ret.count();
int remove = 0;
while(len - remove - 1 > decimal && ret.at(len - remove - 1) == QLatin1Char('0'))
remove++;
if(remove > 0)
ret.chop(remove);
}
return ret;
}
class RDProgressDialog : public QProgressDialog
{
public:
RDProgressDialog(const QString &labelText, QWidget *parent)
// we add 1 so that the progress value never hits maximum until we are actually finished
: QProgressDialog(labelText, QString(), 0, maxProgress + 1, parent),
m_Label(this)
{
setWindowTitle(tr("Please Wait"));
setWindowFlags(Qt::CustomizeWindowHint | Qt::Dialog | Qt::WindowTitleHint);
setWindowIcon(QIcon());
setMinimumSize(QSize(250, 0));
setMaximumSize(QSize(250, 10000));
setCancelButton(NULL);
setMinimumDuration(0);
setWindowModality(Qt::ApplicationModal);
setValue(0);
m_Label.setText(labelText);
m_Label.setAlignment(Qt::AlignCenter);
m_Label.setWordWrap(true);
setLabel(&m_Label);
}
void setPercentage(float percent) { setValue(int(maxProgress * percent)); }
void setInfinite(bool infinite)
{
if(infinite)
{
setMinimum(0);
setMaximum(0);
setValue(0);
}
else
{
setMinimum(0);
setMaximum(maxProgress + 1);
setValue(0);
}
}
void closeAndReset()
{
setValue(maxProgress);
hide();
reset();
}
protected:
void keyPressEvent(QKeyEvent *e) override
{
if(e->key() == Qt::Key_Escape)
return;
QProgressDialog::keyPressEvent(e);
}
QLabel m_Label;
static const int maxProgress = 1000;
};
#if defined(Q_OS_WIN32)
#include <windows.h>
#include <shellapi.h>
typedef LSTATUS(APIENTRY *PFN_RegCreateKeyExA)(HKEY hKey, LPCSTR lpSubKey, DWORD Reserved,
LPSTR lpClass, DWORD dwOptions, REGSAM samDesired,
CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes,
PHKEY phkResult, LPDWORD lpdwDisposition);
typedef LSTATUS(APIENTRY *PFN_RegCloseKey)(HKEY hKey);
#else
#include <unistd.h>
#endif
bool IsRunningAsAdmin()
{
#if defined(Q_OS_WIN32)
// try to open HKLM\Software for write.
HKEY key = NULL;
// access dynamically to get around the pain of trying to link to extra window libs in qt
HMODULE mod = LoadLibraryA("advapi32.dll");
if(mod == NULL)
return false;
PFN_RegCreateKeyExA create = (PFN_RegCreateKeyExA)GetProcAddress(mod, "RegCreateKeyExA");
PFN_RegCloseKey close = (PFN_RegCloseKey)GetProcAddress(mod, "RegCloseKey");
LSTATUS ret = ERROR_PROC_NOT_FOUND;
if(create && close)
{
ret = create(HKEY_LOCAL_MACHINE, "SOFTWARE", 0, NULL, 0, KEY_READ | KEY_WRITE, NULL, &key, NULL);
if(key)
close(key);
}
FreeLibrary(mod);
return (ret == ERROR_SUCCESS);
#else
// this isn't ideal, we should check something else since a user may have permissions to do what
// we want to do
return geteuid() == 0;
#endif
}
bool RunProcessAsAdmin(const QString &fullExecutablePath, const QStringList &params,
std::function<void()> finishedCallback)
{
#if defined(Q_OS_WIN32)
std::wstring wideExe = QDir::toNativeSeparators(fullExecutablePath).toStdWString();
std::wstring wideParams = params.join(QLatin1Char(' ')).toStdWString();
SHELLEXECUTEINFOW info = {};
info.cbSize = sizeof(info);
info.fMask = SEE_MASK_NOCLOSEPROCESS;
info.lpVerb = L"runas";
info.lpFile = wideExe.c_str();
info.lpParameters = wideParams.c_str();
info.nShow = SW_SHOWNORMAL;
ShellExecuteExW(&info);
if((uintptr_t)info.hInstApp > 32 && info.hProcess != NULL)
{
if(finishedCallback)
{
HANDLE h = info.hProcess;
// do the wait on another thread
LambdaThread *thread = new LambdaThread([h, finishedCallback]() {
WaitForSingleObject(h, 30000);
CloseHandle(h);
GUIInvoke::call(finishedCallback);
});
thread->selfDelete(true);
thread->start();
}
else
{
CloseHandle(info.hProcess);
}
return true;
}
return false;
#else
// try to find a way to run the application elevated.
const QString graphicalSudo[] = {
lit("pkexec"), lit("kdesudo"), lit("gksudo"), lit("beesu"),
};
// if none of the graphical options, then look for sudo and either
const QString termEmulator[] = {
lit("x-terminal-emulator"), lit("gnome-terminal"), lit("knosole"), lit("xterm"),
};
for(const QString &sudo : graphicalSudo)
{
QString inPath = QStandardPaths::findExecutable(sudo);
// can't find in path
if(inPath.isEmpty())
continue;
QProcess *process = new QProcess;
QStringList sudoParams;
sudoParams << fullExecutablePath;
for(const QString &p : params)
sudoParams << p;
qInfo() << "Running" << sudo << "with params" << sudoParams;
// run with sudo
process->start(sudo, sudoParams);
// when the process exits, call the callback and delete
QObject::connect(process, OverloadedSlot<int>::of(&QProcess::finished),
[process, finishedCallback](int exitCode) {
process->deleteLater();
GUIInvoke::call(finishedCallback);
});
return true;
}
QString sudo = QStandardPaths::findExecutable(lit("sudo"));
if(sudo.isEmpty())
{
qCritical() << "Couldn't find graphical or terminal sudo program!\n"
<< "Please run " << fullExecutablePath << "with args" << params << "manually.";
return false;
}
for(const QString &term : termEmulator)
{
QString inPath = QStandardPaths::findExecutable(term);
// can't find in path
if(inPath.isEmpty())
continue;
QProcess *process = new QProcess;
// run terminal sudo with emulator
QStringList termParams;
termParams
<< lit("-e")
<< lit("bash -c 'sudo %1 %2'").arg(fullExecutablePath).arg(params.join(QLatin1Char(' ')));
process->start(term, termParams);
// when the process exits, call the callback and delete
QObject::connect(process, OverloadedSlot<int>::of(&QProcess::finished),
[process, finishedCallback](int exitCode) {
process->deleteLater();
GUIInvoke::call(finishedCallback);
});
return true;
}
qCritical() << "Couldn't find graphical or terminal emulator to launch sudo.\n"
<< "Please run " << fullExecutablePath << "with args" << params << "manually.";
return false;
#endif
}
QStringList ParseArgsList(const QString &args)
{
QStringList ret;
if(args.isEmpty())
return ret;
// on windows just use the function provided by the system
#if defined(Q_OS_WIN32)
std::wstring wargs = args.toStdWString();
int argc = 0;
wchar_t **argv = CommandLineToArgvW(wargs.c_str(), &argc);
for(int i = 0; i < argc; i++)
ret << QString::fromWCharArray(argv[i]);
LocalFree(argv);
#else
std::string argString = args.toStdString();
// perform some kind of sane parsing
bool dquot = false, squot = false; // are we inside ''s or ""s
// current character
char *c = &argString[0];
// current argument we're building
std::string a;
while(*c)
{
if(!dquot && !squot && (*c == ' ' || *c == '\t'))
{
if(!a.empty())
ret << QString::fromStdString(a);
a = "";
}
else if(!dquot && *c == '"')
{
dquot = true;
}
else if(!squot && *c == '\'')
{
squot = true;
}
else if(dquot && *c == '"')
{
dquot = false;
}
else if(squot && *c == '\'')
{
squot = false;
}
else if(squot)
{
// single quotes don't escape, just copy literally until we leave single quote mode
a.push_back(*c);
}
else if(dquot)
{
// handle escaping
if(*c == '\\')
{
c++;
if(*c)
{
a.push_back(*c);
}
else
{
qCritical() << "Malformed args list:" << args;
return ret;
}
}
else
{
a.push_back(*c);
}
}
else
{
a.push_back(*c);
}
c++;
}
// if we were building an argument when we hit the end of the string
if(!a.empty())
ret << QString::fromStdString(a);
#endif
return ret;
}
void ShowProgressDialog(QWidget *window, const QString &labelText, ProgressFinishedMethod finished,
ProgressUpdateMethod update)
{
RDProgressDialog dialog(labelText, window);
// if we don't have an update function, set the progress display to be 'infinite spinner'
dialog.setInfinite(!update);
QSemaphore tickerSemaphore(1);
// start a lambda thread to tick our functions and close the progress dialog when we're done.
LambdaThread progressTickerThread([finished, update, &dialog, &tickerSemaphore]() {
while(tickerSemaphore.available())
{
QThread::msleep(30);
if(update)
GUIInvoke::call([update, &dialog]() { dialog.setPercentage(update()); });
GUIInvoke::call([finished, &tickerSemaphore]() {
if(finished())
tickerSemaphore.tryAcquire();
});
}
GUIInvoke::call([&dialog]() { dialog.closeAndReset(); });
});
progressTickerThread.start();
// show the dialog
RDDialog::show(&dialog);
// signal the thread to exit if somehow we got here without it finishing, then wait for it thread
// to clean itself up
tickerSemaphore.tryAcquire();
progressTickerThread.wait();
}
void UpdateTransferProgress(qint64 xfer, qint64 total, QElapsedTimer *timer,
QProgressBar *progressBar, QLabel *progressLabel, QString progressText)
{
if(xfer >= total)
{
progressBar->setMaximum(10000);
progressBar->setValue(10000);
return;
}
if(total <= 0)
{
progressBar->setMaximum(10000);
progressBar->setValue(0);
return;
}
progressBar->setMaximum(10000);
progressBar->setValue(int(10000.0 * (double(xfer) / double(total))));
double xferMB = double(xfer) / 1000000.0;
double totalMB = double(total) / 1000000.0;
double secondsElapsed = double(timer->nsecsElapsed()) * 1.0e-9;
double speedMBS = xferMB / secondsElapsed;
qulonglong secondsRemaining = qulonglong(double(totalMB - xferMB) / speedMBS);
if(secondsElapsed > 1.0)
{
QString remainString;
qulonglong minutesRemaining = (secondsRemaining / 60) % 60;
qulonglong hoursRemaining = (secondsRemaining / 3600);
secondsRemaining %= 60;
if(hoursRemaining > 0)
remainString = QFormatStr("%1:%2:%3")
.arg(hoursRemaining, 2, 10, QLatin1Char('0'))
.arg(minutesRemaining, 2, 10, QLatin1Char('0'))
.arg(secondsRemaining, 2, 10, QLatin1Char('0'));
else if(minutesRemaining > 0)
remainString = QFormatStr("%1:%2")
.arg(minutesRemaining, 2, 10, QLatin1Char('0'))
.arg(secondsRemaining, 2, 10, QLatin1Char('0'));
else
remainString = QApplication::translate("qrenderdoc", "%1 seconds").arg(secondsRemaining);
double speed = speedMBS;
bool MBs = true;
if(speedMBS < 1)
{
MBs = false;
speed *= 1000;
}
progressLabel->setText(
QApplication::translate("qrenderdoc", "%1\n%2 MB / %3 MB. %4 remaining (%5 %6)")
.arg(progressText)
.arg(xferMB, 0, 'f', 2)
.arg(totalMB, 0, 'f', 2)
.arg(remainString)
.arg(speed, 0, 'f', 2)
.arg(MBs ? lit("MB/s") : lit("KB/s")));
}
}
void setEnabledMultiple(const QList<QWidget *> &widgets, bool enabled)
{
for(QWidget *w : widgets)
w->setEnabled(enabled);
}
QString GetSystemUsername()
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString username = env.value(lit("USER"));
if(username == QString())
username = env.value(lit("USERNAME"));
if(username == QString())
username = lit("Unknown_User");
return username;
}
bool IsDarkTheme()
{
float baseLum = getLuminance(QApplication::palette().color(QPalette::Base));
float textLum = getLuminance(QApplication::palette().color(QPalette::Text));
// if the base is dark than the text, then it's a light-on-dark theme (aka dark theme)
return (baseLum < textLum);
}
float getLuminance(const QColor &col)
{
return (float)(0.2126 * qPow(col.redF(), 2.2) + 0.7152 * qPow(col.greenF(), 2.2) +
0.0722 * qPow(col.blueF(), 2.2));
}
QColor contrastingColor(const QColor &col, const QColor &defaultCol)
{
float backLum = getLuminance(col);
float textLum = getLuminance(defaultCol);
bool backDark = backLum < 0.2f;
bool textDark = textLum < 0.2f;
// if they're contrasting, use the text colour desired
if(backDark != textDark)
return defaultCol;
// otherwise pick a contrasting colour
if(backDark)
return QColor(Qt::white);
else
return QColor(Qt::black);
}