diff --git a/qrenderdoc/Code/QRDUtils.h b/qrenderdoc/Code/QRDUtils.h index df12b1d74..7c5289b54 100644 --- a/qrenderdoc/Code/QRDUtils.h +++ b/qrenderdoc/Code/QRDUtils.h @@ -126,6 +126,7 @@ void CombineUsageEvents( class RDTreeWidgetItem; +QVariant SDObject2Variant(const SDObject *obj); void addStructuredChildren(RDTreeWidgetItem *parent, const SDObject &parentObj); struct PointerTypeRegistry diff --git a/qrenderdoc/Windows/Dialogs/ConfigEditor.cpp b/qrenderdoc/Windows/Dialogs/ConfigEditor.cpp index 772079b5e..9b305822b 100644 --- a/qrenderdoc/Windows/Dialogs/ConfigEditor.cpp +++ b/qrenderdoc/Windows/Dialogs/ConfigEditor.cpp @@ -331,8 +331,7 @@ public: m_Text = text; m_KeyText = m_Text; m_KeyText.replace(QLatin1Char('.'), QLatin1Char('_')); - emit beginResetModel(); - emit endResetModel(); + invalidateFilter(); } protected: diff --git a/qrenderdoc/Windows/EventBrowser.cpp b/qrenderdoc/Windows/EventBrowser.cpp index 0f1e5d8c5..a7bee0455 100644 --- a/qrenderdoc/Windows/EventBrowser.cpp +++ b/qrenderdoc/Windows/EventBrowser.cpp @@ -860,10 +860,124 @@ private: friend struct EventFilterModel; }; +using EventFilterCallback = + std::function; + +struct EventFilter +{ + enum MatchType + { + MustMatch, + Normal, + CantMatch + }; + + EventFilter(EventFilterCallback c) : callback(c), type(Normal) {} + EventFilter(EventFilterCallback c, MatchType t) : callback(c), type(t) {} + EventFilterCallback callback; + MatchType type; +}; + +static QMap DrawFlagsLookup; + +bool EvaluateFilterSet(const rdcarray &filters, bool all, uint32_t eid, + const SDChunk *chunk, const DrawcallDescription *draw, QString name) +{ + if(filters.empty()) + return true; + + bool accept = false; + + bool anyNormal = false; + for(const EventFilter &filter : filters) + anyNormal |= (filter.type == EventFilter::Normal); + + // if there aren't any normal filters, they're all CantMatch or MustMatch. In this case we 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(!anyNormal) + accept = true; + + // if any top-level filter matches, we match + for(const EventFilter &filter : filters) + { + // 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 == EventFilter::Normal) + continue; + + bool match = filter.callback(eid, chunk, draw, name); + + // in normal mode, if it matches it should be included (unless a subsequent filter excludes + // it) + if(filter.type == EventFilter::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 == EventFilter::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 == EventFilter::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 EventFilterModel : public QSortFilterProxyModel { - EventFilterModel(ICaptureContext &ctx) : m_Ctx(ctx) {} - void ParseExpression(QString expr) { qInfo() << expr; } +public: + EventFilterModel(ICaptureContext &ctx) : m_Ctx(ctx) + { + // default filter, include draws that aren't pop markers + m_Filters.push_back( + EventFilter([](uint32_t, const SDChunk *, const DrawcallDescription *draw, const rdcstr &) { + return (draw && !(draw->flags & DrawFlags::PopMarker)); + })); + } + + void ResetCache() { m_VisibleCache.clear(); } + QString ParseExpression(QString expr) + { + m_VisibleCache.clear(); + m_Filters.clear(); + QString ret = ParseExpressionToFilters(expr, m_Filters); + if(!ret.isEmpty()) + m_Filters.clear(); + invalidateFilter(); + return ret; + } + protected: virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { @@ -888,22 +1002,677 @@ protected: QModelIndex source_idx = sourceModel()->index(source_row, 0, source_parent); - const DrawcallDescription *draw = - (const DrawcallDescription *)(sourceModel()->data(source_idx, ROLE_EXACT_DRAWCALL).toULongLong()); + uint32_t eid = sourceModel()->data(source_idx, ROLE_SELECTED_EID).toUInt(); + + m_VisibleCache.resize_for_index(eid); + if(m_VisibleCache[eid] != 0) + return m_VisibleCache[eid] > 0; + const SDChunk *chunk = (const SDChunk *)(sourceModel()->data(source_idx, ROLE_CHUNK).toULongLong()); - + const DrawcallDescription *draw = + (const DrawcallDescription *)(sourceModel()->data(source_idx, ROLE_EXACT_DRAWCALL).toULongLong()); QString name = source_idx.data(Qt::DisplayRole).toString(); - // default filter - if(draw && !(draw->flags & DrawFlags::PopMarker)) - return true; - - return false; + m_VisibleCache[eid] = EvaluateFilterSet(m_Filters, false, eid, chunk, draw, name) ? 1 : -1; + 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 m_VisibleCache; + + rdcarray m_Filters; + + EventFilterCallback MakeLiteralMatcher(QString string) + { + QString matchString = string.toLower(); + return [matchString](uint32_t, const SDChunk *, const DrawcallDescription *, const rdcstr &name) { + return QString(name).toLower().contains(matchString); + }; + } + + EventFilterCallback MakeFunctionMatcher(QString name, QString parameters, QString &errors) + { + // builtins + if(name == lit("any")) + { + // $any(...) => returns true if any of the nested subexpressions are true (with exclusions + // treated as normal). + rdcarray filters; + errors = ParseExpressionToFilters(parameters, filters); + + if(errors.isEmpty()) + { + if(filters.isEmpty()) + { + errors = tr("No filters provided to $any()", "EventFilterModel"); + return NULL; + } + + return [filters](uint32_t eid, const SDChunk *chunk, const DrawcallDescription *draw, + const rdcstr &name) { + return EvaluateFilterSet(filters, false, eid, chunk, draw, name); + }; + } + + return NULL; + } + else if(name == lit("all")) + { + // $all(...) => returns true only if all the nested subexpressions are true + rdcarray filters; + errors = ParseExpressionToFilters(parameters, filters); + + if(errors.isEmpty()) + { + if(filters.isEmpty()) + { + errors = tr("No filters provided to $all()", "EventFilterModel"); + return NULL; + } + + return [filters](uint32_t eid, const SDChunk *chunk, const DrawcallDescription *draw, + const rdcstr &name) { + return EvaluateFilterSet(filters, true, eid, chunk, draw, name); + }; + } + + return NULL; + } + else if(name == lit("param")) + { + // $param() => check for a named parameter having a particular value + int idx = parameters.indexOf(QLatin1Char(':')); + + if(idx < 0) + { + errors = tr("Parameter to to $param() should be name: value", "EventFilterModel"); + return NULL; + } + + QString paramName = parameters.mid(0, idx).trimmed(); + QString paramValue = parameters.mid(idx + 1).trimmed(); + + ICaptureContext *ctx = &m_Ctx; + + return [paramName, paramValue, ctx](uint32_t, const SDChunk *chunk, + const DrawcallDescription *, const rdcstr &) { + const SDObject *o = FindChildRecursively(chunk, paramName); + + if(!o) + return false; + + if(o->IsArray()) + { + for(const SDObject *c : *o) + { + if(RichResourceTextFormat(*ctx, SDObject2Variant(c)).contains(paramValue, Qt::CaseInsensitive)) + return true; + } + + return false; + } + else + { + return RichResourceTextFormat(*ctx, SDObject2Variant(o)) + .contains(paramValue, Qt::CaseInsensitive); + } + + }; + } + else if(name == lit("event")) + { + // $event(...) => filters on any event property (at the moment only EID) + QStringList params = parameters.split(QLatin1Char(' '), QString::SkipEmptyParts); + + static const QStringList operators = { + lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="), + }; + + if(params.size() < 1 || + (params[0].toLower() != lit("eid") && params[0].toLower() != lit("eventid"))) + { + errors = tr("Unrecognised $event() property filter expression '%1'", "EventFilterModel") + .arg(parameters); + return NULL; + } + + if(params.size() != 3 || operators.indexOf(params[1]) < 0) + { + errors = tr("Invalid $event() %1 filter expression '%2', expected " + "$event(%1 operator value) with operators %3", + "EventFilterModel") + .arg(params[0]) + .arg(parameters) + .arg(operators.join(lit(", "))); + return NULL; + } + + bool ok = false; + uint32_t eid = params[2].toUInt(&ok, 0); + + if(!ok) + { + errors = + tr("Invalid $event() %1 filter expression '%2', invalid value %3", "EventFilterModel") + .arg(params[0]) + .arg(parameters) + .arg(params[2]); + return NULL; + } + + switch(operators.indexOf(params[1])) + { + case 0: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId == eid; }; + case 1: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId != eid; }; + case 2: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId < eid; }; + case 3: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId > eid; }; + case 4: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId <= eid; }; + case 5: + return [eid](uint32_t eventId, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return eventId >= eid; }; + default: + errors = tr("Internal error, unexpected operator %1", "EventFilterModel").arg(params[1]); + return NULL; + } + } + else if(name == lit("draw")) + { + // $draw(...) => returns true only for draws, optionally with a particular property filter + QStringList params = parameters.split(QLatin1Char(' '), QString::SkipEmptyParts); + + // no parameters, just return if it's a draw + if(params.isEmpty()) + return [](uint32_t, const SDChunk *, const DrawcallDescription *draw, const rdcstr &) { + return draw != NULL; + }; + + // we upcast to int64_t so we can compare both unsigned and signed values without losing any + // precision (we don't have any uint64_ts to compare) + using PropGetter = int64_t (*)(const DrawcallDescription *); + + struct NamedProp + { + const char *name; + PropGetter getter; + }; + +#define NAMED_PROP(name, access) \ + name, [](const DrawcallDescription *draw) -> int64_t { return access; } + + static const NamedProp namedProps[] = { + NAMED_PROP("eventid", draw->eventId), NAMED_PROP("eid", draw->eventId), + NAMED_PROP("parent", draw->parent ? draw->parent->eventId : -12341234), + NAMED_PROP("drawcallid", draw->drawcallId), NAMED_PROP("numindices", draw->numIndices), + NAMED_PROP("numindexes", draw->numIndices), NAMED_PROP("numvertices", draw->numIndices), + NAMED_PROP("numvertexes", draw->numIndices), NAMED_PROP("indexcount", draw->numIndices), + NAMED_PROP("vertexcount", draw->numIndices), NAMED_PROP("numinstances", draw->numInstances), + NAMED_PROP("instancecount", draw->numInstances), + NAMED_PROP("basevertex", draw->baseVertex), NAMED_PROP("indexoffset", draw->indexOffset), + NAMED_PROP("vertexoffset", draw->vertexOffset), + NAMED_PROP("instanceoffset", draw->instanceOffset), + NAMED_PROP("dispatchx", draw->dispatchDimension[0]), + NAMED_PROP("dispatchy", draw->dispatchDimension[1]), + NAMED_PROP("dispatchz", draw->dispatchDimension[2]), + NAMED_PROP("dispatchsize", draw->dispatchDimension[0] * draw->dispatchDimension[1] * + draw->dispatchDimension[2]), + }; + + QByteArray prop = params[0].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(">="), + }; + + if(params.size() != 3 || operators.indexOf(params[1]) < 0) + { + errors = tr("Invalid $draw() %1 filter expression '%2', expected " + "$draw(%1 operator value) with operators %3", + "EventFilterModel") + .arg(params[0]) + .arg(parameters) + .arg(operators.join(lit(", "))); + return NULL; + } + + bool ok = false; + int64_t value = params[2].toLongLong(&ok, 0); + + if(!ok) + { + errors = + tr("Invalid $draw() %1 filter expression '%2', invalid value %3", "EventFilterModel") + .arg(params[0]) + .arg(parameters) + .arg(params[2]); + return NULL; + } + + switch(operators.indexOf(params[1])) + { + case 0: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) == value; }; + case 1: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) != value; }; + case 2: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) < value; }; + case 3: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) > value; }; + case 4: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) <= value; }; + case 5: + return [propGetter, value](uint32_t, const SDChunk *, const DrawcallDescription *draw, + const rdcstr &) { return draw && propGetter(draw) >= value; }; + default: + errors = tr("Internal error, unexpected operator %1", "EventFilterModel").arg(params[1]); + return NULL; + } + } + else if(params[0] == lit("flags")) + { + if(params.size() < 3 || params[1] != lit("&")) + { + errors = tr("Invalid $draw() flags filter expression '%1', expected " + "$draw(flags & SomeFlag|OtherFlag)", + "EventFilterModel") + .arg(parameters); + 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 < params.count(); i++) + flagStrings.append(params[i].split(QLatin1Char('|'), QString::KeepEmptyParts)); + + // if we have an empty string in the list somewhere that means the | list was broken + if(flagStrings.contains(QString())) + { + errors = + tr("Invalid $draw() flags filter expression '%1'", "EventFilterModel").arg(parameters); + return NULL; + } + + if(DrawFlagsLookup.empty()) + { + for(uint32_t i = 0; i <= 31; i++) + { + DrawFlags flag = DrawFlags(1U << i); + + // bit of a hack, see if it's a valid flag by stringising and seeing if it contains + // DrawFlags( + QString str = ToQStr(flag); + if(str.contains(lit("DrawFlags("))) + continue; + + DrawFlagsLookup[str] = flag; + } + } + + DrawFlags flags = DrawFlags::NoFlags; + for(const QString &flagString : flagStrings) + { + auto it = DrawFlagsLookup.find(flagString); + if(it == DrawFlagsLookup.end()) + { + errors = tr("Unrecognised draw flag '%1' in to $draw(flags) filter", "EventFilterModel") + .arg(flagString); + return NULL; + } + + flags |= it.value(); + } + + return [flags](uint32_t, const SDChunk *, const DrawcallDescription *draw, const rdcstr &) { + return draw && (draw->flags & flags); + }; + } + else + { + errors = tr("Unrecognised $draw() property filter expression '%1'", "EventFilterModel") + .arg(parameters); + return NULL; + } + } + + errors = tr("Unknown filter function $%1", "EventFilterModel").arg(name); + return NULL; + } + + QString ParseExpressionToFilters(QString expr, rdcarray &filters) + { + // 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. + // + // 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 -> anything else -> literal -> wait until whitespace -> start + // + // We also have two modifiers: + // + // 5) start -> + -> note that next filter is a MustMatch -> start + // 6) start -> - -> note that next filter is a CantMatch -> start + // + // any non-existant edge is a parse error + + QString errorString; + + enum + { + Start, + QuotedExpr, + FunctionExprName, + FunctionExprParams, + Literal, + } state = Start; + + expr = expr.trimmed(); + + EventFilter::MatchType matchType = EventFilter::Normal; + + // temporary string we're building up somewhere + QString s; + + // parenthesis depth + int parenDepth = 0; + // parameters (since s holds the function name) + QString params; + + int pos = 0; + while(pos < expr.length()) + { + if(state == Start) + { + // 1) skip whitespace while in start + if(expr[pos].isSpace()) + { + pos++; + continue; + } + + // 5) and 6) + if(expr[pos] == QLatin1Char('-')) + { + // stay in the Start state, but store match type + matchType = EventFilter::CantMatch; + pos++; + continue; + } + + if(expr[pos] == QLatin1Char('+')) + { + matchType = EventFilter::MustMatch; + pos++; + continue; + } + + // 3.1) move to function expression if we see a $ + if(expr[pos] == QLatin1Char('$')) + { + // we need at minimum 3 more characters for $x() + if(pos + 3 >= expr.length()) + return tr("Parse Error: Invalid function expression %1", "EventFilterModel") + .arg(expr.mid(pos)); + + // consume the $ + state = FunctionExprName; + pos++; + + continue; + } + + // 2.1) move to quoted expression if we see a " + if(expr[pos] == QLatin1Char('"')) + { + state = QuotedExpr; + pos++; + continue; + } + + // 4.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 == QuotedExpr) + { + // 2.2) handle escaping + if(expr[pos] == QLatin1Char('\\')) + { + if(pos == expr.length() - 1) + return tr("Parse Error: 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()) + return tr("Parse Error: Unexpected character %1 after quoted string", + "EventFilterModel") + .arg(expr[pos + 1]); + + filters.push_back(EventFilter(MakeLiteralMatcher(s), matchType)); + + s.clear(); + pos++; + state = Start; + matchType = EventFilter::Normal; + + continue; + } + else + { + // just another character, append and continue + s.append(expr[pos]); + pos++; + continue; + } + } + + if(state == Literal) + { + // 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()) + s.append(expr[pos]); + + filters.push_back(EventFilter(MakeLiteralMatcher(s), matchType)); + + s.clear(); + pos++; + state = Start; + matchType = EventFilter::Normal; + + 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('(')) + { + if(s.isEmpty()) + return tr( + "Parse Error: Filter function with no name before arguments. \ + If this is not a filter function, surround with quotes.", + "EventFilterModel"); + + state = FunctionExprParams; + parenDepth = 1; + pos++; + 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++; + + return tr("Parse Error: Invalid function name '%1', must begin with a letter. \ + If this is not a filter function, surround with quotes.", + "EventFilterModel") + .arg(expr.mid(pos, end - pos)); + } + + // 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()) + return tr("Parse Error: Unexpected character %1 after filter function", + "EventFilterModel") + .arg(expr[pos + 1]); + + QString errors; + EventFilterCallback filter = MakeFunctionMatcher(s, params, errors); + + if(!filter) + { + if(errors.isEmpty()) + return tr("Unknown filter function '$%1'", "EventFilterModel").arg(s); + + return errors; + } + + filters.push_back(EventFilter(filter, matchType)); + + s.clear(); + params.clear(); + + // move back to the start state + state = Start; + matchType = EventFilter::Normal; + } + else + { + params.append(expr[pos]); + } + + pos++; + continue; + } + } + + // 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 tr("Parse Error: Encountered unterminated literal", "EventFilterModel"); + } + else if(state == QuotedExpr) + { + return tr("Parse Error: Unterminated quoted expression: %1", "EventFilterModel").arg(s); + } + else if(state == FunctionExprName) + { + return tr("Parse Error: Encountered end of filter without function parameters: $%1", + "EventFilterModel") + .arg(s); + } + else if(state == FunctionExprParams) + { + return tr("Parse Error: Encountered end of filter in function: $%1(%2", "EventFilterModel") + .arg(s) + .arg(params); + } + + // any - or + should have been consumed by an expression, if we still have it then it was + // dangling + if(matchType == EventFilter::CantMatch) + return tr("Parse Error: Encountered - without expression", "EventFilterModel"); + if(matchType == EventFilter::MustMatch) + return tr("Parse Error: Encountered + without expression", "EventFilterModel"); + + return errorString; + } }; static bool textEditControl(QWidget *sender) @@ -1098,6 +1867,7 @@ 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(); @@ -1126,6 +1896,7 @@ void EventBrowser::OnCaptureClosed() on_HideFindJump(); + 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(); @@ -1267,9 +2038,12 @@ void EventBrowser::filter_apply() ui->events->clearSelection(); ui->events->setCurrentIndex(QModelIndex()); - m_FilterModel->ParseExpression(ui->filterExpression->text()); + QString parseError = m_FilterModel->ParseExpression(ui->filterExpression->text()); ui->events->setCurrentIndex(m_FilterModel->mapFromSource(m_Model->GetIndexForEID(curSelEvent))); + + if(!parseError.isEmpty()) + qInfo() << parseError; } void EventBrowser::on_findEvent_textEdited(const QString &arg1)