Files
renderdoc/qrenderdoc/Windows/EventBrowser.cpp
T
baldurk f8cf943c42 Fix problem where a new profile wouldn't have default event filter
* In v1.15 if you upgraded from a previous version and had a renderdoc config
  file then the persistent storage storing the filters would get loaded with an
  empty variant map, so the defaults could be set. However if you had no config
  file at all this wouldn't happen so no defaults would be set.
* We detect the case where we load an old profile and it has blank current
  filter or saved filters, and fill in the defaults now. If the filter has been
  customised or some saved filters exist, respectively, we don't touch them.
2021-08-24 10:57:36 +01:00

5413 lines
170 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())
{
version = LatestVersion;
SetDefaultCurrentFilter();
AddDefaultSavedFilters();
}
EventBrowserPersistentStorage(rdcstr name) : CustomPersistentStorage(name)
{
version = LatestVersion;
SetDefaultCurrentFilter();
AddDefaultSavedFilters();
}
void save(QVariant &v) const
{
QVariantMap settings;
settings[lit("version")] = LatestVersion;
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();
int loadedVersion = settings[lit("version")].toInt();
QVariant current = settings[lit("current")];
if(current.isValid() && current.type() == QVariant::String)
{
CurrentFilter = current.toString();
}
else
{
SetDefaultCurrentFilter();
}
SavedFilters.clear();
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
{
AddDefaultSavedFilters();
}
// version 2 we fixed an issue where a default profile wouldn't have the proper default filter
// etc. That shipped in v1.15.
// If we detect an old profile being loaded AND there are: no saved filters, or empty filter, we
// choose to override and set the default filter/add saved filters. If the user has set a
// filter, or saved some, we don't do anything as they have used the feature and we'll respect
// what they've done. If the user has deliberately left it blank we have no way of telling, so
// we set the default filter to cover the common case of someone who hasn't used the filter at
// all.
if(loadedVersion < 2)
{
if(CurrentFilter.isEmpty())
SetDefaultCurrentFilter();
if(SavedFilters.isEmpty())
AddDefaultSavedFilters();
}
}
void SetDefaultCurrentFilter() { CurrentFilter = lit("$action()"); }
void AddDefaultSavedFilters()
{
SavedFilters.push_back(qMakePair(lit("Default"), lit("$action()")));
SavedFilters.push_back(qMakePair(lit("Actions and Barriers"), lit("$action() Barrier")));
SavedFilters.push_back(qMakePair(lit("Hide Copies & Clears"), lit("$action() -Copy -Clear")));
}
static const int LatestVersion = 2;
int version;
QString CurrentFilter;
QList<QPair<QString, QString>> SavedFilters;
};
static EventBrowserPersistentStorage persistantStorage("EventBrowser");
enum
{
COL_NAME,
COL_EID,
COL_ACTION,
COL_DURATION,
COL_COUNT,
};
enum
{
ROLE_SELECTED_EID = Qt::UserRole,
ROLE_EFFECTIVE_EID,
ROLE_GROUPED_ACTION,
ROLE_EXACT_ACTION,
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_Actions.clear();
m_Chunks.clear();
m_Times.clear();
if(!m_Ctx.CurRootActions().empty())
m_Nodes[0] = CreateActionNode(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> &times)
{
// set all times for events to -1.0
m_Times.fill(m_Actions.size() + 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 UseCustomActionNames() { return m_UseCustomActionNames; }
void SetUseCustomActionNames(bool use)
{
if(m_UseCustomActionNames != use)
{
m_UseCustomActionNames = 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_Actions.size())
{
// if the text doesn't exactly match the EID after converting back, don't treat this as an
// EID-only search
eidSearch = (text.trimmed() == QString::number(eid));
if(eidSearch)
m_FindResults.push_back(GetIndexForEID(eid));
}
m_FindEIDSearch = eidSearch;
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, QModelIndex eidStart)
{
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(eidStart);
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(eidStart, 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() const { return m_FindResults.count(); }
bool FindEIDSearch() const { return m_FindEIDSearch; }
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 ActionDescription *action = m_Actions[eid];
if(action)
{
// 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 = action->parent ? 0 : 1;
const rdcarray<ActionDescription> &actions =
action->parent ? action->parent->children : m_Ctx.CurRootActions();
for(const ActionDescription &a : actions)
{
// the first action with an EID greater than the one we're searching for should contain it.
if(a.eventId >= eid)
{
// except if the action 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(a.IsFakeMarker())
{
if(a.eventId == eid)
break;
if(a.children[0].eventId < eid)
{
rowInParent++;
continue;
}
}
// keep counting until we get to the row within this action
for(size_t i = 0; i < a.events.size(); i++)
{
if(a.events[i].eventId < eid)
{
rowInParent++;
continue;
}
if(a.events[i].eventId != eid)
qCritical() << "Couldn't find event" << eid << "within action" << action->eventId;
// stop, we should be at the event now
break;
}
break;
}
rowInParent += a.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 action for event" << eid;
}
return QModelIndex();
}
const ActionDescription *GetActionForEID(uint32_t eid)
{
if(eid < m_Actions.size())
return m_Actions[eid];
return NULL;
}
rdcstr GetEventName(uint32_t eid)
{
if(eid < m_Actions.size())
return RichResourceTextFormat(m_Ctx, GetCachedEIDName(eid));
return rdcstr();
}
rdcarray<rdcstr> GetMarkerList() const
{
rdcarray<rdcstr> ret;
for(auto it = m_Nodes.begin(); it != m_Nodes.end(); ++it)
if(it.value().action && (it.value().action->flags & ActionFlags::PushMarker) &&
!it.value().action->customName.isEmpty())
ret.push_back(it.value().action->customName);
return ret;
}
QModelIndex GetCurrentEID() const { return m_CurrentEID; }
//////////////////////////////////////////////////////////////////////////////////////
//
// 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 action node
return GetIndexForActionChildRow(m_Nodes[0], row, column);
}
else if(parent.internalId() == TagCaptureStart)
{
// no children for this
return QModelIndex();
}
else
{
// otherwise the parent is a real action
auto it = m_Nodes.find(parent.internalId());
if(it != m_Nodes.end())
return GetIndexForActionChildRow(*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_Actions.size())
return QModelIndex();
// otherwise it's an action
const ActionDescription *action = m_Actions[index.internalId()];
// if it has no parent action, the parent is the root
if(action->parent == NULL)
return createIndex(0, 0, TagRoot);
// the parent must be a node since it has at least one child (this action), so we'll have a
// cached
// index
return m_Nodes[action->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_ACTION: return lit("Action #");
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_ACTION: 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_Actions.size())
return QVariant();
if(role == ROLE_SELECTED_EID)
return eid;
if(role == ROLE_GROUPED_ACTION)
return QVariant::fromValue(qulonglong(m_Actions[eid]));
if(role == ROLE_EXACT_ACTION)
{
const ActionDescription *action = m_Actions[eid];
return action->eventId == eid ? QVariant::fromValue(qulonglong(m_Actions[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 ActionDescription *action = m_Actions[eid];
switch(index.column())
{
case COL_NAME: return GetCachedEIDName(eid);
case COL_EID:
case COL_ACTION:
if(action->eventId == eid && !action->children.empty())
{
uint32_t effectiveEID = eid;
auto it = m_Nodes.find(eid);
if(it != m_Nodes.end())
effectiveEID = it->effectiveEID;
if(action->IsFakeMarker())
eid = action->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 actionId = action->actionId;
uint32_t endActionId = m_Actions[effectiveEID]->actionId;
if(action->IsFakeMarker())
actionId = action->children[0].actionId;
return actionId == endActionId
? QVariant()
: QVariant(QFormatStr("%1-%2").arg(actionId).arg(endActionId));
}
}
if(index.column() == COL_EID)
return eid;
else
return action->eventId == eid &&
!(action->flags & (ActionFlags::SetMarker | ActionFlags::PushMarker |
ActionFlags::PopMarker))
? QVariant(action->actionId)
: 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 ActionDescription *action = m_Actions[eid];
// skip events that aren't the actual action
if(action->eventId != eid)
return QVariant();
// if alpha isn't 0, assume the colour is valid
if((action->flags & (ActionFlags::PushMarker | ActionFlags::SetMarker)) &&
action->markerColor.w > 0.0f)
{
QColor col =
QColor::fromRgb(qRgb(action->markerColor.x * 255.0f, action->markerColor.y * 255.0f,
action->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;
bool m_FindEIDSearch;
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 action that contains it.
rdcarray<const ActionDescription *> m_Actions;
rdcarray<const SDChunk *> m_Chunks;
// we can have a bigger structure for every nested node (i.e. action 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
// actions/events.
// we cache this with a depth-first search at init time while populating m_Actions
struct ActionTreeNode
{
const ActionDescription *action;
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 action 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 action. 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 action
QMap<int, size_t> row2action;
static const int Row2ActionFactor = 100;
};
QMap<uint32_t, ActionTreeNode> m_Nodes;
bool m_ShowParameterNames = false;
bool m_ShowAllParameters = false;
bool m_UseCustomActionNames = 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(ActionTreeNode &node)
{
const rdcarray<ActionDescription> &actionChildren =
node.action ? node.action->children : m_Ctx.CurRootActions();
double duration = 0.0;
for(const ActionDescription &a : actionChildren)
{
// ignore out of bounds EIDs - should not happen
if(a.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[a.eventId]);
}
m_Times[node.action ? node.action->eventId : 0] = duration;
}
ActionTreeNode CreateActionNode(const ActionDescription *action)
{
const rdcarray<ActionDescription> &actionRange =
action ? action->children : m_Ctx.CurRootActions();
// account for the Capture Start row we'll add at the top level
int row = action ? 0 : 1;
ActionTreeNode ret;
ret.action = action;
ret.effectiveEID = actionRange.back().eventId;
ret.row2action[0] = 0;
uint32_t row2eidStride = (actionRange.count() / ActionTreeNode::Row2ActionFactor) + 1;
const SDFile &sdfile = m_Ctx.GetStructuredFile();
for(int i = 0; i < actionRange.count(); i++)
{
const ActionDescription &a = actionRange[i];
if((i % row2eidStride) == 0)
ret.row2action[row] = i;
for(const APIEvent &e : a.events)
{
m_Actions.resize_for_index(e.eventId);
m_Chunks.resize_for_index(e.eventId);
m_Actions[e.eventId] = &a;
if(e.chunkIndex != APIEvent::NoChunk && e.chunkIndex < sdfile.chunks.size())
m_Chunks[e.eventId] = sdfile.chunks[e.chunkIndex];
}
row += a.events.count();
if(a.children.empty())
continue;
ActionTreeNode node = CreateActionNode(&a);
node.index = createIndex(row - 1, 0, a.eventId);
if(a.eventId == ret.effectiveEID)
ret.effectiveEID = node.effectiveEID;
m_Nodes[a.eventId] = node;
}
ret.rowCount = row;
return ret;
}
QModelIndex GetIndexForActionChildRow(const ActionTreeNode &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 actions 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 action node
// which helps lower the cost significantly (by a factor of the roughly number of skip entries
// we store).
const rdcarray<ActionDescription> &actions =
node.action ? node.action->children : m_Ctx.CurRootActions();
int curRow = 0;
size_t curAction = 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.row2action.upperBound(row);
if(it != node.row2action.begin())
--it;
// start at the first skip before the desired row
curRow = it.key();
curAction = 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 action node"
<< (node.action ? node.action->eventId : 0);
// start at the first child row instead
curRow = 0;
curAction = 0;
}
// if this action doesn't contain the desired row, advance
while(curAction < actions.size() && curRow + actions[curAction].events.count() <= row)
{
curRow += actions[curAction].events.count();
curAction++;
}
// we've iterated all the actions and didn't come up with this row - but we checked above that
// we're in bounds of rowCount so something went wrong
if(curAction >= actions.size())
{
qCritical() << "Couldn't find action containing row" << row << "in action node"
<< (node.action ? node.action->eventId : 0);
return QModelIndex();
}
// now curAction contains the row at the idx'th event
int idx = row - curRow;
if(idx < 0 || idx >= actions[curAction].events.count())
{
qCritical() << "Got invalid relative index for row" << row << "in action node"
<< (node.action ? node.action->eventId : 0);
return QModelIndex();
}
return createIndex(row, column, actions[curAction].events[idx].eventId);
}
QVariant GetCachedEIDName(uint32_t eid) const
{
auto it = m_EIDNameCache.find(eid);
if(it != m_EIDNameCache.end())
return it.value();
const ActionDescription *action = m_Actions[eid];
QString name;
// markers always use the action 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 multiactions or
// command buffer boundaries.
if(action->eventId == eid)
{
if(m_UseCustomActionNames)
{
name = action->customName;
}
else
{
if((action->flags & (ActionFlags::SetMarker | ActionFlags::PushMarker)) &&
!(action->flags & (ActionFlags::CommandBufferBoundary | ActionFlags::PassBoundary |
ActionFlags::CmdList | ActionFlags::MultiAction)))
name = action->customName;
}
}
if(name.isEmpty())
{
for(const APIEvent &e : action->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, ActionFlags> ActionFlagsLookup;
static QStringList ActionFlagsList;
void CacheActionFlagsLookup()
{
if(ActionFlagsLookup.empty())
{
for(uint32_t i = 0; i <= 31; i++)
{
ActionFlags flag = ActionFlags(1U << i);
// bit of a hack, see if it's a valid flag by stringising and seeing if it contains
// ActionFlags(
QString str = ToQStr(flag);
if(str.contains(lit("ActionFlags(")))
continue;
ActionFlagsList.push_back(str);
ActionFlagsLookup[str.toLower()] = flag;
}
ActionFlagsList.sort();
}
}
bool EvaluateFilterSet(ICaptureContext &ctx, const rdcarray<EventFilter> &filters, bool all,
uint32_t eid, const SDChunk *chunk, const ActionDescription *action,
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, action, 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(action);
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("action")].completer = [this](ICaptureContext *ctx, QString name,
QString parameters) {
return filterCompleter_action(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 ActionDescription *action =
(const ActionDescription *)(sourceModel()->data(source_idx, ROLE_GROUPED_ACTION).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, action, 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 ActionDescription *, 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(
<h3>$regex</h3>
<br />
<table>
<tr><td><code>$regex(/my regex.*match/i)</code></td>- passes if the specified regex matches the event name.</td></tr>
</table>
<p>
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 /:
</p>
<table>
<tr><td>/i</td><td>- Case insensitive match</td>
<tr><td>/x</td><td>- Extended syntax. See regex documentation for more information</td>
<tr><td>/u</td><td>- Use unicode properties. Character classes like \w and \d match more than ASCII</td>
</table>
)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 ActionDescription *, const rdcstr &name) {
QRegularExpressionMatch match = regex.match(QString(name));
return match.isValid() && match.hasMatch();
};
}
QString filterDescription_param() const
{
return tr(R"EOD(
<h3>$param</h3>
<br />
<table>
<tr><td><code>$param(name: value)</code><br />
<code>$param(name = value)</code></td><td> - passes if a given parameter matches a value.</td></tr>
</table>
<p>
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.
</p>
)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 ActionDescription *, 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(
<h3>$event</h3>
<br />
<table>
<tr><td><code>$event(condition)</code></td>- passes if an event property matches a condition.</td></tr>
</table>
<p>
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 <code>$event()</code>.
</p>
<p>
Available numeric properties. Compare with <code>$event(prop > 100)</code> or <code>$event(prop <= 200)</code>.
</p>
<table>
<tr><td>EID:</td> <td>The event's EID.</td></tr>
</table>
)EOD",
"EventFilterModel");
}
rdcarray<rdcstr> filterCompleter_event(ICaptureContext *ctx, const rdcstr &name,
const rdcstr &params)
{
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 ActionDescription *action,
const rdcstr &) { return eventId == eid; };
case 1:
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action,
const rdcstr &) { return eventId != eid; };
case 2:
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action,
const rdcstr &) { return eventId < eid; };
case 3:
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action,
const rdcstr &) { return eventId > eid; };
case 4:
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action,
const rdcstr &) { return eventId <= eid; };
case 5:
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action,
const rdcstr &) { return eventId >= eid; };
default: trace.setError(tr("Internal error", "EventFilterModel")); return NULL;
}
}
QString filterDescription_action() const
{
return tr(R"EOD(
<h3>$action</h3>
<br />
<table>
<tr><td>$action()</td><td>- passes if an event is an action.</td></tr>
<tr><td>$action(condition)</td><td>- passes if an event is an action and matches a condition.</td></tr>
</table>
<p>
If no condition is specified then the event is just included if it's an action - meaning any event
such as draws, dispatches, copies, clears and others that do work and modify resources.<br />
Otherwise the event is included if it's an action AND if the condition is true.
</p>
<p>Available numeric properties. Compare with <code>$action(prop > 100)</code> or <code>$action(prop <= 200)</code></p>
<table>
<tr><td>EID:</td> <td>The action's EID.</td></tr>
<tr><td>actionId:</td> <td>The action ID, starting from 1 and numbering each action.</td></tr>
<tr><td>numIndices:</td> <td>The number of vertices or indices in an action.</td></tr>
<tr><td>baseVertex:</td> <td>The base vertex value for an indexed action.</td></tr>
<tr><td>indexOffset:</td> <td>The index offset for an indexed action.</td></tr>
<tr><td>vertexOffset:</td> <td>The vertex offset for a non-indexed action.</td></tr>
<tr><td>instanceOffset:</td> <td>The instance offset for an instanced action.</td></tr>
<tr><td>dispatchX:</td> <td>The number of groups in the X dimension of a dispatch.</td></tr>
<tr><td>dispatchY:</td> <td>The number of groups in the Y dimension of a dispatch.</td></tr>
<tr><td>dispatchZ:</td> <td>The number of groups in the Z dimension of a dispatch.</td></tr>
<tr><td>dispatchSize:</td> <td>The total number of groups (X * Y * Z) of a dispatch.</td></tr>
<tr><td>duration:</td> <td>The listed duration of an action (only available with durations).</td></tr>
</table>
<p>
Also available is the <code>flags</code> property. Actions have different flags and properties
and these can be queried with a filter such as <code>$action(flags & Clear|ClearDepthStencil)</code>
</p>
<p>The flags available are:</p>
<table>
<tr><td>Clear:</td> <td>This is a clear call, clearing all or a subset of a resource.</td></tr>
<tr><td>Drawcall:</td> <td>This is a graphics pipeline action, rasterizing polygons.</td></tr>
<tr><td>Dispatch:</td> <td>This is a compute dispatch.</td></tr>
<tr><td>CmdList:</td> <td>This is part of the book-keeping for a command list, secondary command buffer or bundle.</td></tr>
<tr><td>SetMarker:</td> <td>This is an individual string debug marker, with no children.</td></tr>
<tr><td>PushMarker:</td> <td>This is a push of a debug marker region, with some child actions</td></tr>
<tr><td>PopMarker:</td> <td>This is the pop of a debug marker region.</td></tr>
<tr><td>Present:</td> <td>This is a graphics present to a window.</td></tr>
<tr><td>MultiAction:</td> <td>This is part of a multi-action, like a MultiDraw or ExecuteIndirect.</td></tr>
<tr><td>Copy:</td> <td>This is a copy call, copying between one resource and another.</td></tr>
<tr><td>Resolve:</td> <td>This is a resolve or non-identical blit, as opposed to a copy.</td></tr>
<tr><td>GenMips:</td> <td>This call is generating mip-maps for a texture</td></tr>
<tr><td>PassBoundary:</td> <td>This is the boundary of a 'pass' explicitly denoted by the API.</td></tr>
<tr><td>Indexed:</td> <td>For graphics pipeline drawcalls, the call uses an index buffer.</td></tr>
<tr><td>Instanced:</td> <td>For graphics pipeline drawcalls, the call uses instancing.</td></tr>
<tr><td>Auto:</td> <td>For graphics pipeline drawcalls, it's automatic based on stream-out.</td></tr>
<tr><td>Indirect:</td> <td>This is an indirect call, sourcing parameters from the GPU.</td></tr>
<tr><td>ClearColor:</td> <td>For clear calls, color values are cleared.</td></tr>
<tr><td>ClearDepthStencil:</td> <td>For clear calls, depth/stencil values are cleared.</td></tr>
<tr><td>BeginPass:</td> <td>For pass boundaries, this begins a pass. A boundary could begin and end.</td></tr>
<tr><td>EndPass:</td> <td>For pass boundaries, this ends a pass. A boundary could begin and end.</td></tr>
<tr><td>CommandBufferBoundary:</td> <td>This denotes the boundary of an entire command buffer.</td></tr>
</table>
)EOD",
"EventFilterModel");
}
rdcarray<rdcstr> filterCompleter_action(ICaptureContext *ctx, const rdcstr &name,
const rdcstr &params)
{
QList<Token> tokens = tokenise(params);
if(tokens.size() <= 1)
return {
"EID", "actionId", "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)
{
CacheActionFlagsLookup();
rdcarray<rdcstr> flags;
for(QString s : ActionFlagsList)
flags.push_back(s);
return flags;
}
return {};
}
IEventBrowser::EventFilterCallback filterFunction_action(QString name, QString parameters,
ParseTrace &trace)
{
// action(...) => returns true only for actions, optionally with a particular property filter
// PopMarker actions are actions so they can contain the other non-action events that might
// trail in a group, but we don't count them as real actions 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 an action
if(tokens.isEmpty())
return [](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::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 ActionDescription *);
struct NamedProp
{
const char *name;
PropGetter getter;
};
#define NAMED_PROP(name, access) \
{ \
name, [](const ActionDescription *action) -> int64_t { return access; } \
}
static const NamedProp namedProps[] = {
NAMED_PROP("eventid", action->eventId), NAMED_PROP("eid", action->eventId),
NAMED_PROP("actionid", action->actionId), NAMED_PROP("numindices", action->numIndices),
NAMED_PROP("numindexes", action->numIndices), NAMED_PROP("numvertices", action->numIndices),
NAMED_PROP("numvertexes", action->numIndices), NAMED_PROP("indexcount", action->numIndices),
NAMED_PROP("vertexcount", action->numIndices),
NAMED_PROP("numinstances", action->numInstances),
NAMED_PROP("instancecount", action->numInstances),
NAMED_PROP("basevertex", action->baseVertex), NAMED_PROP("indexoffset", action->indexOffset),
NAMED_PROP("vertexoffset", action->vertexOffset),
NAMED_PROP("instanceoffset", action->instanceOffset),
NAMED_PROP("dispatchx", action->dispatchDimension[0]),
NAMED_PROP("dispatchy", action->dispatchDimension[1]),
NAMED_PROP("dispatchz", action->dispatchDimension[2]),
NAMED_PROP("dispatchsize", action->dispatchDimension[0] * action->dispatchDimension[1] *
action->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 ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) == value;
};
case 1:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) != value;
};
case 2:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) < value;
};
case 3:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) > value;
};
case 4:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) <= value;
};
case 5:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && !(action->flags & ActionFlags::PopMarker) &&
propGetter(action) >= 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 ActionDescription *, 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 ActionDescription *, 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 ActionDescription *, 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 ActionDescription *, 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 $action(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 action flags expression", "EventFilterModel"));
return NULL;
}
CacheActionFlagsLookup();
ActionFlags flags = ActionFlags::NoFlags;
for(const QString &flagString : flagStrings)
{
auto it = ActionFlagsLookup.find(flagString.toLower());
if(it == ActionFlagsLookup.end())
{
trace.position = tokens[2].position;
trace.length = (tokens.back().position + tokens.back().length) - trace.position + 1;
trace.setError(tr("Unrecognised action flag '%1'", "EventFilterModel").arg(flagString));
return NULL;
}
flags |= it.value();
}
return [flags](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & flags);
};
}
else
{
trace.setError(tr("Unrecognised property expression", "EventFilterModel"));
return NULL;
}
}
QString filterDescription_dispatch() const
{
return tr(R"EOD(
<h3>$dispatch</h3>
<br />
<table>
<tr><td>$dispatch()</td><td>- passes if an event is a dispatch.</td></tr>
<tr><td>$dispatch(condition)</td><td>- passes if an event is a dispatch and matches a condition.</td></tr>
</table>
<p>
If no condition is specified then the event is just included if it's a dispatch.<br />
Otherwise the event is included if it's a dispatch AND if the condition is true.
</p>
<p>Available numeric properties. Compare with <code>$dispatch(prop > 100)</code> or
<code>$dispatch(prop <= 200)</code></p>
<table>
<tr><td>EID:</td> <td>The dispatch's EID.</td></tr>
<tr><td>actionId:</td> <td>The dispatch's action ID, starting from 1 and numbering each action.</td></tr>
<tr><td>x:</td> <td>The number of groups in the X dimension of the dispatch.</td></tr>
<tr><td>y:</td> <td>The number of groups in the Y dimension of the dispatch.</td></tr>
<tr><td>z:</td> <td>The number of groups in the Z dimension of the dispatch.</td></tr>
<tr><td>size:</td> <td>The total number of groups (X * Y * Z) of the dispatch.</td></tr>
<tr><td>duration:</td> <td>The listed duration of the dispatch (only available with durations).</td></tr>
</table>
)EOD",
"EventFilterModel");
}
rdcarray<rdcstr> filterCompleter_dispatch(ICaptureContext *ctx, const rdcstr &name,
const rdcstr &params)
{
QList<Token> tokens = tokenise(params);
if(tokens.size() <= 1)
return {
"EID", "actionId", "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 dispatch
if(tokens.isEmpty())
return [](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::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 ActionDescription *);
struct NamedProp
{
const char *name;
PropGetter getter;
};
static const NamedProp namedProps[] = {
NAMED_PROP("eventid", action->eventId), NAMED_PROP("eid", action->eventId),
NAMED_PROP("actionid", action->actionId),
NAMED_PROP("dispatchx", action->dispatchDimension[0]),
NAMED_PROP("dispatchy", action->dispatchDimension[1]),
NAMED_PROP("dispatchz", action->dispatchDimension[2]),
NAMED_PROP("x", action->dispatchDimension[0]),
NAMED_PROP("y", action->dispatchDimension[1]), NAMED_PROP("z", action->dispatchDimension[2]),
NAMED_PROP("dispatchsize", action->dispatchDimension[0] * action->dispatchDimension[1] *
action->dispatchDimension[2]),
NAMED_PROP("size", action->dispatchDimension[0] * action->dispatchDimension[1] *
action->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 ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) == value;
};
case 1:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) != value;
};
case 2:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) < value;
};
case 3:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) > value;
};
case 4:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) <= value;
};
case 5:
return [propGetter, value](ICaptureContext *, const rdcstr &, const rdcstr &,
uint32_t eventId, const SDChunk *,
const ActionDescription *action, const rdcstr &) {
return action->eventId == eventId && (action->flags & ActionFlags::Dispatch) &&
propGetter(action) >= 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 ActionDescription *action, const rdcstr &) {
double dur = m_Model->GetSecondsDurationForEID(eventId);
return action->eventId == eventId && (action->flags & ActionFlags::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 ActionDescription *action, const rdcstr &) {
double dur = m_Model->GetSecondsDurationForEID(eventId);
return action->eventId == eventId && (action->flags & ActionFlags::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 ActionDescription *action, const rdcstr &) {
double dur = m_Model->GetSecondsDurationForEID(eventId);
return action->eventId == eventId && (action->flags & ActionFlags::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 ActionDescription *action, const rdcstr &) {
double dur = m_Model->GetSecondsDurationForEID(eventId);
return action->eventId == eventId && (action->flags & ActionFlags::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(
<h3>$parent</h3>
<br />
<table>
<tr><td>$parent(marker)<br />
$childOf(marker)</td>
<td>- passes if an event is contained somewhere under a marker of the given name.</td></tr>
</table>
<p>
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
nesting level.
</p>
)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 ActionDescription *action, const rdcstr &) {
while(action->parent)
{
if(QString(action->parent->customName).contains(markerName, Qt::CaseInsensitive))
return true;
action = action->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 ActionDescription *action,
const rdcstr &name) {
return innerFilter(ctx, n, p, eid, chunk, action, 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 ActionDescription *action, const rdcstr &name) {
return EvaluateFilterSet(*ctx, subFilters, false, eid, chunk, action, 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, QColor foreCol,
QVector<FilterExpression> &exprs, QList<QTextEdit::ExtraSelection> &sels)
{
QTextEdit::ExtraSelection sel;
sel.cursor = cursor;
bool hasMust = false;
for(const FilterExpression &f : exprs)
hasMust |= (f.matchType == MatchType::MustMatch);
for(FilterExpression &f : exprs)
{
bool ignored = false;
if(hasMust && f.matchType == MatchType::Normal)
ignored = true;
// we use 6 colours around the wheel to get reasonable distinction. Note that this is not
// critical information and selecting each entry will underline it in the phrase, since we want
// to be colour blind friendly where possible.
// we avoid the 60 degrees near red (which is at 0) to avoid confusion with errors.
QColor col = QColor::fromHslF((float(idx) / 6.0f) * (300.0f / 360.0f) + (0.5f / 6.0f),
ignored ? 0.0f : 1.0f,
qBound(0.05, 0.2 * 0.5 + 0.8 * backCol.lightnessF(), 0.95));
idx = (idx + 1) % 6;
f.col = col;
sel.cursor.setPosition(f.position, QTextCursor::MoveAnchor);
sel.cursor.setPosition(f.position + f.length, QTextCursor::KeepAnchor);
sel.format.setBackground(QBrush(col));
sel.format.setForeground(QBrush(contrastingColor(col, foreCol)));
sel.format.setFontStrikeOut(ignored);
sels.push_back(sel);
if(!ignored)
AddFilterSelections(cursor, idx, backCol, foreCol, 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_ACTION, 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_ACTION, 60);
ui->events->header()->resizeSection(COL_NAME, 200);
ui->events->header()->resizeSection(COL_DURATION, 80);
ui->events->header()->hideSection(COL_ACTION);
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);
ui->find->setChecked(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, 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->setDropDown();
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->filterExpression, &RDTextEdit::dropDownClicked,
[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->timeActions->setEnabled(true);
ui->bookmark->setEnabled(true);
ui->exportActions->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->timeActions->setEnabled(false);
ui->bookmark->setEnabled(false);
ui->exportActions->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_timeActions_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 &current, 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 ActionDescription *action = m_Ctx.GetAction(selectedEID);
if(action && action->IsFakeMarker())
action = &action->children.back();
ui->stepPrev->setEnabled(action && action->previous);
ui->stepNext->setEnabled(action && action->next);
// special case for the first action in the frame
if(selectedEID == 0)
ui->stepNext->setEnabled(true);
// special case for the first 'virtual' action at EID 0
if(m_Ctx.GetFirstAction() && selectedEID == m_Ctx.GetFirstAction()->eventId)
ui->stepPrev->setEnabled(true);
highlightBookmarks();
}
void EventBrowser::on_HideFind()
{
ui->findStrip->hide();
ui->find->setChecked(false);
m_Model->SetFindText(QString());
ui->findEvent->setPalette(palette());
}
void EventBrowser::findHighlight_timeout()
{
FindNext(true);
}
void EventBrowser::FindNext(bool forward)
{
m_Model->SetFindText(ui->findEvent->text());
// get the first result in this direction
QModelIndex firstResult = m_Model->Find(forward, m_Model->GetCurrentEID());
bool forceNone = false;
// we got a result but it might be filtered out. This also means that for EID selects we'll jump
// to the next visible EID
if(firstResult.isValid())
{
QModelIndex result = firstResult;
QModelIndex mappedResult = m_FilterModel->mapFromSource(result);
// loop if we have a valid result but it's invalid when mapped (i.e. filtered out)
while(result.isValid() && !mappedResult.isValid())
{
// get the next result in this direction. If we're doing an EID search, step EID-wise
if(m_Model->FindEIDSearch())
result = m_Model->GetIndexForEID(result.data(ROLE_SELECTED_EID).toUInt() + 1);
else
result = m_Model->Find(forward, result);
// if we've looped around or somehow get an invalid result, break
if(!result.isValid() || result == firstResult)
break;
// map it
mappedResult = m_FilterModel->mapFromSource(result);
// the loop condition will break if we got a mapped result
}
// if we got a valid visible result, select it
if(mappedResult.isValid())
{
SelectEvent(m_FilterModel->mapFromSource(result));
}
else
{
// otherwise tell the results below that there are no visible results, even though there might
// be some that are filtered out
forceNone = true;
}
}
updateFindResultsAvailable(forceNone);
}
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 *saveFilter = new QToolButton(this);
QMenu *importExportMenu = new QMenu(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 action names"));
m_FilterSettings.UseCustom->setToolTip(
tr("Show custom action names for e.g. indirect draws where the explicit parameters are not "
"as directly useful."));
m_FilterSettings.UseCustom->setCheckable(true);
QObject::connect(m_FilterSettings.UseCustom, &QCheckBox::toggled,
[this](bool on) { m_Model->SetUseCustomActionNames(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();
m_FilterSettings.Filter->setDropDown();
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(m_FilterSettings.Filter, &RDTextEdit::dropDownClicked,
[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 = [&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, [&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, [&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, [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();
});
saveFilter->setAutoRaise(true);
saveFilter->setIcon(Icons::save());
saveFilter->setText(tr("Save"));
saveFilter->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
saveFilter->setToolTip(tr("Save current filter"));
saveFilter->setPopupMode(QToolButton::MenuButtonPopup);
QAction *importAction = new QAction(this);
importAction->setText(tr("Import filter set"));
QObject::connect(importAction, &QAction::triggered, [this]() {
QString filename = RDDialog::getOpenFileName(this, tr("Import filter set"), QString(),
tr("JSON Files (*.json)"));
if(!filename.isEmpty())
{
QFile f(filename);
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
QVariantMap filters;
bool success = LoadFromJSON(filters, f, "rdocFilterSet", 1);
EventBrowserPersistentStorage storedFilters;
storedFilters.load(filters);
success &= !storedFilters.SavedFilters.isEmpty();
if(success)
{
QString prompt =
tr("Are you sure you want to overwrite the current set of filters with these?\n\n");
for(int i = 0; i < storedFilters.SavedFilters.count(); i++)
prompt += QFormatStr("%1: %2\n")
.arg(storedFilters.SavedFilters[i].first)
.arg(storedFilters.SavedFilters[i].second);
QMessageBox::StandardButton res =
RDDialog::question(this, tr("Confirm importing filter set"), prompt,
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if(res == QMessageBox::Yes)
{
persistantStorage.SavedFilters = storedFilters.SavedFilters;
}
}
else
{
RDDialog::critical(this, tr("Error importing filter set"),
tr("Couldn't parse filter set %1.").arg(filename));
}
}
else
{
RDDialog::critical(this, tr("Error importing filter set"),
tr("Couldn't open path %1.").arg(filename));
}
}
});
QAction *exportAction = new QAction(this);
exportAction->setText(tr("Export filter set"));
QObject::connect(exportAction, &QAction::triggered, [this]() {
QString filename = RDDialog::getSaveFileName(this, tr("Export filter set"), QString(),
tr("JSON Files (*.json)"));
if(!filename.isEmpty())
{
QFile f(filename);
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
QVariant v;
persistantStorage.save(v);
QVariantMap filters = v.toMap();
SaveToJSON(filters, f, "rdocFilterSet", 1);
}
else
{
RDDialog::critical(this, tr("Error exporting filter set"),
tr("Couldn't open path %1.").arg(filename));
}
}
});
importExportMenu->addAction(importAction);
importExportMenu->addAction(exportAction);
// disable export if there are no filters to export
QObject::connect(importExportMenu, &QMenu::aboutToShow, [exportAction]() {
exportAction->setEnabled(!persistantStorage.SavedFilters.isEmpty());
});
saveFilter->setMenu(importExportMenu);
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->setHtml(tr(R"EOD(
<h3>General filter help</h3>
<p>
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:
<p>
<pre> Draw Clear Copy</pre>
<p>
would match each string against event names, and include any event that matches
<code>Draw</code> OR <code>Clear</code> OR <code>Copy</code>.
</p>
<p>
You can also exclude matches with - such as:
</p>
<pre> Draw Clear Copy -Depth</pre>
<p>
which will match as in the first example, except exclude any events that match
<code>Depth<code>.
</p>
<p>
You can also require matches, which overrides any optional matches:
</p>
<pre> +Draw +Indexed -Instanced</pre>
<p>
which will match only events which match <code>Draw</code> and match <code>Indexed</code> but don't
match <code>Instanced</code>. 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.
</p>
<p>
More complex expressions can be built with nesting:
</p>
<pre> +Draw +(Indexed Instanced) -Indirect</pre>
<p>
Would match only events matching <code>Draw</code> which also match at least one of
<code>Indexed</code> or <code>Instanced</code> but not <code>Indirect</code>.
</p>
<p>
Finally you can use filter functions for more advanced matching than just
strings. These are documented on the left here, but for example
</p>
<pre> $action(numIndices > 1000) Indexed</pre>
<p>
will include any action that matches "Indexed" as a plain string match, OR
is an action which renders more than 1000 indices.
</p>
)EOD").trimmed());
}
else if(f == lit("Literal String"))
{
m_FilterSettings.FuncDocs->setHtml(tr(R"EOD(
<h3>Literal String</h3>
<br />
<table>
<tr><td>"Literal string"<br />
literal_string</td>
<td>- passes if the event's name contains a literal substring anywhere in its name</td></tr>
</table>
<p>
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 <code>$param()</code> function.
</p>
)EOD").trimmed());
}
else
{
f = f.mid(1, f.size() - 3);
m_FilterSettings.FuncDocs->setHtml(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(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),
m_FilterSettings.Filter->palette().color(QPalette::Text), 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->UseCustomActionNames());
// 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 &notes)
{
// 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(false);
}
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();
}
else if(e->key() == Qt::Key_Down)
{
ShowSavedFilterCompleter(ui->filterExpression);
}
else
{
events_keyPress(e);
}
}
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())
{
FindNext(event->modifiers() & Qt::ShiftModifier ? false : true);
}
event->accept();
}
}
void EventBrowser::on_findNext_clicked()
{
FindNext(true);
}
void EventBrowser::on_findPrev_clicked()
{
FindNext(false);
}
void EventBrowser::on_stepNext_clicked()
{
if(!m_Ctx.IsCaptureLoaded() || !ui->stepNext->isEnabled())
return;
const ActionDescription *action = m_Ctx.CurAction();
if(action)
action = action->next;
// special case for the first 'virtual' action at EID 0
if(m_Ctx.CurEvent() == 0)
action = m_Ctx.GetFirstAction();
while(action)
{
// try to select the next event. If successful, stop
if(SelectEvent(action->eventId))
return;
// if it failed, possibly the next action is filtered out. Step along the list until we find one
// which isn't
action = action->next;
}
}
void EventBrowser::on_stepPrev_clicked()
{
if(!m_Ctx.IsCaptureLoaded() || !ui->stepPrev->isEnabled())
return;
const ActionDescription *action = m_Ctx.CurAction();
if(action)
action = action->previous;
while(action)
{
// try to select the previous event. If successful, stop
if(SelectEvent(action->eventId))
return;
// if it failed, possibly the previous action is filtered out. Step along the list until we find
// one which isn't
action = action->previous;
}
// special case for the first 'virtual' action at EID 0
SelectEvent(0);
}
void EventBrowser::on_exportActions_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 | Action #").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++)
ExportAction(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::ExportAction(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 actionIdx = idx.model()->sibling(idx.row(), COL_ACTION, idx);
QString line = QFormatStr("%1 | %2 | %3")
.arg(eidIdx.data(Qt::DisplayRole).toString(), -5)
.arg(nameString, -maxNameLength)
.arg(actionIdx.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++)
{
ExportAction(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)
{
FindNext(event->modifiers() & Qt::ShiftModifier ? false : true);
}
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)
{
ui->find->setChecked(true);
ui->findEvent->setFocus();
event->accept();
}
else if(event->key() == Qt::Key_B)
{
on_bookmark_clicked();
event->accept();
}
else if(event->key() == Qt::Key_T)
{
on_timeActions_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(bool forceNoResults)
{
if(!forceNoResults && (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 ActionDescription *action = GetActionForEID(eid);
if(action)
{
for(const APIEvent &ev : action->events)
{
if(ev.eventId == eid)
return ev;
}
}
return APIEvent();
}
const ActionDescription *EventBrowser::GetActionForEID(uint32_t eid)
{
return m_Model->GetActionForEID(eid);
}
rdcstr EventBrowser::GetEventName(uint32_t eid)
{
return m_Model->GetEventName(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::SetUseCustomActionNames(bool use)
{
m_Model->SetUseCustomActionNames(use);
}
void EventBrowser::SetEmptyRegionsVisible(bool show)
{
m_FilterModel->SetEmptyRegionsVisible(show);
}