mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-07-20 06:27:02 +00:00
f8cf943c42
* 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.
5413 lines
170 KiB
C++
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> ×)
|
|
{
|
|
// 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 ¶ms)
|
|
{
|
|
QList<Token> tokens = tokenise(params);
|
|
|
|
if(tokens.size() <= 1)
|
|
return {"EID"};
|
|
|
|
return {};
|
|
}
|
|
|
|
IEventBrowser::EventFilterCallback filterFunction_event(QString name, QString parameters,
|
|
ParseTrace &trace)
|
|
{
|
|
// $event(...) => filters on any event property (at the moment only EID)
|
|
QList<Token> tokens = tokenise(parameters);
|
|
|
|
static const QStringList operators = {
|
|
lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="),
|
|
};
|
|
|
|
if(tokens.size() < 1)
|
|
{
|
|
trace.setError(tr("Filter parameters required", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
if(tokens[0].text.toLower() != lit("eid") && tokens[0].text.toLower() != lit("eventid"))
|
|
{
|
|
trace.position = tokens[0].position;
|
|
trace.length = tokens[0].length;
|
|
trace.setError(tr("Unrecognised event property", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
int operatorIdx = operators.indexOf(tokens[1].text);
|
|
|
|
if(tokens[1].text == lit("="))
|
|
operatorIdx = 0;
|
|
|
|
if(tokens.size() != 3 || operatorIdx < 0 || operatorIdx >= operators.size())
|
|
{
|
|
trace.setError(tr("Invalid expression, expected single comparison with operators: %3",
|
|
"EventFilterModel")
|
|
.arg(operators.join(lit(", "))));
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = false;
|
|
uint32_t eid = tokens[2].text.toUInt(&ok, 0);
|
|
|
|
if(!ok)
|
|
{
|
|
trace.position = tokens[2].position;
|
|
trace.length = tokens[2].length;
|
|
trace.setError(tr("Invalid value, expected integer", "EventFilterModel"));
|
|
return NULL;
|
|
}
|
|
|
|
switch(operatorIdx)
|
|
{
|
|
case 0:
|
|
return [eid](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
|
|
const SDChunk *, const 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 ¶ms)
|
|
{
|
|
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 ¶ms)
|
|
{
|
|
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 ¤t, const QModelIndex &previous)
|
|
{
|
|
if(!current.isValid())
|
|
return;
|
|
|
|
uint32_t selectedEID = GetSelectedEID(current);
|
|
uint32_t effectiveEID = GetEffectiveEID(current);
|
|
|
|
if(selectedEID == m_Ctx.CurSelectedEvent() && effectiveEID == m_Ctx.CurEvent())
|
|
return;
|
|
|
|
m_Ctx.SetEventID({this}, selectedEID, effectiveEID);
|
|
|
|
m_Model->RefreshCache();
|
|
m_Breadcrumbs->OnEventChanged(effectiveEID);
|
|
|
|
const 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 ¬es)
|
|
{
|
|
// sort by match type
|
|
std::sort(exprs.begin(), exprs.end(), [](const FilterExpression &a, const FilterExpression &b) {
|
|
return a.matchType < b.matchType;
|
|
});
|
|
|
|
bool hasMust = false;
|
|
for(const FilterExpression &f : exprs)
|
|
hasMust |= (f.matchType == MatchType::MustMatch);
|
|
|
|
if(hasMust)
|
|
{
|
|
FilterExpression must;
|
|
|
|
QString ignored;
|
|
for(const FilterExpression &f : exprs)
|
|
{
|
|
if(f.matchType == MatchType::MustMatch)
|
|
{
|
|
must = f;
|
|
}
|
|
else if(f.matchType == MatchType::Normal)
|
|
{
|
|
if(!ignored.isEmpty())
|
|
ignored += lit(", ");
|
|
ignored += f.printName();
|
|
}
|
|
}
|
|
|
|
if(!ignored.isEmpty())
|
|
{
|
|
notes +=
|
|
tr("NOTE: The terms %1 are ignored because must-match terms like <span>+%2</span> take "
|
|
"precedence.\n"
|
|
"There is no sorting amongst matches based on optional keywords, so consider making "
|
|
"them <span>+required</span> to require all of them, or nesting in "
|
|
"<span>+( .. )</span> to require at least one of them.\n")
|
|
.arg(ignored)
|
|
.arg(must.printName());
|
|
}
|
|
}
|
|
|
|
bool first = true;
|
|
for(const FilterExpression &f : exprs)
|
|
{
|
|
QString explanation;
|
|
|
|
if(f.matchType == MatchType::MustMatch)
|
|
{
|
|
explanation = tr("Must be that: ");
|
|
}
|
|
else if(f.matchType == MatchType::CantMatch)
|
|
{
|
|
explanation = tr("Can't be that: ");
|
|
}
|
|
else
|
|
{
|
|
// omit any normal matches if we have a must, they are pointless and we warn the user in the
|
|
// notes
|
|
if(hasMust)
|
|
continue;
|
|
|
|
if(!first)
|
|
explanation = tr("Or: ");
|
|
|
|
first = false;
|
|
}
|
|
|
|
if(!f.function)
|
|
{
|
|
explanation += tr("Name matches '%1'").arg(f.name);
|
|
}
|
|
else if(f.name == lit("$any$"))
|
|
{
|
|
explanation += tr("Any of...");
|
|
}
|
|
else
|
|
{
|
|
explanation += tr("Function %1 passes").arg(f.printName());
|
|
}
|
|
|
|
RDTreeWidgetItem *item =
|
|
new RDTreeWidgetItem({explanation, f.function ? f.name : tr("Literal String")});
|
|
item->setBackgroundColor(f.col);
|
|
|
|
item->setTag(QSize(f.position, f.length));
|
|
|
|
root->addChild(item);
|
|
|
|
AddFilterExplanations(item, f.exprs, notes);
|
|
}
|
|
}
|
|
|
|
void EventBrowser::on_findEvent_textEdited(const QString &arg1)
|
|
{
|
|
if(arg1.isEmpty())
|
|
{
|
|
m_FindHighlight->stop();
|
|
|
|
m_Model->SetFindText(QString());
|
|
updateFindResultsAvailable(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);
|
|
}
|