text: /******************************************************************************
   * The MIT License (MIT)
   *
   * Copyright (c) 2019-2020 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 <QCloseEvent>
  #include <QCollator>
  #include <QDesktopServices>
  #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 <QTextBoundaryFinder>
  #include <QTextDocument>
  #include <QtMath>
  #include "Code/Resources.h"
  #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 <>
  rdcstr DoStringise(const uint32_t &el)
  {
    return QString::number(el);
  }
  
  // these ones we do by hand as it requires formatting
  template <>
  rdcstr DoStringise(const ResourceId &el)
  {
    uint64_t num;
    memcpy(&num, &el, sizeof(num));
    return lit("ResourceId::%1").arg(num);
  }
  
  QMap<QPair<ResourceId, uint32_t>, uint32_t> PointerTypeRegistry::typeMapping;
  rdcarray<ShaderVariableType> PointerTypeRegistry::typeDescriptions;
  
  static const uint32_t TypeIDBit = 0x80000000;
  
  void PointerTypeRegistry::Init()
  {
    typeMapping.clear();
  
    // type ID 0 is reserved as a NULL/empty descriptor
    typeDescriptions.resize(1);
    typeDescriptions[0].descriptor.name = "<Unknown>";
  }
  
  uint32_t PointerTypeRegistry::GetTypeID(ResourceId shader, uint32_t pointerTypeId)
  {
    return typeMapping[qMakePair(shader, pointerTypeId)];
  }
  
  uint32_t PointerTypeRegistry::GetTypeID(const ShaderVariableType &structDef)
  {
    // see if the type is already registered, return its existing ID
    for(uint32_t i = 1; i < typeDescriptions.size(); i++)
    {
      if(structDef == typeDescriptions[i])
        return TypeIDBit | i;
    }
  
    uint32_t id = TypeIDBit | (uint32_t)typeDescriptions.size();
  
    // otherwise register the new type
    typeDescriptions.push_back(structDef);
    typeMapping[qMakePair(ResourceId(), id)] = id;
  
    return id;
  }
  
  const ShaderVariableType &PointerTypeRegistry::GetTypeDescriptor(uint32_t typeId)
  {
    return typeDescriptions[typeId & ~TypeIDBit];
  }
  
  void PointerTypeRegistry::CacheSubTypes(const ShaderReflection *reflection,
                                          ShaderVariableType &structDef)
  {
    if((structDef.descriptor.pointerTypeID & TypeIDBit) == 0)
      structDef.descriptor.pointerTypeID =
          PointerTypeRegistry::GetTypeID(reflection->pointerTypes[structDef.descriptor.pointerTypeID]);
  
    for(ShaderConstant &member : structDef.members)
      CacheSubTypes(reflection, member.type);
  }
  
  void PointerTypeRegistry::CacheShader(const ShaderReflection *reflection)
  {
    // nothing to do if there are no pointer types
    if(reflection->pointerTypes.isEmpty())
      return;
  
    // check if we've already cached this shader (we know there's at least one pointer type)
    if(typeMapping.contains(qMakePair(reflection->resourceId, 0)))
      return;
  
    for(uint32_t i = 0; i < reflection->pointerTypes.size(); i++)
    {
      ShaderVariableType typeDesc = reflection->pointerTypes[i];
  
      // first recursively cache all subtypes needed by the root struct types
      CacheSubTypes(reflection, typeDesc);
  
      // then look up the Type ID for this struct
      typeMapping[qMakePair(reflection->resourceId, i)] = GetTypeID(typeDesc);
    }
  }
  
  template <>
  rdcstr DoStringise(const PointerVal &el)
  {
    if(el.pointerTypeID != ~0U)
    {
      uint32_t ptrTypeId = PointerTypeRegistry::GetTypeID(el.shader, el.pointerTypeID);
  
      return QFormatStr("GPUAddress::%1::%2").arg(el.pointer).arg(ptrTypeId);
    }
    else
    {
      return QFormatStr("GPUAddress::%1").arg(el.pointer);
    }
  }
  
  QString GetTruncatedResourceName(ICaptureContext &ctx, ResourceId id)
  {
    QString name = ctx.GetResourceName(id);
    if(name.length() > 64)
    {
      QTextBoundaryFinder boundaries(QTextBoundaryFinder::Grapheme, name.data(), name.length());
      boundaries.setPosition(64);
      if(!boundaries.isAtBoundary())
        boundaries.toPreviousBoundary();
      int pos = boundaries.position();
      name.resize(pos);
      name += lit("...");
    }
  
    return name;
  }
  
  // this is an opaque struct that contains the data to render, hit-test, etc for some text that
  // contains links to resources. It will update and cache the names of the resources.
  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;
  
      bool highdpi = widget->devicePixelRatioF() > 1.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 = GetTruncatedResourceName(ctx, v.value<ResourceId>()).toHtmlEscaped();
          html += lit("<td valign=\"middle\"><b>%1</b></td>"
                      "<td><img width=\"16\" src=':/link%3.png'></td>")
                      .arg(resname)
                      .arg(highdpi ? lit("@2x") : QString());
          text += resname;
  
          // these generate two blocks (one for each cell)
          fragmentIndexFromBlockIndex.push_back(i);
          fragmentIndexFromBlockIndex.push_back(i);
        }
        else
        {
          html += lit("<td valign=\"middle\">%1</td>").arg(v.toString().toHtmlEscaped());
          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);
      doc.setDefaultFont(widget->font());
  
      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]);
  
      doc.setTextWidth(-1);
      idealWidth = doc.idealWidth();
      doc.setTextWidth(10000);
    }
  };
  
  // we use QSharedPointer to refer to the text since the lifetime management of these objects would
  // get quite complicated. There's not necessarily an obvious QObject parent to assign to if the text
  // is being initialised before being assigned to a widget and we want the most seamless interface we
  // can get.
  typedef QSharedPointer<RichResourceText> RichResourceTextPtr;
  
  Q_DECLARE_METATYPE(RichResourceTextPtr);
  
  void GPUAddress::cacheAddress(const QWidget *widget)
  {
    if(!ctxptr)
      ctxptr = getCaptureContext(widget);
  
    // bail out if we don't have a context
    if(!ctxptr)
      return;
  
    // bail if we're already cached
    if(base != ResourceId())
      return;
  
    // find the first matching buffer
    for(const BufferDescription &b : ctxptr->GetBuffers())
    {
      if(b.gpuAddress && b.gpuAddress <= val.pointer && b.gpuAddress + b.length > val.pointer)
      {
        base = b.resourceId;
        offset = val.pointer - b.gpuAddress;
        return;
      }
    }
  }
  
  // for the same reason as above we use a shared pointer for GPU addresses too. This ensures the
  // cached data doesn't keep getting re-cached in copies.
  typedef QSharedPointer<GPUAddress> GPUAddressPtr;
  
  Q_DECLARE_METATYPE(GPUAddressPtr);
  
  QString ResIdTextToString(RichResourceTextPtr ptr)
  {
    return ptr->text;
  }
  
  QString ResIdToString(ResourceId ptr)
  {
    return ToQStr(ptr);
  }
  
  QString GPUAddressToString(GPUAddressPtr addr)
  {
    if(addr->base != ResourceId())
      return QFormatStr("%1+%2").arg(ToQStr(addr->base)).arg(addr->offset);
    else
      return QFormatStr("0x%1").arg(addr->val.pointer, 0, 16);
  }
  
  void RegisterMetatypeConversions()
  {
    QMetaType::registerConverter<RichResourceTextPtr, QString>(&ResIdTextToString);
    QMetaType::registerConverter<ResourceId, QString>(&ResIdToString);
    QMetaType::registerConverter<GPUAddressPtr, QString>(&GPUAddressToString);
  }
  
  void RichResourceTextInitialise(QVariant &var)
  {
    // we only upconvert from strings, any other type with a string representation is not expected to
    // contain ResourceIds. In particular if the variant is already a ResourceId we can return.
    if(GetVariantMetatype(var) != QMetaType::QString)
      return;
  
    // we trim the string because that will happen naturally when rendering as HTML, and it makes it
    // easier to detect strings where the only contents are ResourceId text.
    QString text = var.toString().trimmed();
  
    // do a simple string search first before using regular expressions
    if(!text.contains(lit("ResourceId::")) && !text.contains(lit("GPUAddress::")))
      return;
  
    // two forms: GPUAddress::012345        - typeless
    //            GPUAddress::012345::991   - using type 991 from PointerTypeRegistry
    static QRegularExpression addrRE(lit("GPUAddress::([0-9]*)(::([0-9]*))?"));
  
    QRegularExpressionMatch match = addrRE.match(text);
  
    if(match.hasMatch())
    {
      // don't support mixed text & addresses. Only do the replacement if we matched the whole string
      if(match.capturedStart(0) == 0 && match.capturedLength(0) == text.length())
      {
        GPUAddressPtr addr(new GPUAddress);
        addr->val.pointer = match.captured(1).toULongLong();
  
        // we deliberately set this to ResourceId() to indicate that we're using an ID from the
        // registry, not a shader-relative index
        addr->val.shader = ResourceId();
        addr->val.pointerTypeID = match.captured(3).toULong();
  
        var = QVariant::fromValue(addr);
        return;
      }
  
      return;
    }
  
    // use regexp to split up into fragments of text and resourceid. The resourceid is then
    // formatted on the fly in RichResourceText::cacheDocument
    static QRegularExpression resRE(lit("(ResourceId::)([0-9]*)"));
  
    match = resRE.match(text);
  
    if(match.hasMatch())
    {
      // if the match is the whole string, this is just a plain ResourceId on its own, so make that
      // the variant without being rich resource text, so we can process it faster.
      if(match.capturedStart(0) == 0 && match.capturedLength(0) == text.length())
      {
        qulonglong idnum = match.captured(2).toULongLong();
        ResourceId id;
        memcpy(&id, &idnum, sizeof(id));
  
        var = id;
        return;
      }
  
      RichResourceTextPtr linkedText(new RichResourceText);
  
      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 = resRE.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>() ||
           var.userType() == qMetaTypeId<GPUAddressPtr>() ||
           var.userType() == qMetaTypeId<ResourceId>();
  }
  
  // I'm not sure if this should come from the style or not - the QTextDocument handles this in
  // the rich text case.
  static const int RichResourceTextMargin = 2;
  
  void RichResourceTextPaint(const QWidget *owner, QPainter *painter, QRect rect, QFont font,
                             QPalette palette, bool mouseOver, QPoint mousePos, const QVariant &var)
  {
    // special case handling for ResourceId/GPUAddress on its own
    if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
    {
      painter->save();
  
      QFont f = painter->font();
      f.setBold(true);
      painter->setFont(f);
  
      static const int margin = RichResourceTextMargin;
  
      rect.adjust(margin, 0, -margin * 2, 0);
  
      QString name;
  
      bool valid = false;
  
      if(var.userType() == qMetaTypeId<ResourceId>())
      {
        ICaptureContext *ctxptr = getCaptureContext(owner);
  
        ResourceId id = var.value<ResourceId>();
  
        valid = (id != ResourceId());
  
        if(ctxptr)
          name = GetTruncatedResourceName(*ctxptr, id);
        else
          name = ToQStr(id);
      }
      else
      {
        GPUAddressPtr ptr = var.value<GPUAddressPtr>();
  
        ptr->cacheAddress(owner);
  
        valid = (ptr->val.pointer != 0);
  
        if(valid)
        {
          if(ptr->base != ResourceId())
          {
            name =
                QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
          }
          else
          {
            name = QFormatStr("Unknown 0x%1").arg(ptr->val.pointer, 16, 16, QLatin1Char('0'));
            valid = false;
          }
        }
        else
        {
          name = lit("NULL");
        }
      }
  
      painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
  
      QRect textRect =
          painter->fontMetrics().boundingRect(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
  
      const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
  
      painter->setClipRect(rect);
  
      textRect.setLeft(rect.left());
      textRect.setWidth(textRect.width() + margin + px.width());
      textRect.setHeight(qMax(textRect.height(), px.height()));
  
      QPoint pos;
      pos.setX(textRect.right() - px.width() + 1);
      pos.setY(textRect.center().y() - px.height() / 2);
  
      painter->drawPixmap(pos, px, px.rect());
  
      if(mouseOver && textRect.contains(mousePos) && valid)
      {
        int underline_y = textRect.bottom() - margin;
  
        painter->setPen(QPen(palette.brush(QPalette::WindowText), 1.0));
        painter->drawLine(QPoint(textRect.left(), underline_y), QPoint(textRect.right(), underline_y));
      }
  
      painter->restore();
  
      return;
    }
  
    RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
  
    linkedText->cacheDocument(owner);
  
    painter->translate(rect.left(), rect.top());
  
    if(font != linkedText->doc.defaultFont())
      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(1, diff / 2);
    else
      painter->translate(1, 0);
  
    linkedText->doc.drawContents(painter, QRectF(0, 0, rect.width() - 1, 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(1, diff / 2);
      else
        p -= QPoint(1, 0);
  
      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);
  
            blockrect.setRight(qMin(blockrect.right(), (qreal)rect.width()));
  
            painter->drawLine(blockrect.bottomLeft(), blockrect.bottomRight());
          }
        }
      }
    }
  }
  
  int RichResourceTextWidthHint(const QWidget *owner, const QFont &font, const QVariant &var)
  {
    // special case handling for ResourceId/GPUAddress on its own
    if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
    {
      QFont f = font;
      f.setBold(true);
  
      static const int margin = RichResourceTextMargin;
  
      QFontMetrics metrics(f);
  
      QString name;
  
      if(var.userType() == qMetaTypeId<ResourceId>())
      {
        ICaptureContext *ctxptr = getCaptureContext(owner);
  
        ResourceId id = var.value<ResourceId>();
  
        if(ctxptr)
          name = GetTruncatedResourceName(*ctxptr, id);
        else
          name = ToQStr(id);
      }
      else
      {
        GPUAddressPtr ptr = var.value<GPUAddressPtr>();
  
        ptr->cacheAddress(owner);
  
        if(ptr->val.pointer != 0)
          name =
              QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
        else
          name = lit("NULL");
      }
  
      const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
  
      int ret = margin + metrics.boundingRect(name).width() + margin + px.width() + margin;
      return ret;
    }
  
    RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
  
    linkedText->cacheDocument(owner);
  
    return linkedText->idealWidth;
  }
  
  bool RichResourceTextMouseEvent(const QWidget *owner, const QVariant &var, QRect rect,
                                  const QFont &font, QMouseEvent *event)
  {
    // only process clicks or moves
    if(event->type() != QEvent::MouseButtonRelease && event->type() != QEvent::MouseMove)
      return false;
  
    // only process left button clicks
    if(event->type() == QEvent::MouseButtonRelease && event->button() != Qt::LeftButton)
      return false;
  
    // special case handling for ResourceId/GPUAddress on its own
    if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
    {
      ResourceId id;
      GPUAddressPtr ptr;
      ICaptureContext *ctxptr = NULL;
  
      if(var.userType() == qMetaTypeId<ResourceId>())
      {
        id = var.value<ResourceId>();
  
        // empty resource ids are not clickable or hover-highlighted.
        if(id == ResourceId())
          return false;
      }
  
      if(var.userType() == qMetaTypeId<GPUAddressPtr>())
      {
        ptr = var.value<GPUAddressPtr>();
  
        ptr->cacheAddress(owner);
  
        // NULL or unknown addresses also are not clickable
        if(ptr->val.pointer == 0 || ptr->base == ResourceId())
          return false;
      }
  
      QFont f = font;
      f.setBold(true);
  
      static const int margin = RichResourceTextMargin;
  
      rect.adjust(margin, 0, -margin * 2, 0);
  
      QString name;
  
      if(var.userType() == qMetaTypeId<ResourceId>())
      {
        ctxptr = getCaptureContext(owner);
  
        if(ctxptr)
          name = GetTruncatedResourceName(*ctxptr, id);
        else
          name = ToQStr(id);
      }
      else
      {
        ctxptr = ptr->ctxptr;
  
        name =
            QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
      }
  
      QRect textRect = QFontMetrics(f).boundingRect(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
  
      const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
  
      rect.setTop(textRect.top());
      rect.setWidth(textRect.width() + margin + px.width());
      rect.setHeight(qMax(textRect.height(), px.height()));
  
      if(rect.contains(event->pos()))
      {
        if(var.userType() == qMetaTypeId<ResourceId>())
        {
          if(event->type() == QEvent::MouseButtonRelease && ctxptr)
          {
            ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
  
            if(!ctx.HasResourceInspector())
              ctx.ShowResourceInspector();
  
            ctx.GetResourceInspector()->Inspect(id);
  
            ctx.RaiseDockWindow(ctx.GetResourceInspector()->Widget());
          }
  
          return true;
        }
        else if(var.userType() == qMetaTypeId<GPUAddressPtr>())
        {
          if(event->type() == QEvent::MouseButtonRelease && ctxptr)
          {
            ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
  
            const ShaderVariableType &ptrType = PointerTypeRegistry::GetTypeDescriptor(ptr->val);
  
            QString formatter;
  
            if(!ptrType.members.isEmpty())
              formatter = BufferFormatter::DeclareStruct(ptrType.descriptor.name, ptrType.members,
                                                         ptrType.descriptor.arrayByteStride);
  
            IBufferViewer *view = ctx.ViewBuffer(ptr->offset, ~0ULL, ptr->base, formatter);
  
            ctx.AddDockWindow(view->Widget(), DockReference::MainToolArea, NULL);
          }
  
          return true;
        }
      }
  
      return false;
    }
  
    RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
  
    linkedText->cacheDocument(owner);
  
    QAbstractTextDocumentLayout *layout = linkedText->doc.documentLayout();
  
    // vertical align to the centre, if there's spare room.
    int diff = rect.height() - linkedText->doc.size().height();
  
    QPoint p = event->pos() - rect.topLeft();
    if(diff > 0)
      p -= QPoint(1, diff / 2);
    else
      p -= QPoint(1, 0);
  
    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(res);
  
            ctx.RaiseDockWindow(ctx.GetResourceInspector()->Widget());
          }
  
          return true;
        }
      }
    }
  
    return false;
  }
  
  QString RichResourceTextFormat(ICaptureContext &ctx, QVariant var)
  {
    RichResourceTextInitialise(var);
    if(var.userType() == qMetaTypeId<ResourceId>())
      return GetTruncatedResourceName(ctx, var.value<ResourceId>());
  
    // either it's something else and wasn't rich resource, in which case just return the string
    // representation, or it's a fully formatted rich resource document, where the cached text will do
    // the trick with ResIdTextToString.
    return var.toString();
  }
  
  RichTextViewDelegate::RichTextViewDelegate(QAbstractItemView *parent)
      : m_widget(parent), ForwardingDelegate(parent)
  {
  }
  
  RichTextViewDelegate::~RichTextViewDelegate()
  {
  }
  
  void RichTextViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
                                   const QModelIndex &index) const
  {
    if(index.isValid())
    {
      QVariant v = index.data();
  
      if(RichResourceTextCheck(v))
      {
        // draw the item without text, so we get the proper background/selection/etc.
        // we'd like to be able to use the parent delegate's paint here, but either it calls to
        // QStyledItemDelegate which will re-fetch the text (bleh), or it calls to the manual
        // delegate which could do anything. So for this case we just use the style and skip the
        // delegate and hope it works out.
        QStyleOptionViewItem opt = option;
        QStyledItemDelegate::initStyleOption(&opt, index);
        opt.text.clear();
        m_widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, m_widget);
  
        painter->save();
  
        QRect rect = option.rect;
        if(!opt.icon.isNull())
        {
          QIcon::Mode mode;
          if((opt.state & QStyle::State_Enabled) == 0)
            mode = QIcon::Disabled;
          else if(opt.state & QStyle::State_Selected)
            mode = QIcon::Selected;
          else
            mode = QIcon::Normal;
          QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
          rect.setX(rect.x() + opt.icon.actualSize(opt.decorationSize, mode, state).width() + 4);
        }
  
        RichResourceTextPaint(m_widget, painter, rect, opt.font, option.palette,
                              option.state & QStyle::State_MouseOver,
                              m_widget->viewport()->mapFromGlobal(QCursor::pos()), v);
  
        painter->restore();
        return;
      }
    }
  
    return ForwardingDelegate::paint(painter, option, index);
  }
  
  QSize RichTextViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
  {
    if(index.isValid())
    {
      QVariant v = index.data();
  
      if(RichResourceTextCheck(v))
        return QSize(RichResourceTextWidthHint(m_widget, option.font, v), option.fontMetrics.height());
    }
  
    return ForwardingDelegate::sizeHint(option, index);
  }
  
  bool RichTextViewDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
                                         const QStyleOptionViewItem &option, const QModelIndex &index)
  {
    if(event->type() == QEvent::MouseButtonRelease && index.isValid())
    {
      QVariant v = index.data();
  
      if(RichResourceTextCheck(v))
      {
        QRect rect = option.rect;
  
        QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
  
        if(!icon.isNull())
        {
          rect.setX(rect.x() +
                    icon.actualSize(option.decorationSize, QIcon::Normal, QIcon::On).width() + 4);
        }
  
        // ignore the return value, we always consume clicks on this cell
        RichResourceTextMouseEvent(m_widget, v, rect, option.font, (QMouseEvent *)event);
        return true;
      }
    }
  
    return ForwardingDelegate::editorEvent(event, model, option, index);
  }
  
  bool RichTextViewDelegate::linkHover(QMouseEvent *e, const QFont &font, const QModelIndex &index)
  {
    if(index.isValid())
    {
      QVariant v = index.data();
  
      if(RichResourceTextCheck(v))
      {
        QRect rect = m_widget->visualRect(index);
  
        QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
  
        if(!icon.isNull())
        {
          rect.setX(
              rect.x() +
              icon.actualSize(QSize(rect.height(), rect.height()), QIcon::Normal, QIcon::On).width() +
              4);
        }
  
        return RichResourceTextMouseEvent(m_widget, v, rect, font, e);
      }
    }
  
    return false;
  }
  
  #include "renderdoc_tostr.inl"
  
  QString ToQStr(const ResourceUsage usage, const GraphicsAPI apitype)
  {
    if(IsD3D(apitype))
    {
      switch(usage)
      {
        case ResourceUsage::Unused: return lit("Unused");
  
        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::Discard: return lit("Discard");
  
        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");
  
        case ResourceUsage::CPUWrite: return lit("CPU Write");
      }
    }
    else if(apitype == GraphicsAPI::OpenGL || apitype == GraphicsAPI::Vulkan)
    {
      const bool vk = (apitype == GraphicsAPI::Vulkan);
  
      switch(usage)
      {
        case ResourceUsage::Unused: return lit("Unused");
  
        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::Discard: return lit("Discard");
  
        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");
  
        case ResourceUsage::CPUWrite: return lit("CPU Write");
      }
    }
  
    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 ToQStr(const AddressMode addr, const GraphicsAPI apitype)
  {
    if(IsD3D(apitype))
    {
      switch(addr)
      {
        case AddressMode::Wrap: return lit("Wrap");
        case AddressMode::Mirror: return lit("Mirror");
        case AddressMode::MirrorOnce: return lit("MirrorOnce");
        case AddressMode::ClampEdge: return lit("ClampEdge");
        case AddressMode::ClampBorder: return lit("ClampBorder");
        default: break;
      }
    }
    else
    {
      switch(addr)
      {
        case AddressMode::Repeat: return lit("Repeat");
        case AddressMode::MirrorRepeat: return lit("MirrorRepeat");
        case AddressMode::MirrorClamp: return lit("MirrorClamp");
        case AddressMode::ClampEdge: return lit("ClampEdge");
        case AddressMode::ClampBorder: return lit("ClampBorder");
        default: break;
      }
    }
  
    return lit("Unknown");
  }
  
  QString TypeString(const SigParameter &sig)
  {
    QString ret = ToQStr(sig.varType);
  
    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[] = {
        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("Unsupported (GroupSize)"),
        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"),
        lit("Unsupported (BaseVertex)"),
        lit("Unsupported (BaseInstance)"),
        lit("Unsupported (DrawIndex)"),
        lit("Unsupported (StencilReference)"),
        lit("Unsupported (PointCoord)"),
        lit("Unsupported (IsHelper)"),
        lit("Unsupported (SubgroupSize)"),
        lit("Unsupported (NumSubgroups)"),
        lit("Unsupported (SubgroupIndexInWorkgroup)"),
        lit("Unsupported (IndexInSubgroup)"),
        lit("Unsupported (SubgroupEqualMask)"),
        lit("Unsupported (SubgroupGreaterEqualMask)"),
        lit("Unsupported (SubgroupGreaterMask)"),
        lit("Unsupported (SubgroupLessEqualMask)"),
        lit("Unsupported (SubgroupLessMask)"),
        lit("Unsupported (DeviceIndex)"),
        lit("Unsupported (IsFullyCovered)"),
        lit("Unsupported (FragAreaSize)"),
        lit("Unsupported (FragInvocationCount)"),
    };
  
    static_assert(arraydim<ShaderBuiltin>() == ARRAY_COUNT(sysValues),
                  "System values have changed - update HLSL stub generation");
  
    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;
      }
  
      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 = draw->previous;
  
        while(prev != NULL && prev->eventId > end)
        {
          if(!(prev->flags & (DrawFlags::Dispatch | DrawFlags::Drawcall | DrawFlags::CmdList)))
          {
            prev = prev->previous;
          }
          else
          {
            distinct = true;
            break;
          }
  
          if(prev == NULL)
            distinct = true;
        }
      }
  
      if(distinct)
      {
        callback(start, end, us);
        if(end == u.eventId && us == u.usage)
        {
          start = 0;
        }
        else
        {
          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.basetype == SDBasic::Resource)
      {
        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:
          {
            QStringList lines = QString(obj->data.str).split(QLatin1Char('\n'));
            QString trimmedStr;
            for(int i = 0; i < 3 && i < lines.count(); i++)
              trimmedStr += lines[i] + QLatin1Char('\n');
            if(lines.count() > 3)
              trimmedStr += lit("...");
            param = trimmedStr.trimmed();
            break;
          }
          case SDBasic::Resource:
          case SDBasic::Enum:
          case SDBasic::UnsignedInteger: param = Formatter::HumanFormat(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(NULL, {});
    methodIndex = invoke->metaObject()->indexOfMethod(QMetaObject::normalizedSignature("doInvoke()"));
    invoke->deleteLater();
  }
  
  void GUIInvoke::call(QObject *obj, const std::function<void()> &f)
  {
    if(!obj)
      qCritical() << "GUIInvoke::call called with NULL object";
  
    if(onUIThread())
    {
      if(obj)
        f();
      return;
    }
  
    defer(obj, f);
  }
  
  void GUIInvoke::defer(QObject *obj, const std::function<void()> &f)
  {
    if(!obj)
      qCritical() << "GUIInvoke::defer called with NULL object";
  
    GUIInvoke *invoke = new GUIInvoke(obj, f);
    invoke->moveToThread(qApp->thread());
    invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::QueuedConnection);
  }
  
  void GUIInvoke::blockcall(QObject *obj, const std::function<void()> &f)
  {
    if(!obj)
      qCritical() << "GUIInvoke::blockcall called with NULL object";
  
    if(onUIThread())
    {
      if(obj)
        f();
      return;
    }
  
    GUIInvoke *invoke = new GUIInvoke(obj, f);
    invoke->moveToThread(qApp->thread());
    invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::BlockingQueuedConnection);
  }
  
  bool GUIInvoke::onUIThread()
  {
    return qApp->thread() == QThread::currentThread();
  }
  
  QString RDDialog::DefaultBrowsePath;
  
  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)
  {
  // workaround for QTBUG-56382 needed on windows only - it can break on other platforms
  #if defined(Q_OS_WIN32)
    dialog->setWindowModality(Qt::ApplicationModal);
    dialog->show();
    QEventLoop loop;
    while(dialog->isVisible())
    {
      loop.processEvents(QEventLoop::WaitForMoreEvents);
      QCoreApplication::sendPostedEvents();
    }
  #else
    dialog->exec();
  #endif
  
    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;
  
    QObject *parentObj = parent;
  
    if(parentObj == NULL)
    {
      // for 'global' message boxes with no parents, just use the app as the parent pointer
      parentObj = qApp;
    }
  
    // if we're already on the right thread, this boils down to a function call
    GUIInvoke::blockcall(parentObj, [&]() {
      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(parent, [&]() {
      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)
  {
    QString d = dir;
    if(d.isEmpty())
      d = DefaultBrowsePath;
  
    QFileDialog fd(parent, caption, d, 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())
      {
        DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
        return files[0];
      }
    }
  
    return QString();
  }
  
  QString RDDialog::getExecutableFileName(QWidget *parent, const QString &caption, const QString &dir,
                                          const QString &defaultExe, QFileDialog::Options options)
  {
    QString d = dir;
    if(d.isEmpty())
      d = DefaultBrowsePath;
  
    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, d, filter);
    fd.setOptions(options);
    fd.setAcceptMode(QFileDialog::AcceptOpen);
    fd.setFileMode(QFileDialog::ExistingFile);
    {
      QFileFilterModel *fileProxy = new QFileFilterModel(parent);
      fileProxy->setRequirePermissions(QDir::Executable);
      fd.setProxyModel(fileProxy);
    }
    if(!defaultExe.isEmpty())
      fd.selectFile(defaultExe);
    show(&fd);
  
    if(fd.result() == QFileDialog::Accepted)
    {
      QStringList files = fd.selectedFiles();
      if(!files.isEmpty())
      {
        DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
        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)
  {
    QString d = dir;
    if(d.isEmpty())
      d = DefaultBrowsePath;
  
    QFileDialog fd(parent, caption, d, 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);
      // we expect that this should always be non-negative because only the filters we know about
      // should get selected
      if(i >= 0)
      {
        fd.setDefaultSuffix(defaultSuffixes.value(i));
      }
      else
      {
        // GNOME has a bug that passes an empty string to filterSelected, so we ignore it with a
        // warning.
        if(filter == QString())
        {
          qWarning() << "Empty filter string passed to QFileDialog::filterSelected. "
                     << "Ignoring this as a likely GNOME bug, default suffix is still: "
                     << fd.defaultSuffix();
        }
        else
        {
          // some filter that we don't recognise was selected! Try to figure out the suffix on the
          // fly
          QStringList suffixes = getDefaultSuffixesFromFilter(filter);
  
          if(suffixes.empty())
          {
            qWarning() << "Unknown filter " << filter << " selected. "
                       << "Couldn't determine filename suffix, default suffix is still: "
                       << fd.defaultSuffix();
          }
          else
          {
            fd.setDefaultSuffix(suffixes[0]);
  
            qWarning() << "Unknown filter " << filter << " selected. "
                       << "Using default suffix: " << fd.defaultSuffix();
          }
        }
      }
    });
    show(&fd);
  
    if(fd.result() == QFileDialog::Accepted)
    {
      if(selectedFilter)
        *selectedFilter = fd.selectedNameFilter();
  
      QStringList files = fd.selectedFiles();
      if(!files.isEmpty())
      {
        DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
        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;
  }
  
  QCollatorSortFilterProxyModel::QCollatorSortFilterProxyModel(QObject *parent)
      : QSortFilterProxyModel(parent)
  {
    m_collator = new QCollator();
  }
  
  QCollatorSortFilterProxyModel::~QCollatorSortFilterProxyModel()
  {
    delete m_collator;
  }
  
  bool QCollatorSortFilterProxyModel::lessThan(const QModelIndex &source_left,
                                               const QModelIndex &source_right) const
  {
    return m_collator->compare(sourceModel()->data(source_left, sortRole()).toString(),
                               sourceModel()->data(source_right, sortRole()).toString()) < 0;
  }
  
  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;
  float Formatter::m_FontBaseSize = 10.0f;    // this should always be overridden below, but just in
                                              // case let's pick a sensible value
  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_FontBaseSize = QApplication::font().pointSizeF();
    }
  
    *m_Font =
        config.Font_PreferMonospaced ? QFontDatabase::systemFont(QFontDatabase::FixedFont) : QFont();
  
    m_Font->setPointSizeF(m_FontBaseSize * config.Font_GlobalScale);
    QFont f = QApplication::font();
    f.setPointSizeF(m_FontBaseSize * config.Font_GlobalScale);
    QApplication::setFont(f);
  
    Formatter::setPalette(QApplication::palette());
  }
  
  void Formatter::setPalette(QPalette palette)
  {
    m_DarkChecker = 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;
  }
  
  QString Formatter::HumanFormat(uint64_t u)
  {
    if(u == UINT16_MAX)
      return lit("UINT16_MAX");
    if(u == UINT32_MAX)
      return lit("UINT32_MAX");
    if(u == UINT64_MAX)
      return lit("UINT64_MAX");
  
    // format as hex when over a certain threshold
    if(u > 0xffffff)
      return lit("0x") + Format(u, true);
  
    return Format(u);
  }
  
  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(500, 200));
      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 enableCancel() { setCancelButtonText(tr("Cancel")); }
    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);
    }
    void closeEvent(QCloseEvent *event) override { event->ignore(); }
    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,
                         QWidget *parent, bool hidden, std::function<void()> finishedCallback)
  {
  #if defined(Q_OS_WIN32)
  
    std::wstring wideExe = QDir::toNativeSeparators(fullExecutablePath).toStdWString();
    std::wstring wideParams;
  
    for(QString p : params)
    {
      wideParams += L"\"";
      wideParams += p.toStdWString();
      wideParams += L"\" ";
    }
  
    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 = hidden ? SW_HIDE : 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, parent, finishedCallback]() {
          WaitForSingleObject(h, 30000);
          CloseHandle(h);
          GUIInvoke::call(parent, 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("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("konsole"), 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;
      // these programs need a -- to indicate the end of their options, before the program
      if(sudo == lit("kdesudo") || sudo == lit("gksudo"))
        sudoParams << lit("--");
  
      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, QProcess::ExitStatus>::of(&QProcess::finished),
                       [parent, process, finishedCallback](int exitCode) {
                         process->deleteLater();
                         GUIInvoke::call(parent, finishedCallback);
                       });
  
      return true;
    }
  
    QString sudo = QStandardPaths::findExecutable(lit("sudo"));
  
    if(sudo.isEmpty())
    {
      RDDialog::critical(parent, lit("Error running program as root"),
                         lit("Couldn't find graphical or terminal sudo program!\n"
                             "Please run '%1' with args '%2' manually.")
                             .arg(fullExecutablePath)
                             .arg(params.join(QLatin1Char(' '))));
      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 'echo Running \"%1 %2\" as root.;echo;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, QProcess::ExitStatus>::of(&QProcess::finished),
                       [parent, process, finishedCallback](int exitCode) {
                         process->deleteLater();
                         GUIInvoke::call(parent, finishedCallback);
                       });
  
      return true;
    }
  
    RDDialog::critical(parent, lit("Error running program as root"),
                       lit("Couldn't find graphical or terminal emulator to launch sudo!\n"
                           "Please manually run: sudo \"%1\" %2")
                           .arg(fullExecutablePath)
                           .arg(params.join(QLatin1Char(' '))));
  
    return false;
  #endif
  }
  
  void RevealFilenameInExternalFileBrowser(const QString &filePath)
  {
  #if defined(Q_OS_WIN32)
    // on windows we can ask explorer to highlight the exact file.
    QProcess::startDetached(lit("explorer.exe"), QStringList() << lit("/select,")
                                                               << QDir::toNativeSeparators(filePath));
  #else
    // on all other platforms, we just use QDesktopServices to invoke the external file browser on the
    // directory and hope that's close enough.
    QDesktopServices::openUrl(QFileInfo(filePath).absoluteDir().absolutePath());
  #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
    rdcstr argString = args;
  
    // 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
    rdcstr a;
  
    while(*c)
    {
      if(!dquot && !squot && (*c == ' ' || *c == '\t'))
      {
        if(!a.empty())
          ret << QString(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(a);
  #endif
  
    return ret;
  }
  
  void ShowProgressDialog(QWidget *window, const QString &labelText, ProgressFinishedMethod finished,
                          ProgressUpdateMethod update, ProgressCancelMethod cancel)
  {
    if(finished())
      return;
  
    RDProgressDialog dialog(labelText, window);
  
    if(cancel)
      dialog.enableCancel();
  
    // 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(&dialog, [update, &dialog]() { dialog.setPercentage(update()); });
  
        GUIInvoke::call(&dialog, [finished, &tickerSemaphore]() {
          if(finished())
            tickerSemaphore.tryAcquire();
        });
      }
  
      GUIInvoke::call(&dialog, [&dialog]() { dialog.closeAndReset(); });
    });
    progressTickerThread.setName(lit("Progress Dialog"));
    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();
  
    if(cancel && dialog.wasCanceled())
      cancel();
  }
  
  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;
  }
  
  void BringToForeground(QWidget *window)
  {
  #ifdef Q_OS_WIN
    SetWindowPos((HWND)window->winId(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    window->setWindowState(Qt::WindowActive);
    window->raise();
    window->showNormal();
    window->show();
    SetWindowPos((HWND)window->winId(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  #else
    window->activateWindow();
    window->raise();
    window->showNormal();
  #endif
  }
  
  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);
  }
  
  // we declare this partial class to get the accessors. THIS IS DANGEROUS as the ABI is unstable and
  // this is a private class. The first few functions have been stable for a while so we hope that it
  // will remain so. If a stable interface is added in future like QX11Info we should definitely use
  // it instead.
  //
  // Unfortunately we need this for Wayland, so we only ever use it when we are absolutely forced to
  // because we're running under the Wayland Qt platform.
  class QOpenGLContext;
  
  class Q_GUI_EXPORT QPlatformNativeInterface : public QObject
  {
    Q_OBJECT
  public:
    virtual void *nativeResourceForIntegration(const QByteArray &resource);
    virtual void *nativeResourceForContext(const QByteArray &resource, QOpenGLContext *context);
    virtual void *nativeResourceForScreen(const QByteArray &resource, QScreen *screen);
    virtual void *nativeResourceForWindow(const QByteArray &resource, QWindow *window);
  };
  
  void *AccessWaylandPlatformInterface(const QByteArray &resource, QWindow *window)
  {
    QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
    return native->nativeResourceForWindow(resource, window);
  }
  
  // Default Qt doesn't do this in release Qt builds, which is all we use
  #if defined(Q_OS_WIN32)
  
  #include <windows.h>
  
  typedef HRESULT(WINAPI *PFN_SetThreadDescription)(HANDLE hThread, PCWSTR lpThreadDescription);
  
  const DWORD MS_VC_EXCEPTION = 0x406D1388;
  #pragma pack(push, 8)
  typedef struct tagTHREADNAME_INFO
  {
    DWORD dwType;        // Must be 0x1000.
    LPCSTR szName;       // Pointer to name (in user addr space).
    DWORD dwThreadID;    // Thread ID (-1=caller thread).
    DWORD dwFlags;       // Reserved for future use, must be zero.
  } THREADNAME_INFO;
  #pragma pack(pop)
  
  static void SetThreadNameWithException(const char *name)
  {
    THREADNAME_INFO info;
    info.dwType = 0x1000;
    info.szName = name;
    info.dwThreadID = GetCurrentThreadId();
    info.dwFlags = 0;
    __try
    {
      RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)(&info));
    }
    __except(EXCEPTION_CONTINUE_EXECUTION)
    {
    }
  }
  
  void LambdaThread::windowsSetName()
  {
    // try to use the fancy modern API
    static PFN_SetThreadDescription setThreadDesc = (PFN_SetThreadDescription)GetProcAddress(
        GetModuleHandleA("kernel32.dll"), "SetThreadDescription");
  
    if(setThreadDesc)
    {
      setThreadDesc(GetCurrentThread(), m_Name.toStdWString().c_str());
    }
    else
    {
      // don't throw the exception if there's no debugger present
      if(!IsDebuggerPresent())
        return;
  
      SetThreadNameWithException(m_Name.toStdString().c_str());
    }
  }
  
  #else
  
  void LambdaThread::windowsSetName()
  {
  }
  
  #endif
  