mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
5083 lines
159 KiB
C++
5083 lines
159 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2021 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 "EventBrowser.h"
|
|
#include <QAbstractItemModel>
|
|
#include <QAbstractSpinBox>
|
|
#include <QComboBox>
|
|
#include <QCompleter>
|
|
#include <QDialogButtonBox>
|
|
#include <QKeyEvent>
|
|
#include <QLineEdit>
|
|
#include <QMenu>
|
|
#include <QPushButton>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionMatch>
|
|
#include <QScrollBar>
|
|
#include <QShortcut>
|
|
#include <QSortFilterProxyModel>
|
|
#include <QStringListModel>
|
|
#include <QStylePainter>
|
|
#include <QTextEdit>
|
|
#include <QTimer>
|
|
#include "Code/QRDUtils.h"
|
|
#include "Code/Resources.h"
|
|
#include "Widgets/CollapseGroupBox.h"
|
|
#include "Widgets/Extended/RDHeaderView.h"
|
|
#include "Widgets/Extended/RDLabel.h"
|
|
#include "Widgets/Extended/RDListWidget.h"
|
|
#include "Widgets/Extended/RDTreeWidget.h"
|
|
#include "Widgets/MarkerBreadcrumbs.h"
|
|
#include "flowlayout/FlowLayout.h"
|
|
#include "scintilla/include/qt/ScintillaEdit.h"
|
|
#include "ui_EventBrowser.h"
|
|
|
|
struct EventBrowserPersistentStorage : public CustomPersistentStorage
|
|
{
|
|
EventBrowserPersistentStorage() : CustomPersistentStorage(rdcstr()) {}
|
|
EventBrowserPersistentStorage(rdcstr name) : CustomPersistentStorage(name) {}
|
|
void save(QVariant &v) const
|
|
{
|
|
QVariantMap settings;
|
|
|
|
settings[lit("current")] = CurrentFilter;
|
|
|
|
QVariantList filters;
|
|
for(const QPair<QString, QString> &f : SavedFilters)
|
|
filters << QVariant(QVariantList({f.first, f.second}));
|
|
|
|
settings[lit("filters")] = filters;
|
|
|
|
v = settings;
|
|
}
|
|
|
|
void load(const QVariant &v)
|
|
{
|
|
QVariantMap settings = v.toMap();
|
|
|
|
QVariant current = settings[lit("current")];
|
|
if(current.isValid() && current.type() == QVariant::String)
|
|
{
|
|
CurrentFilter = current.toString();
|
|
}
|
|
else
|
|
{
|
|
CurrentFilter = lit("$draw()");
|
|
}
|
|
|
|
QVariant saved = settings[lit("filters")];
|
|
if(saved.isValid() && saved.type() == QVariant::List)
|
|
{
|
|
QVariantList filters = saved.toList();
|
|
for(QVariant filter : filters)
|
|
{
|
|
QVariantList filterPair = filter.toList();
|
|
if(filterPair.count() == 2 && filterPair[0].type() == QVariant::String &&
|
|
filterPair[1].type() == QVariant::String)
|
|
{
|
|
QString name = filterPair[0].toString();
|
|
QString expr = filterPair[1].toString();
|
|
|
|
if(!name.isEmpty())
|
|
SavedFilters.push_back({name, expr});
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SavedFilters.push_back(qMakePair(lit("Default"), lit("$draw()")));
|
|
SavedFilters.push_back(qMakePair(lit("Draws and Barriers"), lit("$draw() Barrier")));
|
|
SavedFilters.push_back(qMakePair(lit("Hide Copies & Clears"), lit("$draw() -Copy -Clear")));
|
|
}
|
|
}
|
|
|
|
QString CurrentFilter;
|
|
QList<QPair<QString, QString>> SavedFilters;
|
|
};
|
|
|
|
static EventBrowserPersistentStorage persistantStorage("EventBrowser");
|
|
|
|
enum
|
|
{
|
|
COL_NAME,
|
|
COL_EID,
|
|
COL_DRAW,
|
|
COL_DURATION,
|
|
COL_COUNT,
|
|
};
|
|
|
|
enum
|
|
{
|
|
ROLE_SELECTED_EID = Qt::UserRole,
|
|
ROLE_EFFECTIVE_EID,
|
|
ROLE_GROUPED_DRAWCALL,
|
|
ROLE_EXACT_DRAWCALL,
|
|
ROLE_CHUNK,
|
|
};
|
|
|
|
static uint32_t GetSelectedEID(QModelIndex idx)
|
|
{
|
|
return idx.data(ROLE_SELECTED_EID).toUInt();
|
|
}
|
|
|
|
static uint32_t GetEffectiveEID(QModelIndex idx)
|
|
{
|
|
return idx.data(ROLE_EFFECTIVE_EID).toUInt();
|
|
}
|
|
|
|
struct EventItemModel : public QAbstractItemModel
|
|
{
|
|
EventItemModel(QAbstractItemView *view, ICaptureContext &ctx) : m_View(view), m_Ctx(ctx)
|
|
{
|
|
UpdateDurationColumn();
|
|
|
|
m_CurrentEID = createIndex(0, 0, TagCaptureStart);
|
|
|
|
const QColor highlight = view->palette().color(QPalette::Highlight).toHsl();
|
|
|
|
// this is a bit arbitrary but most highlight colours are blue, and blue text looks like a link.
|
|
// Rotate towards red (a 1/3rd turn). If the highlight colour is something different this will
|
|
// give a similar effect, hopefully.
|
|
qreal hue = highlight.hueF() + 0.333;
|
|
if(hue >= 1.0f)
|
|
hue -= 1.0;
|
|
|
|
// saturate the color a bit
|
|
qreal sat = qMin(1.0, 1.25 * highlight.saturationF());
|
|
|
|
// keep the lightness the same
|
|
qreal light = highlight.lightnessF();
|
|
|
|
QColor paramcol;
|
|
paramcol.setHslF(hue, sat, light);
|
|
|
|
m_ParamColCode = QFormatStr("#%1%2%3")
|
|
.arg(paramcol.red(), 2, 16, QLatin1Char('0'))
|
|
.arg(paramcol.green(), 2, 16, QLatin1Char('0'))
|
|
.arg(paramcol.blue(), 2, 16, QLatin1Char('0'));
|
|
}
|
|
|
|
void ResetModel()
|
|
{
|
|
emit beginResetModel();
|
|
emit endResetModel();
|
|
|
|
m_Nodes.clear();
|
|
m_RowInParentCache.clear();
|
|
m_EIDNameCache.clear();
|
|
m_Draws.clear();
|
|
m_Chunks.clear();
|
|
|
|
if(!m_Ctx.CurDrawcalls().empty())
|
|
m_Nodes[0] = CreateDrawNode(NULL);
|
|
|
|
m_CurrentEID = createIndex(0, 0, TagCaptureStart);
|
|
|
|
RefreshCache();
|
|
}
|
|
|
|
void RefreshCache()
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return;
|
|
|
|
uint32_t eid = m_Ctx.CurSelectedEvent();
|
|
|
|
m_MessageCounts[eid] = m_Ctx.CurPipelineState().GetShaderMessages().count();
|
|
m_EIDNameCache.remove(eid);
|
|
|
|
if(eid != data(m_CurrentEID, ROLE_SELECTED_EID) || eid == 0)
|
|
{
|
|
QModelIndex oldCurrent = m_CurrentEID;
|
|
|
|
m_CurrentEID = GetIndexForEID(eid);
|
|
|
|
if(eid == 0 && m_Ctx.CurEvent() != 0)
|
|
m_CurrentEID = createIndex(0, 0, TagRoot);
|
|
|
|
RefreshIcon(oldCurrent);
|
|
RefreshIcon(m_CurrentEID);
|
|
}
|
|
|
|
if(m_Ctx.GetBookmarks() != m_Bookmarks)
|
|
{
|
|
rdcarray<QModelIndex> indices;
|
|
indices.swap(m_BookmarkIndices);
|
|
|
|
m_Bookmarks = m_Ctx.GetBookmarks();
|
|
|
|
for(const EventBookmark &b : m_Bookmarks)
|
|
m_BookmarkIndices.push_back(GetIndexForEID(b.eventId));
|
|
|
|
for(QModelIndex idx : indices)
|
|
RefreshIcon(idx);
|
|
for(QModelIndex idx : m_BookmarkIndices)
|
|
RefreshIcon(idx);
|
|
}
|
|
|
|
if(m_Ctx.ResourceNameCacheID() != m_RenameCacheID)
|
|
{
|
|
m_View->viewport()->update();
|
|
}
|
|
|
|
m_RenameCacheID = m_Ctx.ResourceNameCacheID();
|
|
}
|
|
|
|
bool HasTimes() { return !m_Times.empty(); }
|
|
void SetTimes(const rdcarray<CounterResult> ×)
|
|
{
|
|
// set all times for events to -1.0
|
|
m_Times.fill(m_Nodes[0].effectiveEID + 1, -1.0);
|
|
|
|
// fill in the actual times
|
|
for(const CounterResult &r : times)
|
|
{
|
|
if(r.eventId < m_Times.size())
|
|
{
|
|
m_Times[r.eventId] = r.value.d;
|
|
}
|
|
}
|
|
|
|
// iterate nodes in reverse order, because parent nodes will always be before children
|
|
// so we know we'll have the results
|
|
QList<uint32_t> nodeEIDs = m_Nodes.keys();
|
|
for(auto it = nodeEIDs.rbegin(); it != nodeEIDs.rend(); it++)
|
|
CalculateTotalDuration(m_Nodes[*it]);
|
|
|
|
// Qt's item model kind of sucks and doesn't have a good way to say "all data in this column has
|
|
// changed" let alone "all data has changed". dataChanged() is limited to only a group of model
|
|
// indices under a single parent. Instead we just force the view itself to refresh here.
|
|
m_View->viewport()->update();
|
|
}
|
|
|
|
bool ShowParameterNames() { return m_ShowParameterNames; }
|
|
void SetShowParameterNames(bool show)
|
|
{
|
|
if(m_ShowParameterNames != show)
|
|
{
|
|
m_ShowParameterNames = show;
|
|
|
|
m_EIDNameCache.clear();
|
|
m_View->viewport()->update();
|
|
}
|
|
}
|
|
|
|
bool ShowAllParameters() { return m_ShowAllParameters; }
|
|
void SetShowAllParameters(bool show)
|
|
{
|
|
if(m_ShowAllParameters != show)
|
|
{
|
|
m_ShowAllParameters = show;
|
|
|
|
m_EIDNameCache.clear();
|
|
m_View->viewport()->update();
|
|
}
|
|
}
|
|
|
|
bool UseCustomDrawNames() { return m_UseCustomDrawNames; }
|
|
void SetUseCustomDrawNames(bool use)
|
|
{
|
|
if(m_UseCustomDrawNames != use)
|
|
{
|
|
m_UseCustomDrawNames = use;
|
|
|
|
m_EIDNameCache.clear();
|
|
m_View->viewport()->update();
|
|
}
|
|
}
|
|
|
|
void UpdateDurationColumn()
|
|
{
|
|
m_TimeUnit = m_Ctx.Config().EventBrowser_TimeUnit;
|
|
emit headerDataChanged(Qt::Horizontal, COL_DURATION, COL_DURATION);
|
|
|
|
m_View->viewport()->update();
|
|
}
|
|
|
|
void SetFindText(QString text)
|
|
{
|
|
if(m_FindString == text)
|
|
return;
|
|
|
|
rdcarray<QModelIndex> oldResults;
|
|
oldResults.swap(m_FindResults);
|
|
|
|
m_FindString = text;
|
|
m_FindResults.clear();
|
|
|
|
bool eidSearch = false;
|
|
// if the user puts in @123
|
|
if(!text.isEmpty() && text[0] == QLatin1Char('@'))
|
|
{
|
|
eidSearch = true;
|
|
text = text.mid(1);
|
|
}
|
|
|
|
bool eidOK = false;
|
|
uint32_t eid = text.toUInt(&eidOK);
|
|
|
|
// include EID in results first if the text parses as an integer
|
|
if(eidOK && eid > 0 && eid < m_Draws.size())
|
|
{
|
|
// if the text doesn't exactly match the EID after converting back, don't treat this as an
|
|
// EID-only search
|
|
if(text.trimmed() != QString::number(eid))
|
|
eidSearch = false;
|
|
|
|
m_FindResults.push_back(GetIndexForEID(eid));
|
|
}
|
|
|
|
if(!m_FindString.isEmpty() && !eidSearch)
|
|
{
|
|
// do a depth-first search to find results
|
|
AccumulateFindResults(createIndex(0, 0, TagRoot));
|
|
}
|
|
|
|
for(QModelIndex i : oldResults)
|
|
RefreshIcon(i);
|
|
for(QModelIndex i : m_FindResults)
|
|
RefreshIcon(i);
|
|
}
|
|
|
|
QModelIndex Find(bool forward)
|
|
{
|
|
if(m_FindResults.empty())
|
|
return QModelIndex();
|
|
|
|
// if we're already on a find result we can just zoom to the next one
|
|
int idx = m_FindResults.indexOf(m_CurrentEID);
|
|
if(idx >= 0)
|
|
{
|
|
if(forward)
|
|
idx++;
|
|
else
|
|
idx--;
|
|
if(idx < 0)
|
|
idx = m_FindResults.count() - 1;
|
|
|
|
idx %= m_FindResults.count();
|
|
|
|
return m_FindResults[idx];
|
|
}
|
|
|
|
// otherwise we need to do a more expensive search. Get the EID for the current, and find the
|
|
// next find result after that (wrapping around)
|
|
uint32_t eid = data(m_CurrentEID, ROLE_EFFECTIVE_EID).toUInt();
|
|
|
|
if(forward)
|
|
{
|
|
// find the first result >= our selected EID
|
|
for(int i = 0; i < m_FindResults.count(); i++)
|
|
{
|
|
uint32_t findEID = data(m_FindResults[i], ROLE_SELECTED_EID).toUInt();
|
|
if(findEID >= eid)
|
|
return m_FindResults[i];
|
|
}
|
|
|
|
// if we didn't find any, we're past all the results - return the first one to wrap
|
|
return m_FindResults[0];
|
|
}
|
|
else
|
|
{
|
|
// find the last result <= our selected EID
|
|
for(int i = m_FindResults.count() - 1; i >= 0; i--)
|
|
{
|
|
uint32_t findEID = data(m_FindResults[i], ROLE_SELECTED_EID).toUInt();
|
|
if(findEID <= eid)
|
|
return m_FindResults[i];
|
|
}
|
|
|
|
// if we didn't find any, we're before all the results - return the last one to wrap
|
|
return m_FindResults.back();
|
|
}
|
|
}
|
|
|
|
int NumFindResults() { return m_FindResults.count(); }
|
|
double GetSecondsDurationForEID(uint32_t eid)
|
|
{
|
|
if(eid < m_Times.size())
|
|
return m_Times[eid];
|
|
|
|
return -1.0;
|
|
}
|
|
|
|
QModelIndex GetIndexForEID(uint32_t eid)
|
|
{
|
|
if(eid == 0)
|
|
return createIndex(0, 0, TagCaptureStart);
|
|
|
|
const DrawcallDescription *draw = m_Draws[eid];
|
|
if(draw)
|
|
{
|
|
// this function is not called regularly (anywhere to do with painting) but only occasionally
|
|
// to find an index by EID, so we do an 'expensive' search for the row in the parent. The
|
|
// cache is LRU and holds a few entries in case the user is jumping back and forth between a
|
|
// few.
|
|
for(size_t i = 0; i < m_RowInParentCache.size(); i++)
|
|
{
|
|
if(m_RowInParentCache[i].first == eid)
|
|
return createIndex(m_RowInParentCache[i].second, 0, eid);
|
|
}
|
|
|
|
const int MaxCacheSize = 10;
|
|
|
|
// oldest entry is on the back, pop it
|
|
if(m_RowInParentCache.size() == MaxCacheSize)
|
|
m_RowInParentCache.pop_back();
|
|
|
|
// account for the Capture Start row we'll add at the top level
|
|
int rowInParent = draw->parent ? 0 : 1;
|
|
|
|
const rdcarray<DrawcallDescription> &draws =
|
|
draw->parent ? draw->parent->children : m_Ctx.CurDrawcalls();
|
|
|
|
for(const DrawcallDescription &d : draws)
|
|
{
|
|
// the first draw with an EID greater than the one we're searching for should contain it.
|
|
if(d.eventId >= eid)
|
|
{
|
|
// except if the draw is a fake marker. In this case its own event ID is invalid, so we
|
|
// check the range of its children (knowing it only has one layer of children)
|
|
if(d.IsFakeMarker())
|
|
{
|
|
if(d.eventId == eid)
|
|
break;
|
|
|
|
if(d.children[0].eventId < eid)
|
|
{
|
|
rowInParent++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// keep counting until we get to the row within this draw
|
|
for(size_t i = 0; i < d.events.size(); i++)
|
|
{
|
|
if(d.events[i].eventId < eid)
|
|
{
|
|
rowInParent++;
|
|
continue;
|
|
}
|
|
|
|
if(d.events[i].eventId != eid)
|
|
qCritical() << "Couldn't find event" << eid << "within draw" << draw->eventId;
|
|
|
|
// stop, we should be at the event now
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
rowInParent += d.events.count();
|
|
}
|
|
|
|
// insert the new element on the front of the cache
|
|
m_RowInParentCache.insert(0, {eid, rowInParent});
|
|
|
|
return createIndex(rowInParent, 0, eid);
|
|
}
|
|
else
|
|
{
|
|
qCritical() << "Couldn't find draw for event" << eid;
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
const DrawcallDescription *GetDrawcallForEID(uint32_t eid)
|
|
{
|
|
if(eid < m_Draws.size())
|
|
return m_Draws[eid];
|
|
|
|
return NULL;
|
|
}
|
|
|
|
rdcarray<rdcstr> GetMarkerList() const
|
|
{
|
|
rdcarray<rdcstr> ret;
|
|
|
|
for(auto it = m_Nodes.begin(); it != m_Nodes.end(); ++it)
|
|
if(it.value().draw && (it.value().draw->flags & DrawFlags::PushMarker))
|
|
ret.push_back(it.value().draw->name);
|
|
|
|
return ret;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// QAbstractItemModel methods
|
|
|
|
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return QModelIndex();
|
|
|
|
// create fake root if the parent is invalid
|
|
if(!parent.isValid())
|
|
return createIndex(0, column, TagRoot);
|
|
|
|
// if the parent is the root, return the index for the specified child if it's in bounds
|
|
if(parent.internalId() == TagRoot)
|
|
{
|
|
// first child is the capture start
|
|
if(row == 0)
|
|
return createIndex(row, column, TagCaptureStart);
|
|
|
|
// the rest are in the root draw node
|
|
return GetIndexForDrawChildRow(m_Nodes[0], row, column);
|
|
}
|
|
else if(parent.internalId() == TagCaptureStart)
|
|
{
|
|
// no children for this
|
|
return QModelIndex();
|
|
}
|
|
else
|
|
{
|
|
// otherwise the parent is a real draw
|
|
auto it = m_Nodes.find(parent.internalId());
|
|
if(it != m_Nodes.end())
|
|
return GetIndexForDrawChildRow(*it, row, column);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex parent(const QModelIndex &index) const override
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded() || !index.isValid() || index.internalId() == TagRoot)
|
|
return QModelIndex();
|
|
|
|
// Capture Start's parent is the root
|
|
if(index.internalId() == TagCaptureStart)
|
|
return createIndex(0, 0, TagRoot);
|
|
|
|
if(index.internalId() >= m_Draws.size())
|
|
return QModelIndex();
|
|
|
|
// otherwise it's a draw
|
|
const DrawcallDescription *draw = m_Draws[index.internalId()];
|
|
|
|
// if it has no parent draw, the parent is the root
|
|
if(draw->parent == NULL)
|
|
return createIndex(0, 0, TagRoot);
|
|
|
|
// the parent must be a node since it has at least one child (this draw), so we'll have a cached
|
|
// index
|
|
return m_Nodes[draw->parent->eventId].index;
|
|
}
|
|
|
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return 0;
|
|
|
|
// only one root
|
|
if(!parent.isValid())
|
|
return 1;
|
|
|
|
// only column 1 has children (Qt convention)
|
|
if(parent.column() != 0)
|
|
return 0;
|
|
|
|
if(parent.internalId() == TagRoot)
|
|
return m_Nodes[0].rowCount;
|
|
|
|
if(parent.internalId() == TagCaptureStart)
|
|
return 0;
|
|
|
|
// otherwise it's an event
|
|
uint32_t eid = (uint32_t)parent.internalId();
|
|
|
|
// if it's a node, return the row count
|
|
auto it = m_Nodes.find(eid);
|
|
if(it != m_Nodes.end())
|
|
return it->rowCount;
|
|
|
|
// no other nodes have children
|
|
return 0;
|
|
}
|
|
|
|
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return COL_COUNT; }
|
|
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
|
|
{
|
|
if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
|
|
{
|
|
switch(section)
|
|
{
|
|
case COL_NAME: return tr("Name");
|
|
case COL_EID: return lit("EID");
|
|
case COL_DRAW: return lit("Draw #");
|
|
case COL_DURATION: return tr("Duration (%1)").arg(UnitSuffix(m_TimeUnit));
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
|
|
{
|
|
if(!index.isValid())
|
|
return QVariant();
|
|
|
|
if(role == Qt::DecorationRole)
|
|
{
|
|
if(index == m_CurrentEID)
|
|
return Icons::flag_green();
|
|
else if(m_BookmarkIndices.contains(index))
|
|
return Icons::asterisk_orange();
|
|
else if(m_FindResults.contains(index))
|
|
return Icons::find();
|
|
return QVariant();
|
|
}
|
|
|
|
if(index.column() == COL_DURATION && role == Qt::TextAlignmentRole)
|
|
return int(Qt::AlignRight | Qt::AlignVCenter);
|
|
|
|
if(index.internalId() == TagRoot)
|
|
{
|
|
if(role == Qt::DisplayRole && index.column() == COL_NAME)
|
|
{
|
|
uint32_t frameNumber = m_Ctx.FrameInfo().frameNumber;
|
|
return frameNumber == ~0U ? tr("User-defined Capture") : tr("Frame #%1").arg(frameNumber);
|
|
}
|
|
|
|
if(role == ROLE_SELECTED_EID)
|
|
return 0;
|
|
|
|
if(role == ROLE_EFFECTIVE_EID)
|
|
return m_Nodes[0].effectiveEID;
|
|
|
|
if(index.column() == COL_DURATION && role == Qt::DisplayRole)
|
|
return FormatDuration(0);
|
|
}
|
|
else if(index.internalId() == TagCaptureStart)
|
|
{
|
|
if(role == Qt::DisplayRole)
|
|
{
|
|
switch(index.column())
|
|
{
|
|
case COL_NAME: return tr("Capture Start");
|
|
case COL_EID: return lit("0");
|
|
case COL_DRAW: return lit("0");
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
if(role == ROLE_SELECTED_EID || role == ROLE_EFFECTIVE_EID)
|
|
return 0;
|
|
|
|
if(index.column() == COL_DURATION && role == Qt::DisplayRole)
|
|
return FormatDuration(~0U);
|
|
}
|
|
else
|
|
{
|
|
uint32_t eid = index.internalId();
|
|
|
|
if(eid >= m_Draws.size())
|
|
return QVariant();
|
|
|
|
if(role == ROLE_SELECTED_EID)
|
|
return eid;
|
|
|
|
if(role == ROLE_GROUPED_DRAWCALL)
|
|
return QVariant::fromValue(qulonglong(m_Draws[eid]));
|
|
|
|
if(role == ROLE_EXACT_DRAWCALL)
|
|
{
|
|
const DrawcallDescription *draw = m_Draws[eid];
|
|
return draw->eventId == eid ? QVariant::fromValue(qulonglong(m_Draws[eid])) : QVariant();
|
|
}
|
|
|
|
if(role == ROLE_CHUNK)
|
|
return QVariant::fromValue(qulonglong(m_Chunks[eid]));
|
|
|
|
if(role == ROLE_EFFECTIVE_EID)
|
|
{
|
|
auto it = m_Nodes.find(eid);
|
|
if(it != m_Nodes.end())
|
|
return it->effectiveEID;
|
|
return eid;
|
|
}
|
|
|
|
if(index.column() == COL_DURATION && role == Qt::DisplayRole)
|
|
return FormatDuration(eid);
|
|
|
|
if(role == Qt::DisplayRole)
|
|
{
|
|
const DrawcallDescription *draw = m_Draws[eid];
|
|
|
|
switch(index.column())
|
|
{
|
|
case COL_NAME: return GetCachedEIDName(eid);
|
|
case COL_EID:
|
|
case COL_DRAW:
|
|
if(draw->eventId == eid && !draw->children.empty())
|
|
{
|
|
uint32_t effectiveEID = eid;
|
|
auto it = m_Nodes.find(eid);
|
|
if(it != m_Nodes.end())
|
|
effectiveEID = it->effectiveEID;
|
|
|
|
if(draw->IsFakeMarker())
|
|
eid = draw->children[0].events[0].eventId;
|
|
|
|
if(index.column() == COL_EID)
|
|
{
|
|
return eid == effectiveEID
|
|
? QVariant(eid)
|
|
: QVariant(QFormatStr("%1-%2").arg(eid).arg(effectiveEID));
|
|
}
|
|
else
|
|
{
|
|
uint32_t drawId = draw->drawcallId;
|
|
uint32_t endDrawId = m_Draws[effectiveEID]->drawcallId;
|
|
|
|
if(draw->IsFakeMarker())
|
|
drawId = draw->children[0].drawcallId;
|
|
|
|
return drawId == endDrawId
|
|
? QVariant()
|
|
: QVariant(QFormatStr("%1-%2").arg(drawId).arg(endDrawId));
|
|
}
|
|
}
|
|
|
|
if(index.column() == COL_EID)
|
|
return eid;
|
|
else
|
|
return draw->eventId == eid &&
|
|
!(draw->flags &
|
|
(DrawFlags::SetMarker | DrawFlags::PushMarker | DrawFlags::PopMarker))
|
|
? QVariant(draw->drawcallId)
|
|
: QVariant();
|
|
default: break;
|
|
}
|
|
}
|
|
else if(role == Qt::BackgroundRole || role == Qt::ForegroundRole ||
|
|
role == RDTreeView::TreeLineColorRole)
|
|
{
|
|
if(m_Ctx.Config().EventBrowser_ApplyColors)
|
|
{
|
|
if(!m_Ctx.Config().EventBrowser_ColorEventRow && role != RDTreeView::TreeLineColorRole)
|
|
return QVariant();
|
|
|
|
const DrawcallDescription *draw = m_Draws[eid];
|
|
|
|
// skip events that aren't the actual draw
|
|
if(draw->eventId != eid)
|
|
return QVariant();
|
|
|
|
// if alpha isn't 0, assume the colour is valid
|
|
if((draw->flags & (DrawFlags::PushMarker | DrawFlags::SetMarker)) &&
|
|
draw->markerColor.w > 0.0f)
|
|
{
|
|
QColor col =
|
|
QColor::fromRgb(qRgb(draw->markerColor.x * 255.0f, draw->markerColor.y * 255.0f,
|
|
draw->markerColor.z * 255.0f));
|
|
|
|
if(role == Qt::BackgroundRole || role == RDTreeView::TreeLineColorRole)
|
|
return QBrush(col);
|
|
else if(role == Qt::ForegroundRole)
|
|
return QBrush(contrastingColor(col, m_View->palette().color(QPalette::Text)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
private:
|
|
ICaptureContext &m_Ctx;
|
|
|
|
QAbstractItemView *m_View;
|
|
|
|
static const quintptr TagRoot = quintptr(UINTPTR_MAX);
|
|
static const quintptr TagCaptureStart = quintptr(0);
|
|
|
|
rdcarray<double> m_Times;
|
|
TimeUnit m_TimeUnit = TimeUnit::Count;
|
|
|
|
QModelIndex m_CurrentEID;
|
|
rdcarray<EventBookmark> m_Bookmarks;
|
|
rdcarray<QModelIndex> m_BookmarkIndices;
|
|
QString m_FindString;
|
|
rdcarray<QModelIndex> m_FindResults;
|
|
|
|
int32_t m_RenameCacheID;
|
|
QString m_ParamColCode;
|
|
|
|
QMap<uint32_t, int> m_MessageCounts;
|
|
|
|
// captures could have a lot of events, 1 million is high but not unreasonable and certainly not
|
|
// an upper bound. We store only 1 pointer per event which gives us reasonable memory usage (i.e.
|
|
// 8MB for that 1-million case on 64-bit) and still lets us look up what we need in O(1) and avoid
|
|
// more expensive lookups when we need the properties for an event.
|
|
// This gives us a pointer for every event ID pointing to the draw that contains it.
|
|
rdcarray<const DrawcallDescription *> m_Draws;
|
|
rdcarray<const SDChunk *> m_Chunks;
|
|
|
|
// we can have a bigger structure for every nested node (i.e. draw with children).
|
|
// This drastically limits how many we need to worry about - some hierarchies have more nodes than
|
|
// others but even in hierarchies with lots of nodes the number is small, compared to
|
|
// draws/events.
|
|
// we cache this with a depth-first search at init time while populating m_Draws
|
|
struct DrawTreeNode
|
|
{
|
|
const DrawcallDescription *draw;
|
|
uint32_t effectiveEID;
|
|
|
|
// cache the index for this node to make parent() significantly faster
|
|
QModelIndex index;
|
|
|
|
// this is the number of child events, meaning all the draws and all of their events, but *not*
|
|
// the events in any of their children.
|
|
uint32_t rowCount;
|
|
|
|
// this is a cache of row index to draw. Rather than being present for every row, this is
|
|
// spaced out such that there are roughly Row2EIDFactor entries at most. This means that the
|
|
// O(n) lookup to find the EID for a given row has to process that many times less entries since
|
|
// it can jump to somewhere nearby.
|
|
//
|
|
// The key is the row, the value is the index in the list of children of the draw
|
|
QMap<int, size_t> row2draw;
|
|
static const int Row2DrawFactor = 100;
|
|
};
|
|
QMap<uint32_t, DrawTreeNode> m_Nodes;
|
|
|
|
bool m_ShowParameterNames = false;
|
|
bool m_ShowAllParameters = false;
|
|
bool m_UseCustomDrawNames = true;
|
|
|
|
// a cache of EID -> row in parent for looking up indices for arbitrary EIDs.
|
|
rdcarray<rdcpair<uint32_t, int>> m_RowInParentCache;
|
|
|
|
// needs to be mutable because we update this inside data()
|
|
mutable QMap<uint32_t, QVariant> m_EIDNameCache;
|
|
|
|
void AccumulateFindResults(QModelIndex root)
|
|
{
|
|
for(int i = 0, count = rowCount(root); i < count; i++)
|
|
{
|
|
QModelIndex idx = index(i, 0, root);
|
|
|
|
// check if there's a match first
|
|
QString name = data(idx).toString();
|
|
|
|
if(name.contains(m_FindString, Qt::CaseInsensitive))
|
|
m_FindResults.push_back(idx);
|
|
|
|
// now recurse
|
|
AccumulateFindResults(idx);
|
|
}
|
|
}
|
|
|
|
void RefreshIcon(QModelIndex idx)
|
|
{
|
|
emit dataChanged(idx.sibling(idx.row(), 0), idx.sibling(idx.row(), COL_COUNT - 1),
|
|
{Qt::DecorationRole, Qt::DisplayRole});
|
|
}
|
|
|
|
QVariant FormatDuration(uint32_t eid) const
|
|
{
|
|
if(m_Times.empty())
|
|
return lit("---");
|
|
|
|
double secs = eid < m_Times.size() ? m_Times[eid] : -1.0;
|
|
|
|
if(secs < 0.0)
|
|
return QVariant();
|
|
|
|
if(m_TimeUnit == TimeUnit::Milliseconds)
|
|
secs *= 1000.0;
|
|
else if(m_TimeUnit == TimeUnit::Microseconds)
|
|
secs *= 1000000.0;
|
|
else if(m_TimeUnit == TimeUnit::Nanoseconds)
|
|
secs *= 1000000000.0;
|
|
|
|
return Formatter::Format(secs);
|
|
}
|
|
|
|
void CalculateTotalDuration(DrawTreeNode &node)
|
|
{
|
|
const rdcarray<DrawcallDescription> &drawChildren =
|
|
node.draw ? node.draw->children : m_Ctx.CurDrawcalls();
|
|
|
|
double duration = 0.0;
|
|
|
|
for(const DrawcallDescription &d : drawChildren)
|
|
{
|
|
// ignore out of bounds EIDs - should not happen
|
|
if(d.eventId >= m_Times.size())
|
|
continue;
|
|
|
|
// add the time for this event, if it's non-negative. Because we fill out nodes in reverse
|
|
// order, any children that are nodes themselves should be populated by now
|
|
duration += qMax(0.0, m_Times[d.eventId]);
|
|
}
|
|
|
|
m_Times[node.draw ? node.draw->eventId : 0] = duration;
|
|
}
|
|
|
|
DrawTreeNode CreateDrawNode(const DrawcallDescription *draw)
|
|
{
|
|
const rdcarray<DrawcallDescription> &drawRange = draw ? draw->children : m_Ctx.CurDrawcalls();
|
|
|
|
// account for the Capture Start row we'll add at the top level
|
|
int row = draw ? 0 : 1;
|
|
|
|
DrawTreeNode ret;
|
|
|
|
ret.draw = draw;
|
|
ret.effectiveEID = drawRange.back().eventId;
|
|
|
|
ret.row2draw[0] = 0;
|
|
|
|
uint32_t row2eidStride = (drawRange.count() / DrawTreeNode::Row2DrawFactor) + 1;
|
|
|
|
const SDFile &sdfile = m_Ctx.GetStructuredFile();
|
|
|
|
for(int i = 0; i < drawRange.count(); i++)
|
|
{
|
|
const DrawcallDescription &d = drawRange[i];
|
|
|
|
if((i % row2eidStride) == 0)
|
|
ret.row2draw[row] = i;
|
|
|
|
for(const APIEvent &e : d.events)
|
|
{
|
|
m_Draws.resize_for_index(e.eventId);
|
|
m_Chunks.resize_for_index(e.eventId);
|
|
m_Draws[e.eventId] = &d;
|
|
if(e.chunkIndex != APIEvent::NoChunk && e.chunkIndex < sdfile.chunks.size())
|
|
m_Chunks[e.eventId] = sdfile.chunks[e.chunkIndex];
|
|
}
|
|
|
|
row += d.events.count();
|
|
|
|
if(d.children.empty())
|
|
continue;
|
|
|
|
DrawTreeNode node = CreateDrawNode(&d);
|
|
|
|
node.index = createIndex(row - 1, 0, d.eventId);
|
|
|
|
if(d.eventId == ret.effectiveEID)
|
|
ret.effectiveEID = node.effectiveEID;
|
|
|
|
m_Nodes[d.eventId] = node;
|
|
}
|
|
|
|
ret.rowCount = row;
|
|
|
|
return ret;
|
|
}
|
|
|
|
QModelIndex GetIndexForDrawChildRow(const DrawTreeNode &node, int row, int column) const
|
|
{
|
|
// if the row is out of bounds, bail
|
|
if(row < 0 || (uint32_t)row >= node.rowCount)
|
|
return QModelIndex();
|
|
|
|
// we do the linear 'counting' within the draws list to find the event at the given row. It's
|
|
// still essentially O(n) however we keep a limited cached of skip entries at each draw node
|
|
// which helps lower the cost significantly (by a factor of the roughly number of skip entries
|
|
// we store).
|
|
|
|
const rdcarray<DrawcallDescription> &draws =
|
|
node.draw ? node.draw->children : m_Ctx.CurDrawcalls();
|
|
int curRow = 0;
|
|
size_t curDraw = 0;
|
|
|
|
// lowerBound doesn't do exactly what we want, we want the first row that is less-equal. So
|
|
// instead we use upperBound and go back one step if we don't get returned the first entry
|
|
auto it = node.row2draw.upperBound(row);
|
|
if(it != node.row2draw.begin())
|
|
--it;
|
|
|
|
// start at the first skip before the desired row
|
|
curRow = it.key();
|
|
curDraw = it.value();
|
|
|
|
if(curRow > row)
|
|
{
|
|
// we should always have a skip, even if it's only the first child
|
|
qCritical() << "Couldn't find skip for" << row << "in draw node"
|
|
<< (node.draw ? node.draw->eventId : 0);
|
|
|
|
// start at the first child row instead
|
|
curRow = 0;
|
|
curDraw = 0;
|
|
}
|
|
|
|
// if this draw doesn't contain the desired row, advance
|
|
while(curDraw < draws.size() && curRow + draws[curDraw].events.count() <= row)
|
|
{
|
|
curRow += draws[curDraw].events.count();
|
|
curDraw++;
|
|
}
|
|
|
|
// we've iterated all the draws and didn't come up with this row - but we checked above that
|
|
// we're in bounds of rowCount so something went wrong
|
|
if(curDraw >= draws.size())
|
|
{
|
|
qCritical() << "Couldn't find draw containing row" << row << "in draw node"
|
|
<< (node.draw ? node.draw->eventId : 0);
|
|
return QModelIndex();
|
|
}
|
|
|
|
// now curDraw contains the row at the idx'th event
|
|
int idx = row - curRow;
|
|
|
|
if(idx < 0 || idx >= draws[curDraw].events.count())
|
|
{
|
|
qCritical() << "Got invalid relative index for row" << row << "in draw node"
|
|
<< (node.draw ? node.draw->eventId : 0);
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(row, column, draws[curDraw].events[idx].eventId);
|
|
}
|
|
|
|
QVariant GetCachedEIDName(uint32_t eid) const
|
|
{
|
|
auto it = m_EIDNameCache.find(eid);
|
|
if(it != m_EIDNameCache.end())
|
|
return it.value();
|
|
|
|
const DrawcallDescription *draw = m_Draws[eid];
|
|
|
|
QString name;
|
|
|
|
// markers always use the draw name no matter what (fake markers must because we don't have a
|
|
// chunk to use to name them). This doesn't apply to 'marker' regions that are multidraws or
|
|
// command buffer boundaries.
|
|
if(draw->eventId == eid)
|
|
{
|
|
if(m_UseCustomDrawNames)
|
|
{
|
|
name = draw->name;
|
|
}
|
|
else
|
|
{
|
|
if((draw->flags & (DrawFlags::SetMarker | DrawFlags::PushMarker)) &&
|
|
!(draw->flags & (DrawFlags::CommandBufferBoundary | DrawFlags::PassBoundary |
|
|
DrawFlags::CmdList | DrawFlags::MultiDraw)))
|
|
name = draw->name;
|
|
}
|
|
}
|
|
|
|
if(name.isEmpty())
|
|
{
|
|
for(const APIEvent &e : draw->events)
|
|
{
|
|
if(e.eventId == eid)
|
|
{
|
|
const SDChunk *chunk = m_Ctx.GetStructuredFile().chunks[e.chunkIndex];
|
|
name = chunk->name;
|
|
|
|
// don't display any "ClassName::" prefix. We keep it for the API inspector which is more
|
|
// verbose
|
|
int nsSep = name.indexOf(lit("::"));
|
|
if(nsSep > 0)
|
|
name.remove(0, nsSep + 2);
|
|
|
|
name += QLatin1Char('(');
|
|
|
|
bool onlyImportant(chunk->type.flags & SDTypeFlags::ImportantChildren);
|
|
|
|
for(size_t i = 0; i < chunk->NumChildren(); i++)
|
|
{
|
|
if(chunk->GetChild(i)->type.flags & SDTypeFlags::Hidden)
|
|
continue;
|
|
|
|
const SDObject *o = chunk->GetChild(i);
|
|
|
|
// never display hidden members
|
|
if(o->type.flags & SDTypeFlags::Hidden)
|
|
continue;
|
|
|
|
if(!onlyImportant || (o->type.flags & SDTypeFlags::Important) || m_ShowAllParameters)
|
|
{
|
|
if(name.at(name.size() - 1) != QLatin1Char('('))
|
|
name += lit(", ");
|
|
|
|
if(m_ShowParameterNames)
|
|
{
|
|
name += lit("<span style='color: %1'>").arg(m_ParamColCode);
|
|
name += o->name;
|
|
name += lit("</span>");
|
|
name += QLatin1Char('=');
|
|
}
|
|
|
|
name += SDObject2Variant(o, true).toString();
|
|
}
|
|
}
|
|
|
|
name += QLatin1Char(')');
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(name.isEmpty())
|
|
qCritical() << "Couldn't find APIEvent for" << eid;
|
|
|
|
// force html even for events that don't reference resources etc, to get the italics for
|
|
// parameters
|
|
if(m_ShowParameterNames)
|
|
name = lit("<rdhtml>") + name + lit("</rdhtml>");
|
|
}
|
|
|
|
if(m_MessageCounts.contains(eid))
|
|
{
|
|
int count = m_MessageCounts[eid];
|
|
if(count > 0)
|
|
name += lit(" __rd_msgs::%1:%2").arg(eid).arg(count);
|
|
}
|
|
|
|
QVariant v = name;
|
|
|
|
RichResourceTextInitialise(v, &m_Ctx);
|
|
|
|
m_EIDNameCache[eid] = v;
|
|
|
|
return v;
|
|
}
|
|
|
|
friend struct EventFilterModel;
|
|
};
|
|
|
|
struct EventFilter
|
|
{
|
|
EventFilter(IEventBrowser::EventFilterCallback c) : callback(c), type(MatchType::Normal) {}
|
|
EventFilter(IEventBrowser::EventFilterCallback c, MatchType t) : callback(c), type(t) {}
|
|
IEventBrowser::EventFilterCallback callback;
|
|
MatchType type;
|
|
};
|
|
|
|
static QMap<QString, DrawFlags> DrawFlagsLookup;
|
|
static QStringList DrawFlagsList;
|
|
|
|
void CacheDrawFlagsLookup()
|
|
{
|
|
if(DrawFlagsLookup.empty())
|
|
{
|
|
for(uint32_t i = 0; i <= 31; i++)
|
|
{
|
|
DrawFlags flag = DrawFlags(1U << i);
|
|
|
|
// bit of a hack, see if it's a valid flag by stringising and seeing if it contains
|
|
// DrawFlags(
|
|
QString str = ToQStr(flag);
|
|
if(str.contains(lit("DrawFlags(")))
|
|
continue;
|
|
|
|
DrawFlagsList.push_back(str);
|
|
DrawFlagsLookup[str.toLower()] = flag;
|
|
}
|
|
|
|
DrawFlagsList.sort();
|
|
}
|
|
}
|
|
|
|
bool EvaluateFilterSet(ICaptureContext &ctx, const rdcarray<EventFilter> &filters, bool all,
|
|
uint32_t eid, const SDChunk *chunk, const DrawcallDescription *draw,
|
|
QString name)
|
|
{
|
|
if(filters.empty())
|
|
return true;
|
|
|
|
bool accept = false;
|
|
|
|
bool anyMust = false;
|
|
for(const EventFilter &filter : filters)
|
|
anyMust |= (filter.type == MatchType::MustMatch);
|
|
|
|
// if there are MustMatch filters we ignore normals for the sake of matching. We also default
|
|
// to acceptance so that if they all pass then we match. This handles cases like +foo -bar which
|
|
// would return the default on a string like "foothing" which would return a false match if we
|
|
// didn't do this
|
|
if(anyMust)
|
|
accept = true;
|
|
|
|
// if any top-level filter matches, we match
|
|
for(const EventFilter &filter : filters)
|
|
{
|
|
// ignore normal filters when we have must matches. Only consider must/can't
|
|
if(anyMust && filter.type == MatchType::Normal)
|
|
continue;
|
|
|
|
// if we've already accepted and this is a normal filter and we are in non-all mode, it won't
|
|
// change the result so don't bother running the filter.
|
|
if(accept && !all && filter.type == MatchType::Normal)
|
|
continue;
|
|
|
|
bool match = filter.callback(&ctx, rdcstr(), rdcstr(), eid, chunk, draw, name);
|
|
|
|
// in normal mode, if it matches it should be included (unless a subsequent filter excludes
|
|
// it)
|
|
if(filter.type == MatchType::Normal)
|
|
{
|
|
if(match)
|
|
accept = true;
|
|
|
|
// in all mode, if any normal filters fails then we fail the whole thing
|
|
if(all && !match)
|
|
return false;
|
|
}
|
|
else if(filter.type == MatchType::CantMatch)
|
|
{
|
|
// if we matched a can't match (e.g -foo) then we can't accept this row
|
|
if(match)
|
|
return false;
|
|
}
|
|
else if(filter.type == MatchType::MustMatch)
|
|
{
|
|
// similarly if we *didn't* match a must match (e.g +foo) then we can't accept this row
|
|
if(!match)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// if we didn't early out with a can't/must failure, then return the result from whatever normal
|
|
// matches we got
|
|
return accept;
|
|
}
|
|
|
|
static const SDObject *FindChildRecursively(const SDObject *parent, rdcstr name)
|
|
{
|
|
const SDObject *o = parent->FindChild(name);
|
|
if(o)
|
|
return o;
|
|
|
|
for(size_t i = 0; i < parent->NumChildren(); i++)
|
|
{
|
|
o = FindChildRecursively(parent->GetChild(i), name);
|
|
if(o)
|
|
return o;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct ParseTrace
|
|
{
|
|
int position = -1;
|
|
int length = 0;
|
|
QString errorText;
|
|
|
|
QVector<FilterExpression> exprs;
|
|
|
|
operator bool() const = delete;
|
|
bool hasErrors() const { return length > 0; }
|
|
ParseTrace &setError(QString text)
|
|
{
|
|
errorText = text;
|
|
return *this;
|
|
}
|
|
void clearErrors()
|
|
{
|
|
errorText.clear();
|
|
position = -1;
|
|
length = 0;
|
|
}
|
|
};
|
|
|
|
struct CustomFilterCallbacks
|
|
{
|
|
QString description;
|
|
IEventBrowser::EventFilterCallback filter;
|
|
IEventBrowser::FilterParseCallback parser;
|
|
IEventBrowser::AutoCompleteCallback completer;
|
|
};
|
|
|
|
struct BuiltinFilterCallbacks
|
|
{
|
|
QString description;
|
|
std::function<IEventBrowser::EventFilterCallback(QString, QString, ParseTrace &)> makeFilter;
|
|
IEventBrowser::AutoCompleteCallback completer;
|
|
};
|
|
|
|
struct EventFilterModel : public QSortFilterProxyModel
|
|
{
|
|
public:
|
|
EventFilterModel(EventItemModel *model, ICaptureContext &ctx) : m_Model(model), m_Ctx(ctx)
|
|
{
|
|
setSourceModel(m_Model);
|
|
|
|
if(m_BuiltinFilters.empty())
|
|
{
|
|
#ifndef STRINGIZE
|
|
#define STRINGIZE2(a) #a
|
|
#define STRINGIZE(a) STRINGIZE2(a)
|
|
#endif
|
|
|
|
#define MAKE_BUILTIN_FILTER(filter_name) \
|
|
m_BuiltinFilters[lit(STRINGIZE(filter_name))].makeFilter = [this]( \
|
|
QString name, QString parameters, ParseTrace &trace) { \
|
|
return filterFunction_##filter_name(name, parameters, trace); \
|
|
}; \
|
|
m_BuiltinFilters[lit(STRINGIZE(filter_name))].description = \
|
|
filterDescription_##filter_name().trimmed();
|
|
|
|
MAKE_BUILTIN_FILTER(regex);
|
|
MAKE_BUILTIN_FILTER(param);
|
|
// MAKE_BUILTIN_FILTER(event);
|
|
MAKE_BUILTIN_FILTER(draw);
|
|
MAKE_BUILTIN_FILTER(dispatch);
|
|
MAKE_BUILTIN_FILTER(childOf);
|
|
MAKE_BUILTIN_FILTER(parent);
|
|
|
|
/*
|
|
m_BuiltinFilters[lit("event")].completer = [this](ICaptureContext *ctx, QString name,
|
|
QString parameters) {
|
|
return filterCompleter_event(ctx, name, parameters);
|
|
};
|
|
*/
|
|
m_BuiltinFilters[lit("draw")].completer = [this](ICaptureContext *ctx, QString name,
|
|
QString parameters) {
|
|
return filterCompleter_draw(ctx, name, parameters);
|
|
};
|
|
m_BuiltinFilters[lit("dispatch")].completer = [this](ICaptureContext *ctx, QString name,
|
|
QString parameters) {
|
|
return filterCompleter_dispatch(ctx, name, parameters);
|
|
};
|
|
m_BuiltinFilters[lit("childOf")].completer = m_BuiltinFilters[lit("parent")].completer =
|
|
[this](ICaptureContext *ctx, QString name, QString parameters) {
|
|
return m_Model->GetMarkerList();
|
|
};
|
|
}
|
|
}
|
|
void ResetCache() { m_VisibleCache.clear(); }
|
|
ParseTrace ParseExpressionToFilters(QString expr, rdcarray<EventFilter> &filters) const;
|
|
|
|
void SetFilters(const rdcarray<EventFilter> &filters)
|
|
{
|
|
m_VisibleCache.clear();
|
|
m_Filters = filters;
|
|
invalidateFilter();
|
|
}
|
|
|
|
QString GetDescription(QString filter)
|
|
{
|
|
if(m_BuiltinFilters.contains(filter))
|
|
return m_BuiltinFilters[filter].description;
|
|
else if(m_CustomFilters.contains(filter))
|
|
return m_CustomFilters[filter].description;
|
|
|
|
return tr("Unknown filter $%1()", "EventFilterModel").arg(filter);
|
|
}
|
|
|
|
QStringList GetCompletions(QString filter, QString params)
|
|
{
|
|
IEventBrowser::AutoCompleteCallback cb;
|
|
|
|
if(m_BuiltinFilters.contains(filter))
|
|
cb = m_BuiltinFilters[filter].completer;
|
|
else if(m_CustomFilters.contains(filter))
|
|
cb = m_CustomFilters[filter].completer;
|
|
|
|
rdcarray<rdcstr> ret;
|
|
|
|
if(cb)
|
|
ret = m_BuiltinFilters[filter].completer(&m_Ctx, filter, params);
|
|
|
|
QStringList qret;
|
|
for(const rdcstr &s : ret)
|
|
qret << s;
|
|
return qret;
|
|
}
|
|
|
|
QStringList GetFunctions()
|
|
{
|
|
QStringList ret = m_BuiltinFilters.keys();
|
|
ret << m_CustomFilters.keys();
|
|
ret.sort();
|
|
return ret;
|
|
}
|
|
|
|
void SetEmptyRegionsVisible(bool visible) { m_EmptyRegionsVisible = visible; }
|
|
static bool RegisterEventFilterFunction(const rdcstr &name, const rdcstr &description,
|
|
IEventBrowser::EventFilterCallback filter,
|
|
IEventBrowser::FilterParseCallback parser,
|
|
IEventBrowser::AutoCompleteCallback completer)
|
|
{
|
|
if(m_BuiltinFilters.contains(name))
|
|
{
|
|
qCritical() << "Registering filter function" << QString(name)
|
|
<< "which is a builtin function.";
|
|
return false;
|
|
}
|
|
|
|
if(m_CustomFilters[name].filter != NULL)
|
|
{
|
|
qCritical() << "Registering filter function" << QString(name)
|
|
<< "which is already registered.";
|
|
return false;
|
|
}
|
|
|
|
m_CustomFilters[name] = {description, filter, parser, completer};
|
|
return true;
|
|
}
|
|
|
|
static bool UnregisterEventFilterFunction(const rdcstr &name)
|
|
{
|
|
m_CustomFilters.remove(name);
|
|
return true;
|
|
}
|
|
|
|
protected:
|
|
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
|
|
{
|
|
// manually implement recursive filtering since older Qt versions don't support it
|
|
if(filterAcceptsSingleRow(source_row, source_parent))
|
|
return true;
|
|
|
|
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
|
|
for(int i = 0, row_count = sourceModel()->rowCount(idx); i < row_count; i++)
|
|
if(filterAcceptsRow(i, idx))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
virtual bool filterAcceptsSingleRow(int source_row, const QModelIndex &source_parent) const
|
|
{
|
|
// always include capture start and the root
|
|
if(!source_parent.isValid() ||
|
|
(source_parent.internalId() == EventItemModel::TagRoot && source_row == 0))
|
|
return true;
|
|
|
|
QModelIndex source_idx = sourceModel()->index(source_row, 0, source_parent);
|
|
|
|
// since we have recursive filtering enabled (so a child keeps its parent visible, to allow
|
|
// filtering for specific things and not hiding the hierarchy along the way that doesn't
|
|
// match), we can implement hiding of empty regions by simply forcing anything with children
|
|
// to not match. That way it will only be included if at least one child matches
|
|
if(!m_EmptyRegionsVisible && sourceModel()->rowCount(source_idx) > 0)
|
|
return false;
|
|
|
|
uint32_t eid = sourceModel()->data(source_idx, ROLE_SELECTED_EID).toUInt();
|
|
|
|
m_VisibleCache.resize_for_index(eid);
|
|
if(m_VisibleCache[eid] != 0)
|
|
return m_VisibleCache[eid] > 0;
|
|
|
|
const SDChunk *chunk =
|
|
(const SDChunk *)(sourceModel()->data(source_idx, ROLE_CHUNK).toULongLong());
|
|
const DrawcallDescription *draw =
|
|
(const DrawcallDescription
|
|
*)(sourceModel()->data(source_idx, ROLE_GROUPED_DRAWCALL).toULongLong());
|
|
QString name = source_idx.data(Qt::DisplayRole).toString();
|
|
|
|
int off = name.indexOf(QLatin1Char('<'));
|
|
while(off >= 0 && off + 4 < name.size())
|
|
{
|
|
if(name[off + 1] == QLatin1Char('/') || name.midRef(off, 5) == lit("<span"))
|
|
{
|
|
int end = name.indexOf(QLatin1Char('>'), off);
|
|
name.remove(off, end - off + 1);
|
|
}
|
|
|
|
off = name.indexOf(QLatin1Char('<'), off + 1);
|
|
}
|
|
|
|
m_VisibleCache[eid] = EvaluateFilterSet(m_Ctx, m_Filters, false, eid, chunk, draw, name);
|
|
return m_VisibleCache[eid] > 0;
|
|
}
|
|
|
|
private:
|
|
ICaptureContext &m_Ctx;
|
|
|
|
// this caches whether a row passes or not, with 0 meaning 'uncached' and 1/-1 being true or
|
|
// false. This means we can resize it and know that we don't pollute with bad data.
|
|
// it must be mutable since we update it in a const function filterAcceptsRow.
|
|
mutable rdcarray<int8_t> m_VisibleCache;
|
|
|
|
EventItemModel *m_Model = NULL;
|
|
|
|
bool m_EmptyRegionsVisible = true;
|
|
rdcarray<EventFilter> m_Filters;
|
|
|
|
IEventBrowser::EventFilterCallback MakeLiteralMatcher(QString string) const
|
|
{
|
|
QString matchString = string.toLower();
|
|
return [matchString](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t,
|
|
const SDChunk *, const DrawcallDescription *, const rdcstr &name) {
|
|
return QString(name).toLower().contains(matchString);
|
|
};
|
|
}
|
|
|
|
struct Token
|
|
{
|
|
int position;
|
|
int length;
|
|
QString text;
|
|
};
|
|
|
|
static QList<Token> tokenise(QString parameters)
|
|
{
|
|
QList<Token> ret;
|
|
|
|
const QString operatorChars = lit("<>=:&");
|
|
|
|
int p = 0;
|
|
while(p < parameters.size())
|
|
{
|
|
if(parameters[p].isSpace())
|
|
{
|
|
p++;
|
|
continue;
|
|
}
|
|
|
|
Token t;
|
|
t.position = p;
|
|
|
|
bool tokenIsOperator = operatorChars.contains(parameters[p]);
|
|
|
|
while(p < parameters.size() && !parameters[p].isSpace() &&
|
|
operatorChars.contains(parameters[p]) == tokenIsOperator)
|
|
p++;
|
|
t.length = p - t.position;
|
|
t.text = parameters.mid(t.position, t.length);
|
|
ret.push_back(t);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString filterDescription_regex() const
|
|
{
|
|
return tr(R"EOD(
|
|
$regex(/my regex.*match/i) - passes if the specified regex matches the event name.
|
|
|
|
This filter can be used to do regex matching against events. The regex itself must
|
|
be surrounded with //s. The syntax is perl-like and supports perl compatible options
|
|
after the trailing /:
|
|
/i - Case insensitive match
|
|
/x - Extended syntax. See regex documentation for more information
|
|
/u - Use unicode properties. Character classes like \w and \d match more than ASCII
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_regex(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
parameters = parameters.trimmed();
|
|
|
|
if(parameters.size() < 2 || parameters[0] != QLatin1Char('/'))
|
|
{
|
|
trace.setError(tr("Parameter to to regex() should be /regex/", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
int end = 0;
|
|
|
|
for(int i = 1; i < parameters.size(); i++)
|
|
{
|
|
if(parameters[i] == QLatin1Char('\\'))
|
|
{
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if(parameters[i] == QLatin1Char('/'))
|
|
{
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(end == 0)
|
|
{
|
|
trace.setError(tr("Unterminated regex", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
QString opts = parameters.right(parameters.size() - end - 1);
|
|
|
|
QRegularExpression::PatternOptions reOpts = QRegularExpression::NoPatternOption;
|
|
|
|
for(QChar c : opts)
|
|
{
|
|
switch(c.toLatin1())
|
|
{
|
|
case 'i': reOpts |= QRegularExpression::CaseInsensitiveOption; break;
|
|
case 'x': reOpts |= QRegularExpression::ExtendedPatternSyntaxOption; break;
|
|
case 'u': reOpts |= QRegularExpression::UseUnicodePropertiesOption; break;
|
|
default:
|
|
trace.setError(tr("Unexpected option '%1' after regex", "EventFilterModel").arg(c));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
QRegularExpression regex(parameters.mid(1, end - 1));
|
|
|
|
if(!regex.isValid())
|
|
{
|
|
trace.setError(tr("Invalid regex", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
return [regex](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t, const SDChunk *,
|
|
const DrawcallDescription *, const rdcstr &name) {
|
|
QRegularExpressionMatch match = regex.match(QString(name));
|
|
return match.isValid() && match.hasMatch();
|
|
};
|
|
}
|
|
|
|
QString filterDescription_param() const
|
|
{
|
|
return tr(R"EOD(
|
|
$param(name: value)
|
|
$param(name = value) - passes if a given parameter matches a value.
|
|
|
|
This filter searches through the parameters to each API call to find a matching name.
|
|
The name is specified case-sensitive and can be at any nesting level. The value is
|
|
searched for as a case-insensitive substring.
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_param(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
// $param() => check for a named parameter having a particular value
|
|
int idx = parameters.indexOf(QLatin1Char(':'));
|
|
|
|
if(idx < 0)
|
|
idx = parameters.indexOf(QLatin1Char('='));
|
|
|
|
if(idx < 0)
|
|
{
|
|
trace.setError(
|
|
tr("Parameter to to $param() should be name: value or name = value", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
QString paramName = parameters.mid(0, idx).trimmed();
|
|
QString paramValue = parameters.mid(idx + 1).trimmed();
|
|
|
|
if(paramValue.isEmpty())
|
|
{
|
|
trace.setError(
|
|
tr("Parameter to to $param() should be name: value or name = value", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
return
|
|
[paramName, paramValue](ICaptureContext *ctx, const rdcstr &, const rdcstr &, uint32_t,
|
|
const SDChunk *chunk, const DrawcallDescription *, const rdcstr &) {
|
|
if(!chunk)
|
|
return false;
|
|
|
|
const SDObject *o = FindChildRecursively(chunk, paramName);
|
|
|
|
if(!o)
|
|
return false;
|
|
|
|
if(o->IsArray())
|
|
{
|
|
for(const SDObject *c : *o)
|
|
{
|
|
if(RichResourceTextFormat(*ctx, SDObject2Variant(c, false))
|
|
.contains(paramValue, Qt::CaseInsensitive))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return RichResourceTextFormat(*ctx, SDObject2Variant(o, false))
|
|
.contains(paramValue, Qt::CaseInsensitive);
|
|
}
|
|
|
|
};
|
|
}
|
|
|
|
QString filterDescription_event() const
|
|
{
|
|
return tr(R"EOD(
|
|
$event(condition) - passes if an event property matches a condition.
|
|
|
|
This filter queries given properties of an event to match simple conditions. A
|
|
condition must be specified, and only one condition can be queried in each $event().
|
|
|
|
Available numeric properties. Compare with $event(prop > 100) or $event(prop <= 200)
|
|
|
|
EID: The event's EID.
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
rdcarray<rdcstr> filterCompleter_event(ICaptureContext *ctx, const rdcstr &name,
|
|
const rdcstr ¶ms)
|
|
{
|
|
QList<Token> tokens = tokenise(params);
|
|
|
|
if(tokens.size() <= 1)
|
|
return {"EID"};
|
|
|
|
return {};
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_event(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
// $event(...) => filters on any event property (at the moment only EID)
|
|
QList<Token> tokens = tokenise(parameters);
|
|
|
|
static const QStringList operators = {
|
|
lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
if(tokens.size() < 1)
|
|
{
|
|
trace.setError(tr("Filter parameters required", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
if(tokens[0].text.toLower() != lit("eid") && tokens[0].text.toLower() != lit("eventid"))
|
|
{
|
|
trace.position = tokens[0].position;
|
|
trace.length = tokens[0].length;
|
|
trace.setError(tr("Unrecognised event property", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
int operatorIdx = operators.indexOf(tokens[1].text);
|
|
|
|
if(tokens[1].text == lit("="))
|
|
operatorIdx = 0;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
uint32_t eid = tokens[2].text.toUInt(&ok, 0);
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected integer", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId == eid; };
|
|
case 1:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId != eid; };
|
|
case 2:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId < eid; };
|
|
case 3:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId > eid; };
|
|
case 4:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId <= eid; };
|
|
case 5:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return eventId >= eid; };
|
|
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
|
|
}
|
|
}
|
|
|
|
QString filterDescription_draw() const
|
|
{
|
|
return tr(R"EOD(
|
|
$draw() - passes if an event is a drawcall.
|
|
$draw(condition) - passes if an event is a drawcall and matches a condition.
|
|
|
|
This filter applies to draw-type events.
|
|
|
|
If no condition is specified then the event is just included if it's a draw.
|
|
Otherwise the event is included if it's a draw AND if the condition is true.
|
|
|
|
Available numeric properties. Compare with $draw(prop > 100) or $draw(prop <= 200)
|
|
|
|
EID: The draw's EID.
|
|
drawId: The draw ID, starting from 1 and numbering each draw.
|
|
numIndices: The number of vertices or indices in a draw.
|
|
baseVertex: The base vertex value for an indexed draw.
|
|
indexOffset: The index offset for an indexed draw.
|
|
vertexOffset: The vertex offset for a non-indexed draw.
|
|
instanceOffset: The instance offset for an instanced draw.
|
|
dispatchX: The number of groups in the X dimension of a dispatch.
|
|
dispatchY: The number of groups in the Y dimension of a dispatch.
|
|
dispatchZ: The number of groups in the Z dimension of a dispatch.
|
|
dispatchSize: The total number of groups (X * Y * Z) of a dispatch.
|
|
duration: The listed duration of a drawcall (only available with durations).
|
|
|
|
Also available is the 'flags' property. Drawcalls have different flags and properties
|
|
and these can be queried with a filter such as $draw(flags & Clear|ClearDepthStencil)
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
rdcarray<rdcstr> filterCompleter_draw(ICaptureContext *ctx, const rdcstr &name, const rdcstr ¶ms)
|
|
{
|
|
QList<Token> tokens = tokenise(params);
|
|
|
|
if(tokens.size() <= 1)
|
|
return {
|
|
"EID", "drawId", "numIndices",
|
|
// most aliases we don't autocomplete but this one we leave
|
|
"numVertices", "numInstances", "baseVertex", "indexOffset", "vertexOffset",
|
|
"instanceOffset", "dispatchX", "dispatchY", "dispatchZ", "dispatchSize", "duration",
|
|
"flags",
|
|
};
|
|
|
|
if(tokens[0].text == lit("flags") && tokens.size() >= 2)
|
|
{
|
|
CacheDrawFlagsLookup();
|
|
|
|
rdcarray<rdcstr> flags;
|
|
|
|
for(QString s : DrawFlagsList)
|
|
flags.push_back(s);
|
|
|
|
return flags;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_draw(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
// $draw(...) => returns true only for draws, optionally with a particular property filter
|
|
// PopMarker draws are draws so they can contain the other non-draw events that might trail
|
|
// in a group, but we don't count them as real draws for any of this filtering except for flags
|
|
// filtering, where they can be manually included
|
|
QList<Token> tokens = tokenise(parameters);
|
|
|
|
// no parameters, just return if it's a draw
|
|
if(tokens.isEmpty())
|
|
return [](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker);
|
|
};
|
|
|
|
// we upcast to int64_t so we can compare both unsigned and signed values without losing any
|
|
// precision (we don't have any uint64_ts to compare)
|
|
using PropGetter = int64_t (*)(const DrawcallDescription *);
|
|
|
|
struct NamedProp
|
|
{
|
|
const char *name;
|
|
PropGetter getter;
|
|
};
|
|
|
|
#define NAMED_PROP(name, access) \
|
|
{ \
|
|
name, [](const DrawcallDescription *draw) -> int64_t { return access; } \
|
|
}
|
|
|
|
static const NamedProp namedProps[] = {
|
|
NAMED_PROP("eventid", draw->eventId), NAMED_PROP("eid", draw->eventId),
|
|
NAMED_PROP("drawcallid", draw->drawcallId), NAMED_PROP("drawid", draw->drawcallId),
|
|
NAMED_PROP("numindices", draw->numIndices), NAMED_PROP("numindexes", draw->numIndices),
|
|
NAMED_PROP("numvertices", draw->numIndices), NAMED_PROP("numvertexes", draw->numIndices),
|
|
NAMED_PROP("indexcount", draw->numIndices), NAMED_PROP("vertexcount", draw->numIndices),
|
|
NAMED_PROP("numinstances", draw->numInstances),
|
|
NAMED_PROP("instancecount", draw->numInstances), NAMED_PROP("basevertex", draw->baseVertex),
|
|
NAMED_PROP("indexoffset", draw->indexOffset), NAMED_PROP("vertexoffset", draw->vertexOffset),
|
|
NAMED_PROP("instanceoffset", draw->instanceOffset),
|
|
NAMED_PROP("dispatchx", draw->dispatchDimension[0]),
|
|
NAMED_PROP("dispatchy", draw->dispatchDimension[1]),
|
|
NAMED_PROP("dispatchz", draw->dispatchDimension[2]),
|
|
NAMED_PROP("dispatchsize", draw->dispatchDimension[0] * draw->dispatchDimension[1] *
|
|
draw->dispatchDimension[2]),
|
|
};
|
|
|
|
QByteArray prop = tokens[0].text.toLower().toLatin1();
|
|
|
|
int numericPropIndex = -1;
|
|
for(size_t i = 0; i < ARRAY_COUNT(namedProps); i++)
|
|
{
|
|
if(prop == namedProps[i].name)
|
|
numericPropIndex = (int)i;
|
|
}
|
|
|
|
// any numeric value that can be compared to a number
|
|
if(numericPropIndex >= 0)
|
|
{
|
|
PropGetter propGetter = namedProps[numericPropIndex].getter;
|
|
|
|
static const QStringList operators = {
|
|
lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
int operatorIdx = tokens.size() > 1 ? operators.indexOf(tokens[1].text) : -1;
|
|
|
|
if(tokens.size() > 1 && tokens[1].text == lit("="))
|
|
operatorIdx = 0;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
int64_t value = tokens[2].text.toLongLong(&ok, 0);
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected integer", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) == value;
|
|
};
|
|
case 1:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) != value;
|
|
};
|
|
case 2:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) < value;
|
|
};
|
|
case 3:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) > value;
|
|
};
|
|
case 4:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) <= value;
|
|
};
|
|
case 5:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && !(draw->flags & DrawFlags::PopMarker) &&
|
|
propGetter(draw) >= value;
|
|
};
|
|
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
|
|
}
|
|
}
|
|
else if(tokens[0].text.toLower() == lit("duration"))
|
|
{
|
|
// deliberately don't allow equality/inequality
|
|
static const QStringList operators = {
|
|
lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
int operatorIdx = tokens.size() > 1 ? operators.indexOf(tokens[1].text) : -1;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
// multiplier to change the read value into nanoseconds
|
|
double mult = 1.0;
|
|
if(tokens[2].text.endsWith(lit("ms")))
|
|
{
|
|
mult = 1000000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("us")))
|
|
{
|
|
mult = 1000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("ns")))
|
|
{
|
|
mult = 1.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("s")))
|
|
{
|
|
mult = 1000000000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 1);
|
|
}
|
|
else
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(
|
|
tr("Duration must be suffixed with one of s, ms, us, or ns", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
double value = tokens[2].text.toDouble(&ok);
|
|
|
|
// if it doesn't read as a double, try as an integer
|
|
if(!ok)
|
|
{
|
|
int64_t valInt = tokens[2].text.toLongLong(&ok);
|
|
|
|
if(ok)
|
|
value = double(valInt);
|
|
}
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected duration suffixed with one of s, ms, us, or ns",
|
|
"EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
int64_t nanoValue = int64_t(value * mult);
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return dur >= 0.0 && int64_t(dur * 1000000000.0) < nanoValue;
|
|
};
|
|
case 1:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return dur >= 0.0 && int64_t(dur * 1000000000.0) > nanoValue;
|
|
};
|
|
case 2:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return dur >= 0.0 && int64_t(dur * 1000000000.0) <= nanoValue;
|
|
};
|
|
case 3:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return dur >= 0.0 && int64_t(dur * 1000000000.0) >= nanoValue;
|
|
};
|
|
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
|
|
}
|
|
}
|
|
else if(tokens[0].text.toLower() == lit("flags"))
|
|
{
|
|
if(tokens.size() < 3 || (tokens[1].text != lit("&") && tokens[1].text != lit("=")))
|
|
{
|
|
trace.position = tokens[0].position;
|
|
trace.length = (tokens[1].position + tokens[1].length) - trace.position + 1;
|
|
trace.setError(tr("Expected $draw(flags & ...)", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
// there could be whitespace in the flags list so iterate over all remaining parameters (as
|
|
// split by whitespace)
|
|
QStringList flagStrings;
|
|
for(int i = 2; i < tokens.count(); i++)
|
|
flagStrings.append(tokens[i].text.split(QLatin1Char('|'), QString::KeepEmptyParts));
|
|
|
|
// if we have an empty string in the list somewhere that means the | list was broken
|
|
if(flagStrings.contains(QString()))
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = (tokens.back().position + tokens.back().length) - trace.position + 1;
|
|
trace.setError(tr("Invalid draw flags expression", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
CacheDrawFlagsLookup();
|
|
|
|
DrawFlags flags = DrawFlags::NoFlags;
|
|
for(const QString &flagString : flagStrings)
|
|
{
|
|
auto it = DrawFlagsLookup.find(flagString.toLower());
|
|
if(it == DrawFlagsLookup.end())
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = (tokens.back().position + tokens.back().length) - trace.position + 1;
|
|
trace.setError(tr("Unrecognised draw flag '%1'", "EventFilterModel").arg(flagString));
|
|
return NULL;
|
|
}
|
|
|
|
flags |= it.value();
|
|
}
|
|
|
|
return [flags](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw,
|
|
const rdcstr &) { return draw->eventId == eventId && (draw->flags & flags); };
|
|
}
|
|
else
|
|
{
|
|
trace.setError(tr("Unrecognised property expression", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
QString filterDescription_dispatch() const
|
|
{
|
|
return tr(R"EOD(
|
|
$dispatch() - passes if an event is a dispatch.
|
|
$dispatch(condition) - passes if an event is a dispatch and matches a condition.
|
|
|
|
This filter applies to compute dispatches.
|
|
|
|
If no condition is specified then the event is just included if it's a dispatch.
|
|
Otherwise the event is included if it's a dispatch AND if the condition is true.
|
|
|
|
Available numeric properties. Compare with $dispatch(prop > 100) or $dispatch(prop <= 200)
|
|
|
|
EID: The draw's EID.
|
|
drawId: The draw ID, starting from 1 and numbering each draw.
|
|
x: The number of groups in the X dimension of a dispatch.
|
|
y: The number of groups in the Y dimension of a dispatch.
|
|
z: The number of groups in the Z dimension of a dispatch.
|
|
size: The total number of groups (X * Y * Z) of a dispatch.
|
|
duration: The listed duration of a drawcall (only available with durations).
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
rdcarray<rdcstr> filterCompleter_dispatch(ICaptureContext *ctx, const rdcstr &name,
|
|
const rdcstr ¶ms)
|
|
{
|
|
QList<Token> tokens = tokenise(params);
|
|
|
|
if(tokens.size() <= 1)
|
|
return {
|
|
"EID", "drawcallId", "drawId", "x", "y", "z", "size", "duration",
|
|
};
|
|
|
|
return {};
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_dispatch(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
// $dispatch(...) => returns true only for dispatches, optionally with a particular property
|
|
// filter
|
|
QList<Token> tokens = tokenise(parameters);
|
|
|
|
// no parameters, just return if it's a draw
|
|
if(tokens.isEmpty())
|
|
return [](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch);
|
|
};
|
|
|
|
// we upcast to int64_t so we can compare both unsigned and signed values without losing any
|
|
// precision (we don't have any uint64_ts to compare)
|
|
using PropGetter = int64_t (*)(const DrawcallDescription *);
|
|
|
|
struct NamedProp
|
|
{
|
|
const char *name;
|
|
PropGetter getter;
|
|
};
|
|
|
|
static const NamedProp namedProps[] = {
|
|
NAMED_PROP("eventid", draw->eventId), NAMED_PROP("eid", draw->eventId),
|
|
NAMED_PROP("drawcallid", draw->drawcallId), NAMED_PROP("drawid", draw->drawcallId),
|
|
NAMED_PROP("dispatchx", draw->dispatchDimension[0]),
|
|
NAMED_PROP("dispatchy", draw->dispatchDimension[1]),
|
|
NAMED_PROP("dispatchz", draw->dispatchDimension[2]),
|
|
NAMED_PROP("x", draw->dispatchDimension[0]), NAMED_PROP("y", draw->dispatchDimension[1]),
|
|
NAMED_PROP("z", draw->dispatchDimension[2]),
|
|
NAMED_PROP("dispatchsize", draw->dispatchDimension[0] * draw->dispatchDimension[1] *
|
|
draw->dispatchDimension[2]),
|
|
NAMED_PROP("size", draw->dispatchDimension[0] * draw->dispatchDimension[1] *
|
|
draw->dispatchDimension[2]),
|
|
};
|
|
|
|
QByteArray prop = tokens[0].text.toLower().toLatin1();
|
|
|
|
int numericPropIndex = -1;
|
|
for(size_t i = 0; i < ARRAY_COUNT(namedProps); i++)
|
|
{
|
|
if(prop == namedProps[i].name)
|
|
numericPropIndex = (int)i;
|
|
}
|
|
|
|
// any numeric value that can be compared to a number
|
|
if(numericPropIndex >= 0)
|
|
{
|
|
PropGetter propGetter = namedProps[numericPropIndex].getter;
|
|
|
|
static const QStringList operators = {
|
|
lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
int operatorIdx = tokens.size() > 1 ? operators.indexOf(tokens[1].text) : -1;
|
|
|
|
if(tokens.size() > 1 && tokens[1].text == lit("="))
|
|
operatorIdx = 0;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
int64_t value = tokens[2].text.toLongLong(&ok, 0);
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected integer", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) == value;
|
|
};
|
|
case 1:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) != value;
|
|
};
|
|
case 2:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) < value;
|
|
};
|
|
case 3:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) > value;
|
|
};
|
|
case 4:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) <= value;
|
|
};
|
|
case 5:
|
|
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
|
|
uint32_t eventId, const SDChunk *,
|
|
const DrawcallDescription *draw, const rdcstr &) {
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
propGetter(draw) >= value;
|
|
};
|
|
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
|
|
}
|
|
}
|
|
else if(tokens[0].text.toLower() == lit("duration"))
|
|
{
|
|
// deliberately don't allow equality/inequality
|
|
static const QStringList operators = {
|
|
lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
int operatorIdx = tokens.size() > 1 ? operators.indexOf(tokens[1].text) : -1;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
// multiplier to change the read value into nanoseconds
|
|
double mult = 1.0;
|
|
if(tokens[2].text.endsWith(lit("ms")))
|
|
{
|
|
mult = 1000000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("us")))
|
|
{
|
|
mult = 1000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("ns")))
|
|
{
|
|
mult = 1.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 2);
|
|
}
|
|
else if(tokens[2].text.endsWith(lit("s")))
|
|
{
|
|
mult = 1000000000.0;
|
|
tokens[2].text.resize(tokens[2].text.size() - 1);
|
|
}
|
|
else
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(
|
|
tr("Duration must be suffixed with one of s, ms, us, or ns", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
double value = tokens[2].text.toDouble(&ok);
|
|
|
|
// if it doesn't read as a double, try as an integer
|
|
if(!ok)
|
|
{
|
|
int64_t valInt = tokens[2].text.toLongLong(&ok);
|
|
|
|
if(ok)
|
|
value = double(valInt);
|
|
}
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected duration suffixed with one of s, ms, us, or ns",
|
|
"EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
int64_t nanoValue = int64_t(value * mult);
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
dur >= 0.0 && int64_t(dur * 1000000000.0) < nanoValue;
|
|
};
|
|
case 1:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
dur >= 0.0 && int64_t(dur * 1000000000.0) > nanoValue;
|
|
};
|
|
case 2:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
dur >= 0.0 && int64_t(dur * 1000000000.0) <= nanoValue;
|
|
};
|
|
case 3:
|
|
return
|
|
[this, nanoValue](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
double dur = m_Model->GetSecondsDurationForEID(eventId);
|
|
return draw->eventId == eventId && (draw->flags & DrawFlags::Dispatch) &&
|
|
dur >= 0.0 && int64_t(dur * 1000000000.0) >= nanoValue;
|
|
};
|
|
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
trace.setError(tr("Unrecognised property expression", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
QString filterDescription_parent() const
|
|
{
|
|
return tr(R"EOD(
|
|
$parent(marker)
|
|
$childOf(marker) - passes if an event is contained somewhere under a marker of the given name
|
|
|
|
Both aliases of this filter function in the same way. Any event that is a child of the given markers
|
|
will pass the filter. This applies not just to immediate children but any grandchildren or further
|
|
nested.
|
|
)EOD",
|
|
"EventFilterModel");
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_parent(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
QString markerName = parameters.trimmed();
|
|
|
|
return [markerName](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const DrawcallDescription *draw, const rdcstr &) {
|
|
while(draw->parent)
|
|
{
|
|
if(QString(draw->parent->name).contains(markerName, Qt::CaseInsensitive))
|
|
return true;
|
|
|
|
draw = draw->parent;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
QString filterDescription_childOf() const { return filterDescription_parent(); }
|
|
IEventBrowser::EventFilterCallback filterFunction_childOf(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
return filterFunction_parent(name, parameters, trace);
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback MakeFunctionMatcher(QString name, QString parameters,
|
|
ParseTrace &trace) const
|
|
{
|
|
if(m_BuiltinFilters.contains(name))
|
|
{
|
|
return m_BuiltinFilters[name].makeFilter(name, parameters, trace);
|
|
}
|
|
|
|
if(m_CustomFilters.contains(name))
|
|
{
|
|
IEventBrowser::EventFilterCallback innerFilter = m_CustomFilters[name].filter;
|
|
IEventBrowser::FilterParseCallback innerParser = m_CustomFilters[name].parser;
|
|
|
|
rdcstr n = name;
|
|
rdcstr p = parameters;
|
|
|
|
QString errString;
|
|
|
|
if(innerParser)
|
|
errString = innerParser(&m_Ctx, n, p);
|
|
|
|
if(!errString.isEmpty())
|
|
{
|
|
trace.setError(errString);
|
|
return NULL;
|
|
}
|
|
|
|
return [n, p, innerFilter](ICaptureContext *ctx, const rdcstr &, const rdcstr &, uint32_t eid,
|
|
const SDChunk *chunk, const DrawcallDescription *draw,
|
|
const rdcstr &name) {
|
|
return innerFilter(ctx, n, p, eid, chunk, draw, name);
|
|
};
|
|
}
|
|
|
|
trace.setError(tr("Unknown filter function", "EventFilterModel").arg(name));
|
|
return NULL;
|
|
}
|
|
|
|
// static so we don't lose this when the event browser is closed and the model is deleted. We
|
|
// could store this in the capture context but it makes more sense logically to keep it here.
|
|
static QMap<QString, CustomFilterCallbacks> m_CustomFilters;
|
|
static QMap<QString, BuiltinFilterCallbacks> m_BuiltinFilters;
|
|
};
|
|
|
|
ParseTrace EventFilterModel::ParseExpressionToFilters(QString expr, rdcarray<EventFilter> &filters) const
|
|
{
|
|
// we have a simple grammar, we pick out subexpressions and they're all independent.
|
|
//
|
|
// - Individual words are literal subexpressions on their own and end with whitespace.
|
|
// - A " starts a quoted literal subexpression, which is a literal string and ends on the next
|
|
// ". It supports escaping " with \ and otherwise any escaped character is the literal
|
|
// character. The quotes must be followed by whitespace or the end of the expression.
|
|
// - Both literal subexpressions are matched case-insensitively. For case-sensitive matching
|
|
// you can use a regexp as a functional expression.
|
|
// - A $ starts a functional expression, with parameters in brackets. The name starts with a
|
|
// letter and contains alphanumeric and . characters. The name is followed by a ( to begin the
|
|
// parameters and ends when the matching ) is found. Whitespace between the name and the ( is
|
|
// ignored, but any other character is a parse error. The ) must be followed by whitespace or
|
|
// the end of the expression or that's a parse error. Note that whitespace is allowed in the
|
|
// parameters.
|
|
// - We allow nesting with simple () brackets. Unquoted literals cannot contain a ( to avoid
|
|
// parsing ambiguity between e.g. Foo(Bar Blah) and Foo (Bar Blah).
|
|
//
|
|
// This means there's a simple state machine we can follow from any point.
|
|
//
|
|
// 1) start -> whitespace -> start
|
|
// 2) start -> " -> quoted_expression -> wait until unescaped " -> start
|
|
// 3) start -> $ -> function_expression -> parse name > optional whitespace ->
|
|
// ( -> wait until matching parenthesis -> ) -> whitespace -> start
|
|
// 4) start -> ( -> nested_expression -> wait until matching unquoted ) -> start
|
|
// 5) start -> anything else -> literal -> wait until whitespace -> start
|
|
//
|
|
// We also have two modifiers:
|
|
//
|
|
// 6) start -> + -> note that next filter is a MustMatch -> start
|
|
// 7) start -> - -> note that next filter is a CantMatch -> start
|
|
//
|
|
// any non-existant edge is a parse error
|
|
|
|
ParseTrace trace;
|
|
|
|
enum
|
|
{
|
|
Start,
|
|
QuotedExpr,
|
|
FunctionExprName,
|
|
FunctionExprParams,
|
|
Literal,
|
|
Nested,
|
|
} state = Start;
|
|
|
|
expr = expr.trimmed();
|
|
|
|
MatchType matchType = MatchType::Normal;
|
|
|
|
// temporary string we're building up somewhere
|
|
QString s;
|
|
|
|
// parenthesis depth
|
|
int parenDepth = 0;
|
|
// start of the current set of parameters
|
|
int paramStartPos = 0;
|
|
// parameters (since s holds the function name)
|
|
QString params;
|
|
// if we're parsing a nested expression and we go inside a quoted string (so parentheses should be
|
|
// ignored)
|
|
bool nestQuote = false;
|
|
|
|
int pos = 0;
|
|
while(pos < expr.length())
|
|
{
|
|
trace.length = qMax(0, pos - trace.position + 1);
|
|
|
|
if(state == Start)
|
|
{
|
|
// 1) skip whitespace while in start
|
|
if(expr[pos].isSpace())
|
|
{
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
// 6) and 7)
|
|
if(expr[pos] == QLatin1Char('-'))
|
|
{
|
|
trace.position = pos;
|
|
|
|
// stay in the Start state, but store match type
|
|
matchType = MatchType::CantMatch;
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
if(expr[pos] == QLatin1Char('+'))
|
|
{
|
|
trace.position = pos;
|
|
|
|
matchType = MatchType::MustMatch;
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
// 3.1) move to function expression if we see a $
|
|
if(expr[pos] == QLatin1Char('$'))
|
|
{
|
|
// only update the position if the match type is normal. If it's mustmatch/cantmatch
|
|
// include everything from the preceeding - or +
|
|
if(matchType == MatchType::Normal)
|
|
trace.position = pos;
|
|
|
|
// we need at minimum 3 more characters for $x()
|
|
if(pos + 3 >= expr.length())
|
|
{
|
|
trace.length = expr.length() - trace.position;
|
|
return trace.setError(
|
|
tr("Invalid function expression\n"
|
|
"If this is not a filter function, surround with quotes.",
|
|
"EventFilterModel"));
|
|
}
|
|
|
|
// consume the $
|
|
state = FunctionExprName;
|
|
pos++;
|
|
|
|
continue;
|
|
}
|
|
|
|
// 2.1) move to quoted expression if we see a "
|
|
if(expr[pos] == QLatin1Char('"'))
|
|
{
|
|
// only update the position if the match type is normal. If it's mustmatch/cantmatch
|
|
// include everything from the preceeding - or +
|
|
if(matchType == MatchType::Normal)
|
|
trace.position = pos;
|
|
|
|
state = QuotedExpr;
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
// 4.1) move to nested expression if we see a (
|
|
if(expr[pos] == QLatin1Char('('))
|
|
{
|
|
// only update the position if the match type is normal. If it's mustmatch/cantmatch
|
|
// include everything from the preceeding - or +
|
|
if(matchType == MatchType::Normal)
|
|
trace.position = pos;
|
|
|
|
state = Nested;
|
|
parenDepth = 1;
|
|
nestQuote = false;
|
|
pos++;
|
|
paramStartPos = pos;
|
|
continue;
|
|
}
|
|
|
|
// don't allow a literal to start with )
|
|
if(expr[pos] == QLatin1Char(')'))
|
|
{
|
|
trace.length = expr.length() - trace.position;
|
|
return trace.setError(
|
|
tr("Invalid function expression\n"
|
|
"Unexpected close parenthesis, if this should be a literal match surround it in "
|
|
"quotes.",
|
|
"EventFilterModel"));
|
|
}
|
|
|
|
// only update the position if the match type is normal. If it's mustmatch/cantmatch
|
|
// include everything from the preceeding - or +
|
|
if(matchType == MatchType::Normal)
|
|
trace.position = pos;
|
|
|
|
// 5.1) for anything else begin parsing a literal expression
|
|
state = Literal;
|
|
// don't continue here, we need to parse the first character of the literal
|
|
}
|
|
|
|
if(state == Nested)
|
|
{
|
|
// 4.2) handle quoted strings and escaping
|
|
if(nestQuote && expr[pos] == QLatin1Char('\\'))
|
|
{
|
|
if(pos == expr.length() - 1)
|
|
{
|
|
trace.length = expr.length() - trace.position;
|
|
return trace.setError(tr("Invalid escape sequence in quoted string", "EventFilterModel"));
|
|
}
|
|
|
|
// append the escape character. We won't process the " below and nestQuote will be
|
|
// true still, so we'll then also append whatever character is quoted
|
|
params.append(expr[pos]);
|
|
pos++;
|
|
}
|
|
else if(expr[pos] == QLatin1Char('"'))
|
|
{
|
|
// start or stop quoting
|
|
nestQuote = !nestQuote;
|
|
}
|
|
|
|
if(!nestQuote)
|
|
{
|
|
if(expr[pos] == QLatin1Char('('))
|
|
{
|
|
parenDepth++;
|
|
}
|
|
else if(expr[pos] == QLatin1Char(')'))
|
|
{
|
|
parenDepth--;
|
|
}
|
|
|
|
if(parenDepth == 0)
|
|
{
|
|
// we've finished the nested expression
|
|
|
|
// however we expect the end of the expression or whitespace next
|
|
if(pos + 1 < expr.length() && !expr[pos + 1].isSpace())
|
|
{
|
|
trace.length = (pos + 1) - trace.position + 1;
|
|
return trace.setError(
|
|
tr("Unexpected character after nested expression", "EventFilterModel"));
|
|
}
|
|
|
|
// reset errors, we'll fix up the location afterwards depending on what's returned
|
|
rdcarray<EventFilter> subFilters;
|
|
ParseTrace subTrace = ParseExpressionToFilters(params, subFilters);
|
|
|
|
if(subTrace.hasErrors())
|
|
{
|
|
// if the errors returned some sub-range for the errors, the position will be
|
|
// relative to the params string so rebase it.
|
|
if(subTrace.position > 0)
|
|
{
|
|
subTrace.position += paramStartPos;
|
|
}
|
|
else
|
|
{
|
|
// otherwise use the whole parent range which includes the function name
|
|
subTrace.position = trace.position;
|
|
subTrace.length = trace.length;
|
|
}
|
|
|
|
return subTrace;
|
|
}
|
|
else if(subFilters.empty())
|
|
{
|
|
trace.length = pos - trace.position + 1;
|
|
return trace.setError(tr("Unexpected empty nested expression", "EventFilterModel"));
|
|
}
|
|
|
|
FilterExpression subexpr;
|
|
|
|
subexpr.matchType = matchType;
|
|
subexpr.function = true;
|
|
subexpr.name = lit("$any$");
|
|
subexpr.params = params;
|
|
|
|
subexpr.position = trace.position;
|
|
subexpr.length = trace.length;
|
|
subexpr.exprs = subTrace.exprs;
|
|
|
|
for(FilterExpression &f : subexpr.exprs)
|
|
{
|
|
f.position += paramStartPos;
|
|
}
|
|
|
|
trace.exprs.push_back(subexpr);
|
|
|
|
auto filter = [subFilters](ICaptureContext *ctx, const rdcstr &, const rdcstr &,
|
|
uint32_t eid, const SDChunk *chunk,
|
|
const DrawcallDescription *draw, const rdcstr &name) {
|
|
return EvaluateFilterSet(*ctx, subFilters, false, eid, chunk, draw, name);
|
|
};
|
|
|
|
filters.push_back(EventFilter(filter, matchType));
|
|
|
|
s.clear();
|
|
params.clear();
|
|
nestQuote = false;
|
|
|
|
// move back to the start state
|
|
state = Start;
|
|
matchType = MatchType::Normal;
|
|
|
|
// skip the )
|
|
pos++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
params.append(expr[pos]);
|
|
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
if(state == QuotedExpr)
|
|
{
|
|
// 2.2) handle escaping
|
|
if(expr[pos] == QLatin1Char('\\'))
|
|
{
|
|
if(pos == expr.length() - 1)
|
|
{
|
|
trace.length = expr.length() - trace.position;
|
|
return trace.setError(tr("Invalid escape sequence in quoted string", "EventFilterModel"));
|
|
}
|
|
|
|
// append the next character, whatever it is, and skip the escape character
|
|
s.append(expr[pos + 1]);
|
|
pos += 2;
|
|
continue;
|
|
}
|
|
else if(expr[pos] == QLatin1Char('"'))
|
|
{
|
|
// if we encounter an unescaped quote we're done
|
|
|
|
// however we expect the end of the expression or whitespace next
|
|
if(pos + 1 < expr.length() && !expr[pos + 1].isSpace())
|
|
{
|
|
trace.length = (pos + 1) - trace.position + 1;
|
|
return trace.setError(tr("Unexpected character after quoted string", "EventFilterModel"));
|
|
}
|
|
|
|
filters.push_back(EventFilter(MakeLiteralMatcher(s), matchType));
|
|
|
|
FilterExpression subexpr;
|
|
|
|
subexpr.matchType = matchType;
|
|
subexpr.function = false;
|
|
subexpr.name = s;
|
|
subexpr.position = trace.position;
|
|
subexpr.length = trace.length;
|
|
|
|
trace.exprs.push_back(subexpr);
|
|
|
|
s.clear();
|
|
pos++;
|
|
state = Start;
|
|
matchType = MatchType::Normal;
|
|
|
|
trace.position = pos;
|
|
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// just another character, append and continue
|
|
s.append(expr[pos]);
|
|
pos++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if(state == Literal)
|
|
{
|
|
// illegal character in unquoted literal
|
|
if(expr[pos] == QLatin1Char('(') || expr[pos] == QLatin1Char(')') ||
|
|
expr[pos] == QLatin1Char('$'))
|
|
{
|
|
trace.length = (pos + 1) - trace.position + 1;
|
|
return trace.setError(
|
|
tr("Unexpected character '%1' in unquoted literal, surround in \"quotes\" to allow "
|
|
"special characters",
|
|
"EventFilterModel")
|
|
.arg(expr[pos]));
|
|
}
|
|
|
|
// if we encounter whitespace or the end of the expression, we're done
|
|
if(expr[pos].isSpace() || pos == expr.length() - 1)
|
|
{
|
|
if(expr[pos].isSpace())
|
|
trace.length--;
|
|
else
|
|
s.append(expr[pos]);
|
|
|
|
filters.push_back(EventFilter(MakeLiteralMatcher(s), matchType));
|
|
|
|
FilterExpression subexpr;
|
|
|
|
subexpr.matchType = matchType;
|
|
subexpr.function = false;
|
|
subexpr.name = s;
|
|
subexpr.position = trace.position;
|
|
subexpr.length = trace.length;
|
|
|
|
trace.exprs.push_back(subexpr);
|
|
|
|
s.clear();
|
|
pos++;
|
|
state = Start;
|
|
matchType = MatchType::Normal;
|
|
|
|
trace.position = pos;
|
|
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// just another character, append and continue
|
|
s.append(expr[pos]);
|
|
pos++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if(state == FunctionExprName)
|
|
{
|
|
// if we encounter a parenthesis check we have a valid filter function name and move to
|
|
// parsing parameters
|
|
if(expr[pos] == QLatin1Char('('))
|
|
{
|
|
state = FunctionExprParams;
|
|
parenDepth = 1;
|
|
pos++;
|
|
paramStartPos = pos;
|
|
|
|
trace.length = pos - trace.position + 1;
|
|
|
|
if(s.isEmpty())
|
|
return trace.setError(
|
|
tr("Filter function with no name before arguments.\n"
|
|
"If this is not a filter function, surround with quotes.",
|
|
"EventFilterModel"));
|
|
|
|
continue;
|
|
}
|
|
|
|
// otherwise we're still parsing the name.
|
|
|
|
// name must begin with a letter
|
|
if(s.isEmpty() && !expr[pos].isLetter())
|
|
{
|
|
// scan to the end of what looks like a function name for the error message
|
|
int end = pos;
|
|
while(end < expr.length() && (expr[end].isLetterOrNumber() || expr[end] == QLatin1Char('.')))
|
|
end++;
|
|
|
|
trace.length = end - trace.position + 1;
|
|
|
|
return trace.setError(
|
|
tr("Invalid filter function name, must begin with a letter.\n"
|
|
"If this is not a filter function, surround with quotes.",
|
|
"EventFilterModel"));
|
|
}
|
|
|
|
// add this character to the name we're building
|
|
s.append(expr[pos]);
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
if(state == FunctionExprParams)
|
|
{
|
|
if(expr[pos] == QLatin1Char('('))
|
|
{
|
|
parenDepth++;
|
|
}
|
|
else if(expr[pos] == QLatin1Char(')'))
|
|
{
|
|
parenDepth--;
|
|
}
|
|
|
|
if(parenDepth == 0)
|
|
{
|
|
// we've finished the filter function
|
|
|
|
// however we expect the end of the expression or whitespace next
|
|
if(pos + 1 < expr.length() && !expr[pos + 1].isSpace())
|
|
{
|
|
trace.length = (pos + 1) - trace.position + 1;
|
|
return trace.setError(
|
|
tr("Unexpected character after filter function", "EventFilterModel"));
|
|
}
|
|
|
|
// reset errors, we'll fix up the location afterwards depending on what's returned
|
|
ParseTrace subTrace;
|
|
IEventBrowser::EventFilterCallback filter = MakeFunctionMatcher(s, params, subTrace);
|
|
|
|
if(!filter)
|
|
{
|
|
// if the errors returned some sub-range for the errors, the position will be
|
|
// relative to the params string so rebase it.
|
|
if(subTrace.position > 0)
|
|
{
|
|
subTrace.position += paramStartPos;
|
|
}
|
|
else
|
|
{
|
|
// otherwise use the whole parent range which includes the function name
|
|
subTrace.position = trace.position;
|
|
subTrace.length = trace.length;
|
|
}
|
|
|
|
return subTrace;
|
|
}
|
|
|
|
FilterExpression subexpr;
|
|
|
|
subexpr.matchType = matchType;
|
|
subexpr.function = true;
|
|
subexpr.name = s;
|
|
subexpr.params = params;
|
|
|
|
subexpr.position = trace.position;
|
|
subexpr.length = trace.length;
|
|
subexpr.exprs = subTrace.exprs;
|
|
|
|
for(FilterExpression &f : subexpr.exprs)
|
|
{
|
|
f.position += paramStartPos;
|
|
}
|
|
|
|
trace.exprs.push_back(subexpr);
|
|
|
|
filters.push_back(EventFilter(filter, matchType));
|
|
|
|
s.clear();
|
|
params.clear();
|
|
|
|
// move back to the start state
|
|
state = Start;
|
|
matchType = MatchType::Normal;
|
|
}
|
|
else
|
|
{
|
|
params.append(expr[pos]);
|
|
}
|
|
|
|
pos++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
trace.length = expr.length() - trace.position;
|
|
|
|
// we shouldn't be in any parentheses
|
|
if(parenDepth > 0)
|
|
{
|
|
return trace.setError(tr("Encountered unterminated parenthesis", "EventFilterModel"));
|
|
}
|
|
|
|
// we should be back in the normal state, because all the other states have termination states
|
|
if(state == Literal)
|
|
{
|
|
// shouldn't be possible as the Literal state terminates itself when it sees the end of the
|
|
// string
|
|
return trace.setError(tr("Encountered unterminated literal", "EventFilterModel"));
|
|
}
|
|
else if(state == QuotedExpr)
|
|
{
|
|
return trace.setError(tr("Unterminated quoted expression", "EventFilterModel"));
|
|
}
|
|
else if(state == FunctionExprName)
|
|
{
|
|
return trace.setError(tr("Filter function has no parameters", "EventFilterModel"));
|
|
}
|
|
else if(state == FunctionExprParams)
|
|
{
|
|
return trace.setError(tr("Filter function parameters incomplete", "EventFilterModel"));
|
|
}
|
|
|
|
// any - or + should have been consumed by an expression, if we still have it then it was
|
|
// dangling
|
|
if(matchType == MatchType::CantMatch)
|
|
return trace.setError(tr("- expects an expression to exclude", "EventFilterModel"));
|
|
if(matchType == MatchType::MustMatch)
|
|
return trace.setError(tr("+ expects an expression to require", "EventFilterModel"));
|
|
|
|
// if we got here, we succeeded. Stop tracking errors
|
|
trace.clearErrors();
|
|
|
|
return trace;
|
|
}
|
|
|
|
QMap<QString, CustomFilterCallbacks> EventFilterModel::m_CustomFilters;
|
|
QMap<QString, BuiltinFilterCallbacks> EventFilterModel::m_BuiltinFilters;
|
|
|
|
static bool textEditControl(QWidget *sender)
|
|
{
|
|
if(qobject_cast<QLineEdit *>(sender) || qobject_cast<QTextEdit *>(sender) ||
|
|
qobject_cast<QAbstractSpinBox *>(sender) || qobject_cast<ScintillaEditBase *>(sender))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
QComboBox *combo = qobject_cast<QComboBox *>(sender);
|
|
if(combo && combo->isEditable())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void AddFilterSelections(QTextCursor cursor, int &idx, QColor backCol,
|
|
QVector<FilterExpression> &exprs, QList<QTextEdit::ExtraSelection> &sels)
|
|
{
|
|
QTextEdit::ExtraSelection sel;
|
|
|
|
sel.cursor = cursor;
|
|
|
|
for(FilterExpression &f : exprs)
|
|
{
|
|
QColor col = QColor::fromHslF(float(idx++ % 6) / 6.0f, 1.0f,
|
|
qBound(0.05, 0.5 + 0.5 * backCol.lightnessF(), 0.95));
|
|
|
|
f.col = col;
|
|
|
|
sel.cursor.setPosition(f.position, QTextCursor::MoveAnchor);
|
|
sel.cursor.setPosition(f.position + f.length, QTextCursor::KeepAnchor);
|
|
sel.format.setBackground(QBrush(col));
|
|
sels.push_back(sel);
|
|
|
|
AddFilterSelections(cursor, idx, backCol, f.exprs, sels);
|
|
}
|
|
}
|
|
|
|
ParseErrorTipLabel::ParseErrorTipLabel(QWidget *widget) : QLabel(NULL), m_Widget(widget)
|
|
{
|
|
int margin = style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, NULL, this);
|
|
int opacity = style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, NULL, this);
|
|
|
|
setWindowFlags(Qt::ToolTip);
|
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
setForegroundRole(QPalette::ToolTipText);
|
|
setBackgroundRole(QPalette::ToolTipBase);
|
|
setMargin(margin + 1);
|
|
setFrameStyle(QFrame::NoFrame);
|
|
setAlignment(Qt::AlignLeft);
|
|
setIndent(1);
|
|
setWindowOpacity(opacity / 255.0);
|
|
}
|
|
|
|
void ParseErrorTipLabel::paintEvent(QPaintEvent *ev)
|
|
{
|
|
QStylePainter p(this);
|
|
QStyleOptionFrame opt;
|
|
opt.init(this);
|
|
p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
|
|
p.end();
|
|
|
|
if(!m_Widget->hasFocus())
|
|
GUIInvoke::call(this, [this]() { hide(); });
|
|
|
|
QLabel::paintEvent(ev);
|
|
}
|
|
|
|
void ParseErrorTipLabel::resizeEvent(QResizeEvent *e)
|
|
{
|
|
QStyleHintReturnMask frameMask;
|
|
QStyleOption option;
|
|
option.init(this);
|
|
if(style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask))
|
|
setMask(frameMask.region);
|
|
|
|
QLabel::resizeEvent(e);
|
|
}
|
|
|
|
EventBrowser::EventBrowser(ICaptureContext &ctx, QWidget *parent)
|
|
: QFrame(parent), ui(new Ui::EventBrowser), m_Ctx(ctx)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
m_ParseError = new ParseErrorTipLabel(ui->filterExpression);
|
|
m_ParseTrace = new ParseTrace;
|
|
|
|
clearBookmarks();
|
|
|
|
m_Model = new EventItemModel(ui->events, m_Ctx);
|
|
m_FilterModel = new EventFilterModel(m_Model, m_Ctx);
|
|
|
|
ui->events->setModel(m_FilterModel);
|
|
|
|
m_delegate = new RichTextViewDelegate(ui->events);
|
|
ui->events->setItemDelegate(m_delegate);
|
|
|
|
ui->find->setFont(Formatter::PreferredFont());
|
|
ui->events->setFont(Formatter::PreferredFont());
|
|
|
|
ui->events->setHeader(new RDHeaderView(Qt::Horizontal, this));
|
|
ui->events->header()->setStretchLastSection(true);
|
|
ui->events->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
// we set up the name column as column 0 so that it gets the tree controls.
|
|
ui->events->header()->setSectionResizeMode(COL_NAME, QHeaderView::Interactive);
|
|
ui->events->header()->setSectionResizeMode(COL_EID, QHeaderView::Interactive);
|
|
ui->events->header()->setSectionResizeMode(COL_DRAW, QHeaderView::Interactive);
|
|
ui->events->header()->setSectionResizeMode(COL_DURATION, QHeaderView::Interactive);
|
|
|
|
ui->events->header()->setMinimumSectionSize(40);
|
|
|
|
ui->events->header()->setSectionsMovable(true);
|
|
|
|
ui->events->header()->setCascadingSectionResizes(false);
|
|
|
|
ui->events->setItemVerticalMargin(0);
|
|
ui->events->setIgnoreIconSize(true);
|
|
|
|
ui->events->setColoredTreeLineWidth(3.0f);
|
|
|
|
// set up default section layout. This will be overridden in restoreState()
|
|
ui->events->header()->resizeSection(COL_EID, 80);
|
|
ui->events->header()->resizeSection(COL_DRAW, 60);
|
|
ui->events->header()->resizeSection(COL_NAME, 200);
|
|
ui->events->header()->resizeSection(COL_DURATION, 80);
|
|
|
|
ui->events->header()->hideSection(COL_DRAW);
|
|
ui->events->header()->hideSection(COL_DURATION);
|
|
|
|
ui->events->header()->moveSection(COL_NAME, 2);
|
|
|
|
UpdateDurationColumn();
|
|
|
|
m_FindHighlight = new QTimer(this);
|
|
m_FindHighlight->setInterval(400);
|
|
m_FindHighlight->setSingleShot(true);
|
|
connect(m_FindHighlight, &QTimer::timeout, this, &EventBrowser::findHighlight_timeout);
|
|
|
|
m_FilterTimeout = new QTimer(this);
|
|
m_FilterTimeout->setInterval(1200);
|
|
m_FilterTimeout->setSingleShot(true);
|
|
connect(m_FilterTimeout, &QTimer::timeout, this, &EventBrowser::filter_apply);
|
|
|
|
QObject::connect(ui->events, &RDTreeView::keyPress, this, &EventBrowser::events_keyPress);
|
|
QObject::connect(ui->events->selectionModel(), &QItemSelectionModel::currentChanged, this,
|
|
&EventBrowser::events_currentChanged);
|
|
on_find_toggled(false);
|
|
ui->bookmarkStrip->hide();
|
|
|
|
m_BookmarkStripLayout = new FlowLayout(ui->bookmarkStrip, 0, 3, 3);
|
|
m_BookmarkSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
|
|
ui->bookmarkStrip->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
|
m_BookmarkStripLayout->addWidget(ui->bookmarkStripHeader);
|
|
m_BookmarkStripLayout->addItem(m_BookmarkSpacer);
|
|
|
|
{
|
|
QHBoxLayout *box = new QHBoxLayout(ui->breadcrumbStrip);
|
|
box->setContentsMargins(QMargins(0, 0, 0, 0));
|
|
box->setMargin(0);
|
|
box->setSpacing(0);
|
|
m_Breadcrumbs = new MarkerBreadcrumbs(m_Ctx, this);
|
|
box->addWidget(m_Breadcrumbs);
|
|
ui->breadcrumbStrip->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
|
|
|
m_Breadcrumbs->hide();
|
|
}
|
|
|
|
Qt::Key keys[] = {
|
|
Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5,
|
|
Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9, Qt::Key_0,
|
|
};
|
|
for(int i = 0; i < 10; i++)
|
|
{
|
|
ctx.GetMainWindow()->RegisterShortcut(QKeySequence(keys[i] | Qt::ControlModifier).toString(),
|
|
NULL, [this, i](QWidget *) { jumpToBookmark(i); });
|
|
}
|
|
|
|
ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_Left | Qt::ControlModifier).toString(),
|
|
NULL, [this](QWidget *sender) {
|
|
// don't apply this shortcut if we're in a text edit type
|
|
// control
|
|
if(textEditControl(sender))
|
|
return;
|
|
|
|
on_stepPrev_clicked();
|
|
});
|
|
|
|
ctx.GetMainWindow()->RegisterShortcut(
|
|
QKeySequence(Qt::Key_Right | Qt::ControlModifier).toString(), NULL, [this](QWidget *sender) {
|
|
// don't apply this shortcut if we're in a text edit type
|
|
// control
|
|
if(textEditControl(sender))
|
|
return;
|
|
|
|
on_stepNext_clicked();
|
|
});
|
|
|
|
ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_Escape).toString(), ui->findStrip,
|
|
[this](QWidget *) { on_HideFind(); });
|
|
|
|
ui->events->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(ui->events, &RDTreeView::customContextMenuRequested, this,
|
|
&EventBrowser::events_contextMenu);
|
|
|
|
ui->events->header()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(ui->events->header(), &QHeaderView::customContextMenuRequested, this,
|
|
&EventBrowser::events_contextMenu);
|
|
|
|
{
|
|
QMenu *extensionsMenu = new QMenu(this);
|
|
|
|
ui->extensions->setMenu(extensionsMenu);
|
|
ui->extensions->setPopupMode(QToolButton::InstantPopup);
|
|
|
|
QObject::connect(extensionsMenu, &QMenu::aboutToShow, [this, extensionsMenu]() {
|
|
extensionsMenu->clear();
|
|
m_Ctx.Extensions().MenuDisplaying(PanelMenu::EventBrowser, extensionsMenu, ui->extensions, {});
|
|
});
|
|
}
|
|
|
|
OnCaptureClosed();
|
|
|
|
ui->filterExpression->setSingleLine();
|
|
ui->filterExpression->setHoverTrack();
|
|
ui->filterExpression->enableCompletion();
|
|
ui->filterExpression->setAcceptRichText(false);
|
|
|
|
ui->filterExpression->setText(persistantStorage.CurrentFilter);
|
|
|
|
m_SavedCompleter = new QCompleter(this);
|
|
m_SavedCompleter->setWidget(ui->filterExpression);
|
|
m_SavedCompleter->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
|
|
m_SavedCompleter->setWrapAround(false);
|
|
m_SavedCompletionModel = new QStringListModel(this);
|
|
m_SavedCompleter->setModel(m_SavedCompletionModel);
|
|
m_SavedCompleter->setCompletionRole(Qt::DisplayRole);
|
|
|
|
QObject::connect(m_SavedCompleter, OverloadedSlot<const QModelIndex &>::of(&QCompleter::activated),
|
|
[this](const QModelIndex &idx) {
|
|
int i = idx.row();
|
|
if(i >= 0 && i < persistantStorage.SavedFilters.count())
|
|
{
|
|
m_CurrentFilterText->setPlainText(persistantStorage.SavedFilters[i].second);
|
|
|
|
QTextCursor c = m_CurrentFilterText->textCursor();
|
|
c.movePosition(QTextCursor::EndOfLine);
|
|
m_CurrentFilterText->setTextCursor(c);
|
|
}
|
|
});
|
|
|
|
QObject::connect(ui->filterExpression, &RDTextEdit::keyPress, this,
|
|
&EventBrowser::savedFilter_keyPress);
|
|
|
|
QObject::connect(ui->recentFilters, &QToolButton::clicked,
|
|
[this]() { ShowSavedFilterCompleter(ui->filterExpression); });
|
|
|
|
QObject::connect(ui->filterSettings, &QToolButton::clicked, this,
|
|
&EventBrowser::filterSettings_clicked);
|
|
|
|
QObject::connect(ui->filterExpression, &RDTextEdit::keyPress, this,
|
|
&EventBrowser::filter_forceCompletion_keyPress);
|
|
QObject::connect(ui->filterExpression, &RDTextEdit::completionBegin, this,
|
|
&EventBrowser::filter_CompletionBegin);
|
|
|
|
ui->filterExpression->setCompletionWordCharacters(lit("_$"));
|
|
|
|
QObject::connect(ui->filterExpression, &RDTextEdit::completionEnd, this,
|
|
&EventBrowser::on_filterExpression_textChanged);
|
|
|
|
if(m_FilterTimeout->isActive())
|
|
m_FilterTimeout->stop();
|
|
|
|
CreateFilterDialog();
|
|
|
|
m_redPalette = palette();
|
|
m_redPalette.setColor(QPalette::Base, Qt::red);
|
|
|
|
m_Ctx.AddCaptureViewer(this);
|
|
}
|
|
|
|
EventBrowser::~EventBrowser()
|
|
{
|
|
delete m_ParseError;
|
|
delete m_ParseTrace;
|
|
|
|
// unregister any shortcuts we registered
|
|
Qt::Key keys[] = {
|
|
Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5,
|
|
Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9, Qt::Key_0,
|
|
};
|
|
for(int i = 0; i < 10; i++)
|
|
{
|
|
m_Ctx.GetMainWindow()->UnregisterShortcut(
|
|
QKeySequence(keys[i] | Qt::ControlModifier).toString(), NULL);
|
|
}
|
|
|
|
m_Ctx.GetMainWindow()->UnregisterShortcut(
|
|
QKeySequence(Qt::Key_Left | Qt::ControlModifier).toString(), NULL);
|
|
|
|
m_Ctx.GetMainWindow()->UnregisterShortcut(
|
|
QKeySequence(Qt::Key_Right | Qt::ControlModifier).toString(), NULL);
|
|
|
|
m_Ctx.GetMainWindow()->UnregisterShortcut(QString(), ui->findStrip);
|
|
|
|
m_Ctx.BuiltinWindowClosed(this);
|
|
m_Ctx.RemoveCaptureViewer(this);
|
|
delete ui;
|
|
}
|
|
|
|
void EventBrowser::OnCaptureLoaded()
|
|
{
|
|
ui->events->setIgnoreBackgroundColors(!m_Ctx.Config().EventBrowser_ColorEventRow);
|
|
|
|
m_FilterModel->ResetCache();
|
|
// older Qt versions lose all the sections when a model resets even if the sections don't change.
|
|
// Manually save/restore them
|
|
QVariant p = persistData();
|
|
m_Model->ResetModel();
|
|
setPersistData(p);
|
|
|
|
// expand the root frame node
|
|
ui->events->expand(ui->events->model()->index(0, 0));
|
|
|
|
filter_apply();
|
|
|
|
ui->events->scrollTo(ui->events->model()->index(0, 0));
|
|
|
|
clearBookmarks();
|
|
repopulateBookmarks();
|
|
|
|
m_Breadcrumbs->show();
|
|
m_Breadcrumbs->ForceRefresh();
|
|
|
|
ui->find->setEnabled(true);
|
|
ui->timeDraws->setEnabled(true);
|
|
ui->bookmark->setEnabled(true);
|
|
ui->exportDraws->setEnabled(true);
|
|
ui->stepPrev->setEnabled(true);
|
|
ui->stepNext->setEnabled(true);
|
|
}
|
|
|
|
void EventBrowser::OnCaptureClosed()
|
|
{
|
|
clearBookmarks();
|
|
|
|
on_HideFind();
|
|
|
|
m_Breadcrumbs->hide();
|
|
|
|
m_FilterModel->ResetCache();
|
|
// older Qt versions lose all the sections when a model resets even if the sections don't change.
|
|
// Manually save/restore them
|
|
QVariant p = persistData();
|
|
m_Model->ResetModel();
|
|
setPersistData(p);
|
|
|
|
ui->find->setEnabled(false);
|
|
ui->timeDraws->setEnabled(false);
|
|
ui->bookmark->setEnabled(false);
|
|
ui->exportDraws->setEnabled(false);
|
|
ui->stepPrev->setEnabled(false);
|
|
ui->stepNext->setEnabled(false);
|
|
}
|
|
|
|
void EventBrowser::OnEventChanged(uint32_t eventId)
|
|
{
|
|
if(!SelectEvent(eventId))
|
|
ui->events->setCurrentIndex(QModelIndex());
|
|
repopulateBookmarks();
|
|
highlightBookmarks();
|
|
|
|
m_Model->RefreshCache();
|
|
m_Breadcrumbs->OnEventChanged(eventId);
|
|
}
|
|
|
|
void EventBrowser::on_find_toggled(bool checked)
|
|
{
|
|
ui->findStrip->setVisible(checked);
|
|
if(checked)
|
|
ui->findEvent->setFocus();
|
|
}
|
|
|
|
void EventBrowser::on_bookmark_clicked()
|
|
{
|
|
QModelIndex idx = ui->events->currentIndex();
|
|
|
|
if(idx.isValid())
|
|
{
|
|
EventBookmark mark(GetSelectedEID(idx));
|
|
|
|
if(m_Ctx.GetBookmarks().contains(mark))
|
|
m_Ctx.RemoveBookmark(mark.eventId);
|
|
else
|
|
m_Ctx.SetBookmark(mark);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_timeDraws_clicked()
|
|
{
|
|
ANALYTIC_SET(UIFeatures.DrawcallTimes, true);
|
|
|
|
ui->events->header()->showSection(COL_DURATION);
|
|
|
|
m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
|
|
m_Model->SetTimes(r->FetchCounters({GPUCounter::EventGPUDuration}));
|
|
|
|
GUIInvoke::call(this, [this]() { ui->events->update(); });
|
|
});
|
|
}
|
|
|
|
void EventBrowser::events_currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
|
|
{
|
|
if(!current.isValid())
|
|
return;
|
|
|
|
uint32_t selectedEID = GetSelectedEID(current);
|
|
uint32_t effectiveEID = GetEffectiveEID(current);
|
|
|
|
if(selectedEID == m_Ctx.CurSelectedEvent() && effectiveEID == m_Ctx.CurEvent())
|
|
return;
|
|
|
|
m_Ctx.SetEventID({this}, selectedEID, effectiveEID);
|
|
|
|
m_Model->RefreshCache();
|
|
m_Breadcrumbs->OnEventChanged(effectiveEID);
|
|
|
|
const DrawcallDescription *draw = m_Ctx.GetDrawcall(selectedEID);
|
|
|
|
if(draw && draw->IsFakeMarker())
|
|
draw = &draw->children.back();
|
|
|
|
ui->stepPrev->setEnabled(draw && draw->previous);
|
|
ui->stepNext->setEnabled(draw && draw->next);
|
|
|
|
// special case for the first draw in the frame
|
|
if(selectedEID == 0)
|
|
ui->stepNext->setEnabled(true);
|
|
|
|
// special case for the first 'virtual' draw at EID 0
|
|
if(m_Ctx.GetFirstDrawcall() && selectedEID == m_Ctx.GetFirstDrawcall()->eventId)
|
|
ui->stepPrev->setEnabled(true);
|
|
|
|
highlightBookmarks();
|
|
}
|
|
|
|
void EventBrowser::on_HideFind()
|
|
{
|
|
ui->findStrip->hide();
|
|
|
|
ui->find->setChecked(false);
|
|
|
|
ui->findEvent->setText(QString());
|
|
m_Model->SetFindText(QString());
|
|
updateFindResultsAvailable();
|
|
}
|
|
|
|
void EventBrowser::findHighlight_timeout()
|
|
{
|
|
m_Model->SetFindText(ui->findEvent->text());
|
|
SelectEvent(m_FilterModel->mapFromSource(m_Model->Find(true)));
|
|
updateFindResultsAvailable();
|
|
}
|
|
|
|
void EventBrowser::CreateFilterDialog()
|
|
{
|
|
// we create this dialog manually since it's relatively simple, and it needs fairly tight
|
|
// integration with the main window
|
|
m_FilterSettings.Dialog = new QDialog(this);
|
|
|
|
QDialogButtonBox *buttons = new QDialogButtonBox(this);
|
|
RDLabel *explainTitle = new RDLabel(this);
|
|
RDLabel *listLabel = new RDLabel(this);
|
|
RDLabel *filterLabel = new RDLabel(this);
|
|
CollapseGroupBox *settingsGroup = new CollapseGroupBox(this);
|
|
QToolButton *recentFilters = new QToolButton(this);
|
|
QToolButton *saveFilter = new QToolButton(this);
|
|
|
|
QVBoxLayout *settingsLayout = new QVBoxLayout();
|
|
m_FilterSettings.ShowParams = new QCheckBox(this);
|
|
m_FilterSettings.ShowAll = new QCheckBox(this);
|
|
m_FilterSettings.UseCustom = new QCheckBox(this);
|
|
|
|
m_FilterSettings.Notes = new RDLabel(this);
|
|
m_FilterSettings.FuncDocs = new RDTextEdit(this);
|
|
m_FilterSettings.Filter = new RDTextEdit(this);
|
|
m_FilterSettings.FuncList = new QListWidget(this);
|
|
m_FilterSettings.Explanation = new RDTreeWidget(this);
|
|
|
|
m_FilterSettings.Dialog->setWindowTitle(tr("Event Filter Configuration"));
|
|
m_FilterSettings.Dialog->setWindowFlags(m_FilterSettings.Dialog->windowFlags() &
|
|
~Qt::WindowContextHelpButtonHint);
|
|
m_FilterSettings.Dialog->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
m_FilterSettings.Dialog->setMinimumSize(QSize(600, 600));
|
|
|
|
settingsGroup->setTitle(tr("General settings"));
|
|
{
|
|
m_FilterSettings.ShowParams->setText(tr("Show parameter names and values"));
|
|
m_FilterSettings.ShowParams->setToolTip(
|
|
tr("Show parameter names in event names well as the values."));
|
|
m_FilterSettings.ShowParams->setCheckable(true);
|
|
|
|
QObject::connect(m_FilterSettings.ShowParams, &QCheckBox::toggled,
|
|
[this](bool on) { m_Model->SetShowParameterNames(on); });
|
|
|
|
m_FilterSettings.ShowAll->setText(tr("Show all parameters"));
|
|
m_FilterSettings.ShowAll->setToolTip(
|
|
tr("Show all parameters in each event, instead of only the most relevant."));
|
|
m_FilterSettings.ShowAll->setCheckable(true);
|
|
|
|
QObject::connect(m_FilterSettings.ShowAll, &QCheckBox::toggled,
|
|
[this](bool on) { m_Model->SetShowAllParameters(on); });
|
|
|
|
m_FilterSettings.UseCustom->setText(tr("Show custom draw names"));
|
|
m_FilterSettings.UseCustom->setToolTip(
|
|
tr("Show custom draw names for e.g. indirect draws where the values are not as directly "
|
|
"useful."));
|
|
m_FilterSettings.UseCustom->setCheckable(true);
|
|
|
|
QObject::connect(m_FilterSettings.UseCustom, &QCheckBox::toggled,
|
|
[this](bool on) { m_Model->SetUseCustomDrawNames(on); });
|
|
|
|
settingsLayout->addWidget(m_FilterSettings.ShowParams);
|
|
settingsLayout->addWidget(m_FilterSettings.ShowAll);
|
|
settingsLayout->addWidget(m_FilterSettings.UseCustom);
|
|
}
|
|
settingsGroup->setLayout(settingsLayout);
|
|
|
|
buttons->addButton(QDialogButtonBox::Ok);
|
|
QObject::connect(buttons, &QDialogButtonBox::accepted, m_FilterSettings.Dialog, &QDialog::accept);
|
|
|
|
filterLabel->setText(tr("Current filter:"));
|
|
|
|
m_FilterSettings.Filter->setReadOnly(false);
|
|
m_FilterSettings.Filter->enableCompletion();
|
|
m_FilterSettings.Filter->setAcceptRichText(false);
|
|
m_FilterSettings.Filter->setSingleLine();
|
|
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::keyPress, this,
|
|
&EventBrowser::filter_forceCompletion_keyPress);
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::completionBegin, this,
|
|
&EventBrowser::filter_CompletionBegin);
|
|
|
|
m_FilterSettings.Filter->setCompletionWordCharacters(lit("_$"));
|
|
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::completionEnd, m_FilterSettings.Filter,
|
|
&RDTextEdit::textChanged);
|
|
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::keyPress, this,
|
|
&EventBrowser::savedFilter_keyPress);
|
|
|
|
QObject::connect(recentFilters, &QToolButton::clicked,
|
|
[this]() { ShowSavedFilterCompleter(m_FilterSettings.Filter); });
|
|
|
|
QObject::connect(saveFilter, &QToolButton::clicked, [this]() {
|
|
QDialog *dialog = new QDialog(this);
|
|
|
|
dialog->setWindowTitle(tr("Save filters"));
|
|
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
dialog->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
RDLineEdit saveName;
|
|
saveName.setPlaceholderText(tr("Name of filter"));
|
|
|
|
RDListWidget filters;
|
|
for(const QPair<QString, QString> &f : persistantStorage.SavedFilters)
|
|
filters.addItem(f.first);
|
|
|
|
QPushButton saveButton;
|
|
saveButton.setText(tr("Save"));
|
|
saveButton.setIcon(Icons::save());
|
|
QPushButton deleteButton;
|
|
deleteButton.setText(tr("Delete"));
|
|
|
|
QDialogButtonBox saveDialogButtons;
|
|
saveDialogButtons.addButton(QDialogButtonBox::Ok);
|
|
QObject::connect(&saveDialogButtons, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
|
|
|
|
QGridLayout grid;
|
|
grid.addWidget(&saveName, 0, 0, 1, 1);
|
|
grid.addWidget(&saveButton, 0, 1, 1, 1);
|
|
grid.addWidget(&filters, 1, 0, 1, 1);
|
|
grid.addWidget(&deleteButton, 1, 1, 1, 1, Qt::AlignHCenter | Qt::AlignTop);
|
|
grid.addWidget(&saveDialogButtons, 2, 0, 1, 2);
|
|
|
|
dialog->setLayout(&grid);
|
|
|
|
auto enterCallback = [this, &saveButton, &deleteButton](QKeyEvent *e) {
|
|
if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
|
|
{
|
|
saveButton.click();
|
|
e->accept();
|
|
}
|
|
|
|
if(e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete)
|
|
{
|
|
deleteButton.click();
|
|
e->accept();
|
|
}
|
|
};
|
|
|
|
QObject::connect(&saveName, &RDLineEdit::keyPress, enterCallback);
|
|
QObject::connect(&filters, &RDListWidget::keyPress, enterCallback);
|
|
QObject::connect(&filters, &RDListWidget::itemActivated,
|
|
[&saveButton](QListWidgetItem *) { saveButton.click(); });
|
|
|
|
QObject::connect(&saveName, &RDLineEdit::textChanged, [this, &saveName, &filters]() {
|
|
for(int i = 0; i < persistantStorage.SavedFilters.count(); i++)
|
|
{
|
|
if(saveName.text().trimmed().toLower() == persistantStorage.SavedFilters[i].first.toLower())
|
|
{
|
|
if(filters.currentRow() != i)
|
|
filters.setCurrentRow(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
filters.setCurrentItem(NULL);
|
|
});
|
|
|
|
QObject::connect(&filters, &QListWidget::currentRowChanged, [this, &saveName](int row) {
|
|
if(row >= 0 && row < persistantStorage.SavedFilters.count())
|
|
saveName.setText(persistantStorage.SavedFilters[row].first);
|
|
});
|
|
|
|
QObject::connect(&saveButton, &QPushButton::clicked, [this, dialog, &saveName, &filters]() {
|
|
QString n = saveName.text().trimmed();
|
|
QString f = m_FilterSettings.Filter->toPlainText();
|
|
|
|
for(int i = 0; i < persistantStorage.SavedFilters.count(); i++)
|
|
{
|
|
if(n.toLower() == persistantStorage.SavedFilters[i].first.toLower())
|
|
{
|
|
if(persistantStorage.SavedFilters[i].second.trimmed() == f.trimmed())
|
|
{
|
|
dialog->accept();
|
|
return;
|
|
}
|
|
|
|
QMessageBox::StandardButton res = RDDialog::question(
|
|
dialog, tr("Delete filter?"),
|
|
tr("Are you sure you want to overwrite the %1 filter? From:\n\n%2\n\nTo:\n\n%3")
|
|
.arg(persistantStorage.SavedFilters[i].first)
|
|
.arg(persistantStorage.SavedFilters[i].second)
|
|
.arg(f),
|
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
|
|
|
if(res != QMessageBox::Yes)
|
|
return;
|
|
|
|
delete filters.takeItem(i);
|
|
persistantStorage.SavedFilters.erase(persistantStorage.SavedFilters.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
persistantStorage.SavedFilters.insert(0, {n, f});
|
|
|
|
dialog->accept();
|
|
});
|
|
|
|
QObject::connect(&deleteButton, &QPushButton::clicked, [this, dialog, &filters]() {
|
|
QListWidgetItem *item = filters.currentItem();
|
|
if(!item)
|
|
return;
|
|
|
|
for(int i = 0; i < persistantStorage.SavedFilters.count(); i++)
|
|
{
|
|
if(item->text().trimmed().toLower() == persistantStorage.SavedFilters[i].first.toLower())
|
|
{
|
|
QMessageBox::StandardButton res =
|
|
RDDialog::question(dialog, tr("Delete filter?"),
|
|
tr("Are you sure you want to delete the %1 filter?\n\n%2")
|
|
.arg(persistantStorage.SavedFilters[i].first)
|
|
.arg(persistantStorage.SavedFilters[i].second),
|
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
|
|
|
if(res == QMessageBox::Yes)
|
|
{
|
|
delete filters.takeItem(i);
|
|
persistantStorage.SavedFilters.erase(persistantStorage.SavedFilters.begin() + i);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
RDDialog::show(dialog);
|
|
|
|
dialog->deleteLater();
|
|
});
|
|
|
|
recentFilters->setAutoRaise(true);
|
|
recentFilters->setIcon(Icons::filter_reapply());
|
|
recentFilters->setToolTip(tr("Load saved filters"));
|
|
saveFilter->setAutoRaise(true);
|
|
saveFilter->setIcon(Icons::save());
|
|
saveFilter->setText(tr("Save"));
|
|
saveFilter->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
|
saveFilter->setToolTip(tr("Save current filter"));
|
|
|
|
explainTitle->setText(lit("Show an event if:"));
|
|
|
|
m_FilterSettings.Notes->setWordWrap(true);
|
|
|
|
listLabel->setText(tr("Available functions"));
|
|
|
|
m_FilterSettings.FuncList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
m_FilterSettings.FuncList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
|
|
|
|
m_FilterSettings.Explanation->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
m_FilterSettings.Explanation->setColumns({lit("explanation")});
|
|
m_FilterSettings.Explanation->setHeaderHidden(true);
|
|
m_FilterSettings.Explanation->setClearSelectionOnFocusLoss(true);
|
|
|
|
// set up the same timeout system for filter changes
|
|
m_FilterSettings.Timeout = new QTimer();
|
|
m_FilterSettings.Timeout->setInterval(1200);
|
|
m_FilterSettings.Timeout->setSingleShot(true);
|
|
|
|
m_FilterSettings.FuncDocs->setReadOnly(true);
|
|
m_FilterSettings.FuncDocs->setAcceptRichText(false);
|
|
|
|
QObject::connect(m_FilterSettings.FuncList, &QListWidget::currentItemChanged,
|
|
[this](QListWidgetItem *current, QListWidgetItem *) {
|
|
if(current)
|
|
{
|
|
QString f = current->text();
|
|
if(m_FilterSettings.FuncList->row(current) == 0)
|
|
{
|
|
m_FilterSettings.FuncDocs->setText(tr(R"EOD(
|
|
General filter help
|
|
|
|
Filters loosely follow a general search-term syntax, by default matching an event
|
|
if any of the terms matches. E.g. a filter such as:
|
|
|
|
Draw Clear Copy
|
|
|
|
would match each string against event names, and include any event that matches
|
|
"Draw" OR "Clear" OR "Copy".
|
|
|
|
You can also exclude matches with - such as:
|
|
|
|
Draw Clear Copy -Depth
|
|
|
|
which will match as in the first example, except exclude any events that match
|
|
'Depth'.
|
|
|
|
You can also require matches, which overrides any optional matches:
|
|
|
|
+Draw +Indexed -Instanced
|
|
|
|
which will match only events which match "Draw" and match "Indexed" but don't match
|
|
"Instanced". In this case adding a term with no + or - prefix will be ignored,
|
|
since an event will be excluded if it doesn't match all the +required terms
|
|
anyway.
|
|
|
|
More complex expressions can be built with nesting:
|
|
|
|
+Draw +(Indexed Instanced) -Indirect
|
|
|
|
Would match only events matching "Draw" which also match at least one of "Indexed"
|
|
or "Instanced" but not "Indirect".
|
|
|
|
Finally you can use filter functions for more advanced matching than just
|
|
strings. These are documented on the left here, but for example
|
|
|
|
$draw(numIndices > 1000) Indexed
|
|
|
|
will include any drawcall that matches "Indexed" as a plain string match, OR
|
|
renders more than 1000 indices.
|
|
)EOD").trimmed());
|
|
}
|
|
else if(f == lit("Literal String"))
|
|
{
|
|
m_FilterSettings.FuncDocs->setText(tr(R"EOD(
|
|
"Literal string"
|
|
literal_string - passes if the event's name contains a literal string
|
|
|
|
Any literal string, optionally included in quotes to include special characters
|
|
or whitespace, will be case-insensitively matched against the event name. Note that
|
|
this doesn't include all parameters, only those that appear in the name summary.
|
|
For searching arbitrary parameters consider using the $param() function.
|
|
)EOD").trimmed());
|
|
}
|
|
else
|
|
{
|
|
f = f.mid(1, f.size() - 3);
|
|
m_FilterSettings.FuncDocs->setText(m_FilterModel->GetDescription(f));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_FilterSettings.FuncDocs->setText(QString());
|
|
}
|
|
});
|
|
|
|
// if the filter is changed, clear any current notes/explanation and start the usual timeout
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::textChanged, [this]() {
|
|
m_FilterSettings.Filter->setExtraSelections({});
|
|
m_FilterSettings.Explanation->clear();
|
|
m_FilterSettings.Notes->clear();
|
|
|
|
m_FilterSettings.Timeout->start();
|
|
});
|
|
|
|
// if return/enter is hit, immediately fire the timeout
|
|
QObject::connect(m_FilterSettings.Filter, &RDTextEdit::keyPress, [this](QKeyEvent *e) {
|
|
if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
|
|
{
|
|
// stop the timer, we'll manually fire it instantly
|
|
m_FilterSettings.Timeout->stop();
|
|
m_FilterSettings.Timeout->timeout({});
|
|
}
|
|
|
|
if(e->key() == Qt::Key_Down)
|
|
{
|
|
ShowSavedFilterCompleter(m_FilterSettings.Filter);
|
|
}
|
|
});
|
|
|
|
QObject::connect(m_FilterSettings.Explanation, &RDTreeWidget::currentItemChanged, this,
|
|
&EventBrowser::explanation_currentItemChanged);
|
|
|
|
QObject::connect(m_FilterSettings.Timeout, &QTimer::timeout, this,
|
|
&EventBrowser::settings_filterApply);
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout();
|
|
|
|
QHBoxLayout *filterLayout = new QHBoxLayout();
|
|
{
|
|
filterLayout->addWidget(filterLabel);
|
|
filterLayout->addWidget(m_FilterSettings.Filter);
|
|
filterLayout->addWidget(recentFilters);
|
|
filterLayout->addWidget(saveFilter);
|
|
|
|
layout->addLayout(filterLayout);
|
|
}
|
|
|
|
layout->addWidget(m_FilterSettings.Notes);
|
|
layout->addWidget(explainTitle);
|
|
layout->addWidget(m_FilterSettings.Explanation);
|
|
|
|
layout->addWidget(settingsGroup);
|
|
|
|
QHBoxLayout *funcsLayout = new QHBoxLayout();
|
|
|
|
{
|
|
layout->addWidget(listLabel);
|
|
funcsLayout->addWidget(m_FilterSettings.FuncList);
|
|
funcsLayout->addWidget(m_FilterSettings.FuncDocs);
|
|
layout->addLayout(funcsLayout);
|
|
}
|
|
|
|
layout->addWidget(buttons);
|
|
|
|
m_FilterSettings.Dialog->setLayout(layout);
|
|
}
|
|
|
|
void EventBrowser::explanation_currentItemChanged(RDTreeWidgetItem *current, RDTreeWidgetItem *prev)
|
|
{
|
|
// when an item is selected, its tag should have a QSize containing the expression range which
|
|
// we underline
|
|
|
|
QString funcName;
|
|
|
|
if(current->dataCount() >= 1)
|
|
funcName = current->text(1);
|
|
|
|
QStringList funcs = m_FilterModel->GetFunctions();
|
|
funcs.insert(0, tr("General Help"));
|
|
funcs.insert(1, tr("Literal String"));
|
|
|
|
int idx = funcs.indexOf(funcName);
|
|
if(idx >= 0)
|
|
m_FilterSettings.FuncList->setCurrentItem(m_FilterSettings.FuncList->item(idx));
|
|
|
|
QList<QTextEdit::ExtraSelection> sels = m_FilterSettings.Filter->extraSelections();
|
|
|
|
// remove any previously underlined phrases
|
|
for(auto it = sels.begin(); it != sels.end();)
|
|
{
|
|
if(it->format.underlineStyle() == QTextCharFormat::SingleUnderline)
|
|
{
|
|
it = sels.erase(it);
|
|
continue;
|
|
}
|
|
|
|
it++;
|
|
}
|
|
|
|
QSize range = current->tag().toSize();
|
|
|
|
int pos = range.width();
|
|
int len = range.height();
|
|
|
|
if(pos >= 0 && len > 0)
|
|
{
|
|
QTextEdit::ExtraSelection sel;
|
|
|
|
sel.cursor = m_FilterSettings.Filter->textCursor();
|
|
sel.cursor.setPosition(pos, QTextCursor::MoveAnchor);
|
|
|
|
sel.cursor.setPosition(pos + len, QTextCursor::KeepAnchor);
|
|
sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
|
|
|
sels.push_back(sel);
|
|
}
|
|
|
|
m_FilterSettings.Filter->setExtraSelections(sels);
|
|
}
|
|
|
|
void EventBrowser::settings_filterApply()
|
|
{
|
|
if(m_FilterSettings.Timeout->isActive())
|
|
m_FilterSettings.Timeout->stop();
|
|
|
|
ParseTrace trace;
|
|
rdcarray<EventFilter> filters;
|
|
trace = m_FilterModel->ParseExpressionToFilters(m_FilterSettings.Filter->toPlainText(), filters);
|
|
|
|
QList<QTextEdit::ExtraSelection> sels;
|
|
|
|
QPalette pal = m_FilterSettings.Filter->palette();
|
|
|
|
if(trace.hasErrors())
|
|
{
|
|
pal.setColor(QPalette::WindowText, QColor(170, 0, 0));
|
|
m_FilterSettings.Notes->setPalette(pal);
|
|
m_FilterSettings.Explanation->clear();
|
|
|
|
m_FilterSettings.Notes->setText(trace.errorText.trimmed());
|
|
|
|
QTextEdit::ExtraSelection sel;
|
|
|
|
sel.cursor = m_FilterSettings.Filter->textCursor();
|
|
sel.cursor.setPosition(trace.position, QTextCursor::MoveAnchor);
|
|
|
|
sel.cursor.setPosition(trace.position + trace.length, QTextCursor::KeepAnchor);
|
|
sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
|
sel.format.setUnderlineColor(QColor(Qt::red));
|
|
sels.push_back(sel);
|
|
}
|
|
else
|
|
{
|
|
m_FilterSettings.Notes->setPalette(pal);
|
|
|
|
int idx = 0;
|
|
QString notesText;
|
|
|
|
AddFilterSelections(m_FilterSettings.Filter->textCursor(), idx,
|
|
m_FilterSettings.Filter->palette().color(QPalette::Base), trace.exprs, sels);
|
|
m_FilterSettings.Explanation->clear();
|
|
AddFilterExplanations(m_FilterSettings.Explanation->invisibleRootItem(), trace.exprs, notesText);
|
|
|
|
if(trace.exprs.isEmpty())
|
|
{
|
|
m_FilterSettings.Explanation->addTopLevelItem(
|
|
new RDTreeWidgetItem({tr("No filters - all events shown"), QString()}));
|
|
}
|
|
|
|
m_FilterSettings.Notes->setText(QFormatStr("<html>%1</html>").arg(notesText.trimmed()));
|
|
|
|
ui->filterExpression->setPlainText(m_FilterSettings.Filter->toPlainText());
|
|
m_FilterTimeout->stop();
|
|
filter_apply();
|
|
}
|
|
|
|
m_FilterSettings.Filter->setExtraSelections(sels);
|
|
m_FilterSettings.Explanation->expandAllItems(m_FilterSettings.Explanation->invisibleRootItem());
|
|
}
|
|
|
|
void EventBrowser::filterSettings_clicked()
|
|
{
|
|
// resolve any current pending filter timeout first
|
|
filter_apply();
|
|
|
|
// update the global parameter checkboxes
|
|
m_FilterSettings.ShowParams->setChecked(m_Model->ShowParameterNames());
|
|
m_FilterSettings.ShowAll->setChecked(m_Model->ShowAllParameters());
|
|
m_FilterSettings.UseCustom->setChecked(m_Model->UseCustomDrawNames());
|
|
|
|
// fill out the list of filter functions with the current list
|
|
m_FilterSettings.FuncList->clear();
|
|
m_FilterSettings.FuncList->addItem(tr("General Help"));
|
|
m_FilterSettings.FuncList->addItem(tr("Literal String"));
|
|
for(QString f : m_FilterModel->GetFunctions())
|
|
m_FilterSettings.FuncList->addItem(QFormatStr("$%1()").arg(f));
|
|
|
|
m_FilterSettings.FuncList->setCurrentRow(0);
|
|
|
|
m_FilterSettings.Filter->setText(ui->filterExpression->toPlainText());
|
|
// immediately process and apply the filter
|
|
settings_filterApply();
|
|
|
|
// show the dialog now
|
|
RDDialog::show(m_FilterSettings.Dialog);
|
|
|
|
// if the filter changed and hasn't been applied, update it immediately
|
|
QString filterExpr = m_FilterSettings.Filter->toPlainText();
|
|
if(filterExpr != ui->filterExpression->toPlainText())
|
|
{
|
|
ui->filterExpression->setPlainText(filterExpr);
|
|
filter_apply();
|
|
}
|
|
}
|
|
|
|
void EventBrowser::filter_CompletionBegin(QString prefix)
|
|
{
|
|
if(m_FilterTimeout->isActive())
|
|
m_FilterTimeout->stop();
|
|
|
|
RDTextEdit *sender = qobject_cast<RDTextEdit *>(QObject::sender());
|
|
|
|
QString context = sender->toPlainText();
|
|
int pos = sender->textCursor().position();
|
|
context.remove(pos, context.length() - pos);
|
|
|
|
pos = context.lastIndexOf(QLatin1Char('$'));
|
|
if(pos > 0)
|
|
context.remove(0, pos);
|
|
|
|
// if the prefix starts with a $, set completion for all the
|
|
if(prefix.startsWith(QLatin1Char('$')))
|
|
{
|
|
QStringList completions;
|
|
|
|
for(const QString &s : m_FilterModel->GetFunctions())
|
|
completions.append(QLatin1Char('$') + s);
|
|
|
|
sender->setCompletionStrings(completions);
|
|
}
|
|
else if(context.startsWith(QLatin1Char('$')) && context.contains(QLatin1Char('(')) &&
|
|
!context.contains(QLatin1Char(')')))
|
|
{
|
|
pos = context.indexOf(QLatin1Char('('));
|
|
|
|
QString filter = context.mid(1, pos - 1);
|
|
context.remove(0, pos + 1);
|
|
|
|
sender->setCompletionStrings(m_FilterModel->GetCompletions(filter, context));
|
|
}
|
|
else
|
|
{
|
|
sender->setCompletionStrings({});
|
|
}
|
|
}
|
|
|
|
void EventBrowser::filter_apply()
|
|
{
|
|
if(ui->filterExpression->completionInProgress())
|
|
return;
|
|
|
|
if(m_FilterTimeout->isActive())
|
|
m_FilterTimeout->stop();
|
|
|
|
m_Breadcrumbs->ForceRefresh();
|
|
|
|
// unselect everything while applying the filter, to avoid updating the source model while the
|
|
// filter is processing if the current event is no longer selected
|
|
uint32_t curSelEvent = m_Ctx.CurSelectedEvent();
|
|
ui->events->clearSelection();
|
|
ui->events->setCurrentIndex(QModelIndex());
|
|
|
|
ExpansionKeyGen keygen = [](QModelIndex idx, uint) { return idx.data(ROLE_SELECTED_EID).toUInt(); };
|
|
|
|
// update the expansion with the current state. Any rows that were hidden won't have their
|
|
// collapsed/expanded state lost by this. We use the EID as a key because it will be the same even
|
|
// if the model index changes (e.g. if some events are shown or hidden)
|
|
ui->events->updateExpansion(m_EventsExpansion, keygen);
|
|
|
|
QString expression = ui->filterExpression->toPlainText();
|
|
persistantStorage.CurrentFilter = expression;
|
|
|
|
rdcarray<EventFilter> filters;
|
|
*m_ParseTrace = m_FilterModel->ParseExpressionToFilters(expression, filters);
|
|
|
|
if(m_ParseTrace->hasErrors())
|
|
filters.clear();
|
|
|
|
m_FilterModel->SetFilters(filters);
|
|
|
|
QList<QTextEdit::ExtraSelection> sels;
|
|
|
|
if(m_ParseTrace->errorText.isEmpty())
|
|
m_ParseError->setText(QString());
|
|
else
|
|
m_ParseError->setText(m_ParseTrace->errorText +
|
|
tr("\n\nOpen filter settings window for syntax help & explanation"));
|
|
m_ParseError->hide();
|
|
|
|
if(m_ParseTrace->hasErrors())
|
|
{
|
|
QTextEdit::ExtraSelection sel;
|
|
|
|
m_ParseTrace->exprs.clear();
|
|
|
|
sel.cursor = ui->filterExpression->textCursor();
|
|
sel.cursor.setPosition(m_ParseTrace->position, QTextCursor::MoveAnchor);
|
|
|
|
m_ParseErrorPos = ui->filterExpression->cursorRect(sel.cursor).bottomLeft();
|
|
|
|
sel.cursor.setPosition(m_ParseTrace->position + m_ParseTrace->length, QTextCursor::KeepAnchor);
|
|
sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
|
sel.format.setUnderlineColor(QColor(Qt::red));
|
|
sels.push_back(sel);
|
|
|
|
QPoint pos = ui->filterExpression->viewport()->mapToGlobal(m_ParseErrorPos);
|
|
pos.setY(pos.y() + 4);
|
|
|
|
if(ui->filterExpression->viewport()->rect().contains(
|
|
ui->filterExpression->viewport()->mapFromGlobal(QCursor::pos())))
|
|
{
|
|
m_ParseError->move(pos);
|
|
m_ParseError->show();
|
|
}
|
|
else
|
|
{
|
|
m_ParseError->hide();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_ParseError->hide();
|
|
}
|
|
|
|
ui->filterExpression->setExtraSelections(sels);
|
|
|
|
ui->events->applyExpansion(m_EventsExpansion, keygen);
|
|
|
|
ui->events->setCurrentIndex(m_FilterModel->mapFromSource(m_Model->GetIndexForEID(curSelEvent)));
|
|
}
|
|
|
|
void EventBrowser::AddFilterExplanations(RDTreeWidgetItem *root, QVector<FilterExpression> exprs,
|
|
QString ¬es)
|
|
{
|
|
// sort by match type
|
|
std::sort(exprs.begin(), exprs.end(), [](const FilterExpression &a, const FilterExpression &b) {
|
|
return a.matchType < b.matchType;
|
|
});
|
|
|
|
bool hasMust = false;
|
|
for(const FilterExpression &f : exprs)
|
|
hasMust |= (f.matchType == MatchType::MustMatch);
|
|
|
|
if(hasMust)
|
|
{
|
|
FilterExpression must;
|
|
|
|
QString ignored;
|
|
for(const FilterExpression &f : exprs)
|
|
{
|
|
if(f.matchType == MatchType::MustMatch)
|
|
{
|
|
must = f;
|
|
}
|
|
else if(f.matchType == MatchType::Normal)
|
|
{
|
|
if(!ignored.isEmpty())
|
|
ignored += lit(", ");
|
|
ignored += f.printName();
|
|
}
|
|
}
|
|
|
|
if(!ignored.isEmpty())
|
|
{
|
|
notes +=
|
|
tr("NOTE: The terms %1 are ignored because must-match terms like <span>+%2</span> take "
|
|
"precedence.\n"
|
|
"There is no sorting amongst matches based on optional keywords, so consider making "
|
|
"them <span>+required</span> to require all of them, or nesting in "
|
|
"<span>+( .. )</span> to require at least one of them.\n")
|
|
.arg(ignored)
|
|
.arg(must.printName());
|
|
}
|
|
}
|
|
|
|
bool first = true;
|
|
for(const FilterExpression &f : exprs)
|
|
{
|
|
QString explanation;
|
|
|
|
if(f.matchType == MatchType::MustMatch)
|
|
{
|
|
explanation = tr("Must be that: ");
|
|
}
|
|
else if(f.matchType == MatchType::CantMatch)
|
|
{
|
|
explanation = tr("Can't be that: ");
|
|
}
|
|
else
|
|
{
|
|
// omit any normal matches if we have a must, they are pointless and we warn the user in the
|
|
// notes
|
|
if(hasMust)
|
|
continue;
|
|
|
|
if(!first)
|
|
explanation = tr("Or: ");
|
|
|
|
first = false;
|
|
}
|
|
|
|
if(!f.function)
|
|
{
|
|
explanation += tr("Name matches '%1'").arg(f.name);
|
|
}
|
|
else if(f.name == lit("$any$"))
|
|
{
|
|
explanation += tr("Any of...");
|
|
}
|
|
else
|
|
{
|
|
explanation += tr("Function %1 passes").arg(f.printName());
|
|
}
|
|
|
|
RDTreeWidgetItem *item =
|
|
new RDTreeWidgetItem({explanation, f.function ? f.name : tr("Literal String")});
|
|
item->setBackgroundColor(f.col);
|
|
|
|
item->setTag(QSize(f.position, f.length));
|
|
|
|
root->addChild(item);
|
|
|
|
AddFilterExplanations(item, f.exprs, notes);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_findEvent_textEdited(const QString &arg1)
|
|
{
|
|
if(arg1.isEmpty())
|
|
{
|
|
m_FindHighlight->stop();
|
|
|
|
m_Model->SetFindText(QString());
|
|
updateFindResultsAvailable();
|
|
}
|
|
else
|
|
{
|
|
m_FindHighlight->start(); // restart
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_filterExpression_keyPress(QKeyEvent *e)
|
|
{
|
|
if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
|
|
{
|
|
// stop the timer, we'll manually fire it instantly
|
|
if(m_FilterTimeout->isActive())
|
|
m_FilterTimeout->stop();
|
|
|
|
filter_apply();
|
|
}
|
|
|
|
if(e->key() == Qt::Key_Down)
|
|
{
|
|
ShowSavedFilterCompleter(ui->filterExpression);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::filter_forceCompletion_keyPress(QKeyEvent *e)
|
|
{
|
|
if(e->key() == Qt::Key_Dollar || e->key() == Qt::Key_Ampersand || e->key() == Qt::Key_Equal ||
|
|
e->key() == Qt::Key_Bar || e->key() == Qt::Key_ParenLeft)
|
|
{
|
|
// force autocompletion for filter functions, as long as we're not inside a quoted string
|
|
|
|
RDTextEdit *sender = qobject_cast<RDTextEdit *>(QObject::sender());
|
|
|
|
QString str = sender->toPlainText();
|
|
|
|
bool inQuote = false;
|
|
for(int i = 0; i < str.length(); i++)
|
|
{
|
|
if(!inQuote && str[i] == QLatin1Char('"'))
|
|
{
|
|
inQuote = true;
|
|
}
|
|
else if(inQuote)
|
|
{
|
|
if(str[i] == QLatin1Char('\\'))
|
|
{
|
|
// skip over the next character
|
|
i++;
|
|
}
|
|
else if(str[i] == QLatin1Char('"'))
|
|
{
|
|
inQuote = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!inQuote)
|
|
sender->triggerCompletion();
|
|
}
|
|
}
|
|
|
|
void EventBrowser::ShowSavedFilterCompleter(RDTextEdit *filter)
|
|
{
|
|
QStringList strs;
|
|
|
|
for(QPair<QString, QString> &f : persistantStorage.SavedFilters)
|
|
strs << f.first + lit(": ") + f.second;
|
|
|
|
m_SavedCompletionModel->setStringList(strs);
|
|
|
|
m_SavedCompleter->setWidget(filter);
|
|
|
|
QRect r = filter->rect();
|
|
m_SavedCompleter->complete(r);
|
|
|
|
m_CurrentFilterText = filter;
|
|
}
|
|
|
|
void EventBrowser::savedFilter_keyPress(QKeyEvent *e)
|
|
{
|
|
if(!m_SavedCompleter->popup()->isVisible())
|
|
return;
|
|
|
|
switch(e->key())
|
|
{
|
|
// if a completion is in progress ignore any events the completer will process
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
m_SavedCompleter->activated(m_SavedCompleter->popup()->selectionModel()->currentIndex());
|
|
m_SavedCompleter->popup()->hide();
|
|
return;
|
|
// allow key scrolling
|
|
case Qt::Key_Up:
|
|
case Qt::Key_Down:
|
|
case Qt::Key_PageUp:
|
|
case Qt::Key_PageDown: return;
|
|
default: break;
|
|
}
|
|
|
|
// all other keys close the popup
|
|
m_SavedCompleter->popup()->hide();
|
|
}
|
|
|
|
void EventBrowser::on_filterExpression_textChanged()
|
|
{
|
|
if(!ui->filterExpression->completionInProgress())
|
|
m_FilterTimeout->start();
|
|
|
|
m_ParseError->setText(QString());
|
|
m_ParseError->hide();
|
|
ui->filterExpression->setExtraSelections({});
|
|
}
|
|
|
|
void EventBrowser::on_filterExpression_mouseMoved(QMouseEvent *event)
|
|
{
|
|
if(!m_ParseError->text().isEmpty())
|
|
{
|
|
QPoint pos = ui->filterExpression->viewport()->mapToGlobal(m_ParseErrorPos);
|
|
pos.setY(pos.y() + 4);
|
|
|
|
m_ParseError->move(pos);
|
|
m_ParseError->show();
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_filterExpression_hoverEnter()
|
|
{
|
|
if(!m_ParseError->text().isEmpty())
|
|
{
|
|
QPoint pos = ui->filterExpression->viewport()->mapToGlobal(m_ParseErrorPos);
|
|
pos.setY(pos.y() + 4);
|
|
|
|
m_ParseError->move(pos);
|
|
m_ParseError->show();
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_filterExpression_hoverLeave()
|
|
{
|
|
m_ParseError->hide();
|
|
}
|
|
|
|
void EventBrowser::on_findEvent_returnPressed()
|
|
{
|
|
// stop the timer, we'll manually fire it instantly
|
|
if(m_FindHighlight->isActive())
|
|
m_FindHighlight->stop();
|
|
|
|
findHighlight_timeout();
|
|
}
|
|
|
|
void EventBrowser::on_findEvent_keyPress(QKeyEvent *event)
|
|
{
|
|
if(event->key() == Qt::Key_F3)
|
|
{
|
|
// stop the timer, we'll manually fire it instantly
|
|
if(m_FindHighlight->isActive())
|
|
m_FindHighlight->stop();
|
|
|
|
if(!ui->findEvent->text().isEmpty())
|
|
SelectEvent(m_FilterModel->mapFromSource(
|
|
m_Model->Find(event->modifiers() & Qt::ShiftModifier ? false : true)));
|
|
|
|
event->accept();
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_findNext_clicked()
|
|
{
|
|
SelectEvent(m_FilterModel->mapFromSource(m_Model->Find(true)));
|
|
updateFindResultsAvailable();
|
|
}
|
|
|
|
void EventBrowser::on_findPrev_clicked()
|
|
{
|
|
SelectEvent(m_FilterModel->mapFromSource(m_Model->Find(false)));
|
|
updateFindResultsAvailable();
|
|
}
|
|
|
|
void EventBrowser::on_stepNext_clicked()
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded() || !ui->stepNext->isEnabled())
|
|
return;
|
|
|
|
const DrawcallDescription *draw = m_Ctx.CurDrawcall();
|
|
|
|
if(draw)
|
|
draw = draw->next;
|
|
|
|
// special case for the first 'virtual' draw at EID 0
|
|
if(m_Ctx.CurEvent() == 0)
|
|
draw = m_Ctx.GetFirstDrawcall();
|
|
|
|
while(draw)
|
|
{
|
|
// try to select the next event. If successful, stop
|
|
if(SelectEvent(draw->eventId))
|
|
return;
|
|
|
|
// if it failed, possibly the next draw is filtered out. Step along the list until we find one
|
|
// which isn't
|
|
draw = draw->next;
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_stepPrev_clicked()
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded() || !ui->stepPrev->isEnabled())
|
|
return;
|
|
|
|
const DrawcallDescription *draw = m_Ctx.CurDrawcall();
|
|
|
|
if(draw)
|
|
draw = draw->previous;
|
|
|
|
while(draw)
|
|
{
|
|
// try to select the previous event. If successful, stop
|
|
if(SelectEvent(draw->eventId))
|
|
return;
|
|
|
|
// if it failed, possibly the previous draw is filtered out. Step along the list until we find
|
|
// one which isn't
|
|
draw = draw->previous;
|
|
}
|
|
|
|
// special case for the first 'virtual' draw at EID 0
|
|
SelectEvent(0);
|
|
}
|
|
|
|
void EventBrowser::on_exportDraws_clicked()
|
|
{
|
|
QString filename =
|
|
RDDialog::getSaveFileName(this, tr("Save Event List"), QString(), tr("Text files (*.txt)"));
|
|
|
|
if(!filename.isEmpty())
|
|
{
|
|
ANALYTIC_SET(Export.EventBrowser, true);
|
|
|
|
QDir dirinfo = QFileInfo(filename).dir();
|
|
if(dirinfo.exists())
|
|
{
|
|
QFile f(filename);
|
|
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
|
{
|
|
QTextStream stream(&f);
|
|
|
|
stream << tr("%1 - Frame #%2\n\n")
|
|
.arg(m_Ctx.GetCaptureFilename())
|
|
.arg(m_Ctx.FrameInfo().frameNumber);
|
|
|
|
int maxNameLength = 0;
|
|
|
|
QModelIndex root = ui->events->model()->index(0, COL_NAME);
|
|
|
|
// skip child 0, which is the Capture Start entry that we don't want to include
|
|
for(int i = 1, rowCount = ui->events->model()->rowCount(root); i < rowCount; i++)
|
|
GetMaxNameLength(maxNameLength, 0, false, ui->events->model()->index(i, COL_NAME, root));
|
|
|
|
QString line = QFormatStr(" EID | %1 | Draw #").arg(lit("Event"), -maxNameLength);
|
|
|
|
if(m_Model->HasTimes())
|
|
{
|
|
line += QFormatStr(" | %1 (%2)").arg(tr("Duration")).arg(ToQStr(m_TimeUnit));
|
|
}
|
|
|
|
stream << line << "\n";
|
|
|
|
line = QFormatStr("--------%1-----------").arg(QString(), maxNameLength, QLatin1Char('-'));
|
|
|
|
if(m_Model->HasTimes())
|
|
{
|
|
int maxDurationLength = 0;
|
|
maxDurationLength = qMax(maxDurationLength, Formatter::Format(1.0).length());
|
|
maxDurationLength = qMax(maxDurationLength, Formatter::Format(1.2345e-200).length());
|
|
maxDurationLength =
|
|
qMax(maxDurationLength, Formatter::Format(123456.7890123456789).length());
|
|
line += QString(3 + maxDurationLength, QLatin1Char('-')); // 3 extra for " | "
|
|
}
|
|
|
|
stream << line << "\n";
|
|
|
|
for(int i = 1, rowCount = ui->events->model()->rowCount(root); i < rowCount; i++)
|
|
ExportDrawcall(stream, maxNameLength, 0, false,
|
|
ui->events->model()->index(i, COL_NAME, root));
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Error saving event list"),
|
|
tr("Couldn't open path %1 for write.\n%2").arg(filename).arg(f.errorString()));
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(this, tr("Invalid directory"),
|
|
tr("Cannot find target directory to save to"));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_colSelect_clicked()
|
|
{
|
|
QStringList headers;
|
|
for(int i = 0; i < ui->events->model()->columnCount(); i++)
|
|
headers << ui->events->model()->headerData(i, Qt::Horizontal).toString();
|
|
UpdateVisibleColumns(tr("Select Event Browser Columns"), COL_COUNT, ui->events->header(), headers);
|
|
}
|
|
|
|
QString EventBrowser::GetExportString(int indent, bool firstchild, const QModelIndex &idx)
|
|
{
|
|
QString prefix = QString(indent * 2 - (firstchild ? 1 : 0), QLatin1Char(' '));
|
|
if(firstchild)
|
|
prefix += QLatin1Char('\\');
|
|
|
|
return QFormatStr("%1- %2").arg(prefix).arg(
|
|
RichResourceTextFormat(m_Ctx, idx.data(Qt::DisplayRole)));
|
|
}
|
|
|
|
void EventBrowser::GetMaxNameLength(int &maxNameLength, int indent, bool firstchild,
|
|
const QModelIndex &idx)
|
|
{
|
|
QString nameString = GetExportString(indent, firstchild, idx);
|
|
|
|
maxNameLength = qMax(maxNameLength, nameString.count());
|
|
|
|
firstchild = true;
|
|
|
|
for(int i = 0, rowCount = idx.model()->rowCount(idx); i < rowCount; i++)
|
|
{
|
|
GetMaxNameLength(maxNameLength, indent + 1, firstchild, idx.child(i, COL_NAME));
|
|
firstchild = false;
|
|
}
|
|
}
|
|
|
|
void EventBrowser::ExportDrawcall(QTextStream &writer, int maxNameLength, int indent,
|
|
bool firstchild, const QModelIndex &idx)
|
|
{
|
|
QString nameString = GetExportString(indent, firstchild, idx);
|
|
|
|
QModelIndex eidIdx = idx.model()->sibling(idx.row(), COL_EID, idx);
|
|
QModelIndex drawIdx = idx.model()->sibling(idx.row(), COL_DRAW, idx);
|
|
|
|
QString line = QFormatStr("%1 | %2 | %3")
|
|
.arg(eidIdx.data(Qt::DisplayRole).toString(), -5)
|
|
.arg(nameString, -maxNameLength)
|
|
.arg(drawIdx.data(Qt::DisplayRole).toString(), -6);
|
|
|
|
if(m_Model->HasTimes())
|
|
{
|
|
QVariant duration = idx.model()->sibling(idx.row(), COL_DURATION, idx).data(Qt::DisplayRole);
|
|
|
|
if(duration != QVariant())
|
|
{
|
|
line += QFormatStr(" | %1").arg(duration.toString());
|
|
}
|
|
else
|
|
{
|
|
line += lit(" |");
|
|
}
|
|
}
|
|
|
|
writer << line << "\n";
|
|
|
|
firstchild = true;
|
|
|
|
for(int i = 0, rowCount = idx.model()->rowCount(idx); i < rowCount; i++)
|
|
{
|
|
ExportDrawcall(writer, maxNameLength, indent + 1, firstchild, idx.child(i, COL_NAME));
|
|
firstchild = false;
|
|
}
|
|
}
|
|
|
|
QVariant EventBrowser::persistData()
|
|
{
|
|
QVariantMap state;
|
|
|
|
// temporarily turn off stretching the last section so we can get the real sizes.
|
|
ui->events->header()->setStretchLastSection(false);
|
|
|
|
QVariantList columns;
|
|
for(int i = 0; i < COL_COUNT; i++)
|
|
{
|
|
QVariantMap col;
|
|
|
|
bool hidden = ui->events->header()->isSectionHidden(i);
|
|
|
|
// we temporarily make the section visible to get its size, since otherwise it returns 0.
|
|
// There's no other way to access the 'hidden section sizes' which are transient and will be
|
|
// lost otherwise.
|
|
ui->events->header()->showSection(i);
|
|
int size = ui->events->header()->sectionSize(i);
|
|
if(hidden)
|
|
ui->events->header()->hideSection(i);
|
|
|
|
// name is just informative
|
|
col[lit("name")] = ui->events->model()->headerData(i, Qt::Horizontal);
|
|
col[lit("index")] = ui->events->header()->visualIndex(i);
|
|
col[lit("hidden")] = hidden;
|
|
col[lit("size")] = size;
|
|
columns.push_back(col);
|
|
}
|
|
|
|
ui->events->header()->setStretchLastSection(true);
|
|
|
|
state[lit("columns")] = columns;
|
|
|
|
return state;
|
|
}
|
|
|
|
void EventBrowser::setPersistData(const QVariant &persistData)
|
|
{
|
|
QVariantMap state = persistData.toMap();
|
|
|
|
QVariantList columns = state[lit("columns")].toList();
|
|
for(int i = 0; i < columns.count() && i < COL_COUNT; i++)
|
|
{
|
|
QVariantMap col = columns[i].toMap();
|
|
|
|
int oldVisIdx = ui->events->header()->visualIndex(i);
|
|
int visIdx = col[lit("index")].toInt();
|
|
int size = col[lit("size")].toInt();
|
|
bool hidden = col[lit("hidden")].toBool();
|
|
|
|
ui->events->header()->moveSection(oldVisIdx, visIdx);
|
|
ui->events->header()->resizeSection(i, size);
|
|
if(hidden)
|
|
ui->events->header()->hideSection(i);
|
|
else
|
|
ui->events->header()->showSection(i);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::events_keyPress(QKeyEvent *event)
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return;
|
|
|
|
if(event->key() == Qt::Key_F3)
|
|
{
|
|
if(event->modifiers() == Qt::ShiftModifier)
|
|
SelectEvent(m_FilterModel->mapFromSource(m_Model->Find(false)));
|
|
else
|
|
SelectEvent(m_FilterModel->mapFromSource(m_Model->Find(true)));
|
|
updateFindResultsAvailable();
|
|
}
|
|
|
|
if(event->modifiers() == Qt::ControlModifier)
|
|
{
|
|
// support Ctrl-G as a legacy shortcut, from the old 'goto EID' which was separate from find
|
|
if(event->key() == Qt::Key_F || event->key() == Qt::Key_G)
|
|
{
|
|
on_find_toggled(true);
|
|
event->accept();
|
|
}
|
|
else if(event->key() == Qt::Key_B)
|
|
{
|
|
on_bookmark_clicked();
|
|
event->accept();
|
|
}
|
|
else if(event->key() == Qt::Key_T)
|
|
{
|
|
on_timeDraws_clicked();
|
|
event->accept();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EventBrowser::events_contextMenu(const QPoint &pos)
|
|
{
|
|
QModelIndex index = ui->events->indexAt(pos);
|
|
|
|
QMenu contextMenu(this);
|
|
|
|
QAction expandAll(tr("&Expand All"), this);
|
|
QAction collapseAll(tr("&Collapse All"), this);
|
|
QAction toggleBookmark(tr("Toggle &Bookmark"), this);
|
|
QAction selectCols(tr("&Select Columns..."), this);
|
|
QAction rgpSelect(tr("Select &RGP Event"), this);
|
|
rgpSelect.setIcon(Icons::connect());
|
|
|
|
contextMenu.addAction(&expandAll);
|
|
contextMenu.addAction(&collapseAll);
|
|
contextMenu.addAction(&toggleBookmark);
|
|
contextMenu.addAction(&selectCols);
|
|
|
|
expandAll.setIcon(Icons::arrow_out());
|
|
collapseAll.setIcon(Icons::arrow_in());
|
|
toggleBookmark.setIcon(Icons::asterisk_orange());
|
|
selectCols.setIcon(Icons::timeline_marker());
|
|
|
|
expandAll.setEnabled(index.isValid() && ui->events->model()->rowCount(index) > 0);
|
|
collapseAll.setEnabled(expandAll.isEnabled());
|
|
toggleBookmark.setEnabled(m_Ctx.IsCaptureLoaded());
|
|
|
|
QObject::connect(&expandAll, &QAction::triggered,
|
|
[this, index]() { ui->events->expandAll(index); });
|
|
|
|
QObject::connect(&collapseAll, &QAction::triggered,
|
|
[this, index]() { ui->events->collapseAll(index); });
|
|
|
|
QObject::connect(&toggleBookmark, &QAction::triggered, this, &EventBrowser::on_bookmark_clicked);
|
|
|
|
QObject::connect(&selectCols, &QAction::triggered, this, &EventBrowser::on_colSelect_clicked);
|
|
|
|
IRGPInterop *rgp = m_Ctx.GetRGPInterop();
|
|
if(rgp && rgp->HasRGPEvent(m_Ctx.CurEvent()))
|
|
{
|
|
contextMenu.addAction(&rgpSelect);
|
|
QObject::connect(&rgpSelect, &QAction::triggered,
|
|
[this, rgp]() { rgp->SelectRGPEvent(m_Ctx.CurEvent()); });
|
|
}
|
|
|
|
contextMenu.addSeparator();
|
|
|
|
m_Ctx.Extensions().MenuDisplaying(ContextMenu::EventBrowser_Event, &contextMenu,
|
|
{{"eventId", m_Ctx.CurEvent()}});
|
|
|
|
RDDialog::show(&contextMenu, ui->events->viewport()->mapToGlobal(pos));
|
|
}
|
|
|
|
void EventBrowser::clearBookmarks()
|
|
{
|
|
for(QToolButton *b : m_BookmarkButtons)
|
|
delete b;
|
|
|
|
m_BookmarkButtons.clear();
|
|
|
|
ui->bookmarkStrip->setVisible(false);
|
|
}
|
|
|
|
void EventBrowser::repopulateBookmarks()
|
|
{
|
|
const rdcarray<EventBookmark> bookmarks = m_Ctx.GetBookmarks();
|
|
|
|
// add any bookmark markers that we don't have
|
|
for(const EventBookmark &mark : bookmarks)
|
|
{
|
|
if(!m_BookmarkButtons.contains(mark.eventId))
|
|
{
|
|
uint32_t EID = mark.eventId;
|
|
|
|
QToolButton *but = new QToolButton(this);
|
|
|
|
but->setText(QString::number(EID));
|
|
but->setCheckable(true);
|
|
but->setAutoRaise(true);
|
|
but->setProperty("eid", EID);
|
|
QObject::connect(but, &QToolButton::clicked, [this, but, EID]() {
|
|
but->setChecked(true);
|
|
if(!SelectEvent(EID))
|
|
ui->events->setCurrentIndex(QModelIndex());
|
|
highlightBookmarks();
|
|
});
|
|
|
|
m_BookmarkButtons[EID] = but;
|
|
|
|
highlightBookmarks();
|
|
|
|
m_BookmarkStripLayout->removeItem(m_BookmarkSpacer);
|
|
m_BookmarkStripLayout->addWidget(but);
|
|
m_BookmarkStripLayout->addItem(m_BookmarkSpacer);
|
|
}
|
|
}
|
|
|
|
// remove any bookmark markers we shouldn't have
|
|
for(uint32_t EID : m_BookmarkButtons.keys())
|
|
{
|
|
if(!bookmarks.contains(EventBookmark(EID)))
|
|
{
|
|
delete m_BookmarkButtons[EID];
|
|
m_BookmarkButtons.remove(EID);
|
|
}
|
|
}
|
|
|
|
ui->bookmarkStrip->setVisible(!bookmarks.isEmpty());
|
|
|
|
m_Model->RefreshCache();
|
|
}
|
|
|
|
void EventBrowser::jumpToBookmark(int idx)
|
|
{
|
|
const rdcarray<EventBookmark> bookmarks = m_Ctx.GetBookmarks();
|
|
if(idx < 0 || idx >= bookmarks.count() || !m_Ctx.IsCaptureLoaded())
|
|
return;
|
|
|
|
// don't exclude ourselves, so we're updated as normal
|
|
if(!SelectEvent(bookmarks[idx].eventId))
|
|
ui->events->setCurrentIndex(QModelIndex());
|
|
}
|
|
|
|
void EventBrowser::highlightBookmarks()
|
|
{
|
|
for(uint32_t eid : m_BookmarkButtons.keys())
|
|
{
|
|
if(eid == m_Ctx.CurEvent())
|
|
m_BookmarkButtons[eid]->setChecked(true);
|
|
else
|
|
m_BookmarkButtons[eid]->setChecked(false);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::ExpandNode(QModelIndex idx)
|
|
{
|
|
QModelIndex i = idx;
|
|
while(idx.isValid())
|
|
{
|
|
ui->events->expand(idx);
|
|
idx = idx.parent();
|
|
}
|
|
|
|
if(i.isValid())
|
|
ui->events->scrollTo(i);
|
|
}
|
|
|
|
bool EventBrowser::SelectEvent(uint32_t eventId)
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return false;
|
|
|
|
QModelIndex found = m_FilterModel->mapFromSource(m_Model->GetIndexForEID(eventId));
|
|
if(found.isValid())
|
|
{
|
|
SelectEvent(found);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void EventBrowser::SelectEvent(QModelIndex idx)
|
|
{
|
|
if(!idx.isValid())
|
|
return;
|
|
|
|
ui->events->selectionModel()->setCurrentIndex(
|
|
idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
|
|
|
ExpandNode(idx);
|
|
}
|
|
|
|
void EventBrowser::updateFindResultsAvailable()
|
|
{
|
|
if(m_Model->NumFindResults() > 0 || ui->findEvent->text().isEmpty())
|
|
ui->findEvent->setPalette(palette());
|
|
else
|
|
ui->findEvent->setPalette(m_redPalette);
|
|
}
|
|
|
|
void EventBrowser::UpdateDurationColumn()
|
|
{
|
|
if(m_TimeUnit == m_Ctx.Config().EventBrowser_TimeUnit)
|
|
return;
|
|
|
|
m_TimeUnit = m_Ctx.Config().EventBrowser_TimeUnit;
|
|
|
|
m_Model->UpdateDurationColumn();
|
|
}
|
|
|
|
APIEvent EventBrowser::GetAPIEventForEID(uint32_t eid)
|
|
{
|
|
const DrawcallDescription *draw = GetDrawcallForEID(eid);
|
|
|
|
if(draw)
|
|
{
|
|
for(const APIEvent &ev : draw->events)
|
|
{
|
|
if(ev.eventId == eid)
|
|
return ev;
|
|
}
|
|
}
|
|
|
|
return APIEvent();
|
|
}
|
|
|
|
const DrawcallDescription *EventBrowser::GetDrawcallForEID(uint32_t eid)
|
|
{
|
|
return m_Model->GetDrawcallForEID(eid);
|
|
}
|
|
|
|
bool EventBrowser::IsAPIEventVisible(uint32_t eid)
|
|
{
|
|
return m_FilterModel->mapFromSource(m_Model->GetIndexForEID(eid)).isValid();
|
|
}
|
|
|
|
bool EventBrowser::RegisterEventFilterFunction(const rdcstr &name, const rdcstr &description,
|
|
EventFilterCallback filter, FilterParseCallback parser,
|
|
AutoCompleteCallback completer)
|
|
{
|
|
return EventFilterModel::RegisterEventFilterFunction(name, description, filter, parser, completer);
|
|
}
|
|
|
|
bool EventBrowser::UnregisterEventFilterFunction(const rdcstr &name)
|
|
{
|
|
return EventFilterModel::UnregisterEventFilterFunction(name);
|
|
}
|
|
|
|
void EventBrowser::SetCurrentFilterText(const rdcstr &text)
|
|
{
|
|
ui->filterExpression->setPlainText(text);
|
|
filter_apply();
|
|
}
|
|
|
|
rdcstr EventBrowser::GetCurrentFilterText()
|
|
{
|
|
return ui->filterExpression->toPlainText();
|
|
}
|
|
|
|
void EventBrowser::SetShowParameterNames(bool show)
|
|
{
|
|
m_Model->SetShowParameterNames(show);
|
|
}
|
|
|
|
void EventBrowser::SetShowAllParameters(bool show)
|
|
{
|
|
m_Model->SetShowAllParameters(show);
|
|
}
|
|
|
|
void EventBrowser::SetUseCustomDrawNames(bool use)
|
|
{
|
|
m_Model->SetUseCustomDrawNames(use);
|
|
}
|
|
|
|
void EventBrowser::SetEmptyRegionsVisible(bool show)
|
|
{
|
|
m_FilterModel->SetEmptyRegionsVisible(show);
|
|
}
|