diff --git a/src/blackmisc/logmessage.cpp b/src/blackmisc/logmessage.cpp index e289528a8..cb9450dca 100644 --- a/src/blackmisc/logmessage.cpp +++ b/src/blackmisc/logmessage.cpp @@ -67,9 +67,7 @@ namespace BlackMisc void CLogMessage::preformatted(const CStatusMessage &statusMessage) { if (statusMessage.isEmpty()) { return; } // just skip empty messages - CLogMessage msg(statusMessage.getCategories()); - msg.m_severity = statusMessage.getSeverity(); - msg.m_message = statusMessage.getMessage(); + CLogMessage(statusMessage.getCategories()).log(statusMessage.getSeverity(), u"%1") << statusMessage.getMessage(); } void CLogMessage::preformatted(const CStatusMessageList &statusMessages) diff --git a/src/blackmisc/registermetadata.cpp b/src/blackmisc/registermetadata.cpp index 153fa3c26..6700e44f3 100644 --- a/src/blackmisc/registermetadata.cpp +++ b/src/blackmisc/registermetadata.cpp @@ -83,6 +83,7 @@ namespace BlackMisc CRgbColor::registerMetadata(); CStatusMessage::registerMetadata(); CStatusMessageList::registerMetadata(); + CStrongStringView::registerMetadata(); CValueCachePacket::registerMetadata(); CVariant::registerMetadata(); CVariantList::registerMetadata(); diff --git a/src/blackmisc/statusmessage.cpp b/src/blackmisc/statusmessage.cpp index 9a1b10f89..cc8c893d1 100644 --- a/src/blackmisc/statusmessage.cpp +++ b/src/blackmisc/statusmessage.cpp @@ -23,35 +23,37 @@ namespace BlackMisc { namespace Private { - namespace - { - template QString arg(std::index_sequence, const QString &format, const QStringList &args) { return format.arg(args[Is]...); } - QString arg(std::index_sequence<>, const QString &format, const QStringList &) { return format; } - } - - QString arg(const QString &format, const QStringList &args) + QString arg(QStringView format, const QStringList &args) { if (format.isEmpty()) { - return args.join(" "); + return args.join(u' '); } - else + + thread_local QString temp; + temp.resize(0); // unlike clear(), resize(0) doesn't release the capacity if there are no implicitly shared copies + + for (auto it = format.begin(); ; ) { - switch (args.size()) + const auto pc = std::find(it, format.end(), u'%'); + temp.append(&*it, std::distance(it, pc)); + if ((it = pc) == format.end()) { break; } + if (++it == format.end()) { temp += u'%'; break; } + + if (*it == u'%') { temp += u'%'; ++it; continue; } + if (is09(*it)) { - case 0: return arg(std::make_index_sequence<0>(), format, args); - case 1: return arg(std::make_index_sequence<1>(), format, args); - case 2: return arg(std::make_index_sequence<2>(), format, args); - case 3: return arg(std::make_index_sequence<3>(), format, args); - case 4: return arg(std::make_index_sequence<4>(), format, args); - case 5: return arg(std::make_index_sequence<5>(), format, args); - case 6: return arg(std::make_index_sequence<6>(), format, args); - case 7: return arg(std::make_index_sequence<7>(), format, args); - case 8: return arg(std::make_index_sequence<8>(), format, args); - default: qWarning("Too many arguments to BlackMisc::Private::arg"); // intentional fall-through - case 9: return arg(std::make_index_sequence<9>(), format, args); + int n = it->unicode() - u'0'; + Q_ASSERT(n >= 0 && n <= 9); + if (++it != format.end() && is09(*it)) { n = n * 10 + it->unicode() - u'0'; ++it; } + Q_ASSERT(n >= 0 && n <= 99); + if (n <= args.size()) { temp += args[n - 1]; } else { temp += u'%' % QString::number(n); } } + else { temp += u'%'; } } + QString result = temp; + result.squeeze(); // release unused capacity and implicitly detach so temp keeps its capacity for next time + return result; } } @@ -235,13 +237,13 @@ namespace BlackMisc void CStatusMessage::prependMessage(const QString &msg) { if (msg.isEmpty()) { return; } - m_message = msg + m_message; + m_message = QString(msg % m_message.view()); } void CStatusMessage::appendMessage(const QString &msg) { if (msg.isEmpty()) { return; } - m_message += msg; + m_message = QString(m_message.view() % msg); } void CStatusMessage::markAsHandledBy(const QObject *object) const diff --git a/src/blackmisc/statusmessage.h b/src/blackmisc/statusmessage.h index ec34027da..1a1b6da00 100644 --- a/src/blackmisc/statusmessage.h +++ b/src/blackmisc/statusmessage.h @@ -27,8 +27,8 @@ namespace BlackMisc namespace Private { - //! Like QString::arg() but accepts a QStringList of args. - BLACKMISC_EXPORT QString arg(const QString &format, const QStringList &args); + //! Like QString::arg() but accepts a QVector of args. + BLACKMISC_EXPORT QString arg(QStringView format, const QStringList &args); } /*! @@ -42,6 +42,81 @@ namespace BlackMisc SeverityError }; + /*! + * Special-purpose string class used by CMessageBase. + * + * Wraps a QStringView that can be constructed from a UTF-16 string literal or from a QString. + * If constructed from a QString, the QString is stored to prevent a dangling pointer. + */ + class CStrongStringView : + public Mixin::MetaType, + public Mixin::EqualsByCompare, + public Mixin::LessThanByCompare, + public Mixin::DBusOperators, + public Mixin::JsonOperators + { + public: + //! Default constructor. + CStrongStringView() = default; + + //! Construct from a UTF-16 character array. + template + CStrongStringView(const char16_t (&string)[N]) : m_view(string) {} + + //! Construct from a QString. + CStrongStringView(const QString &string) : m_string(string), m_view(m_string) {} + + //! Deleted constructor. + CStrongStringView(const char *) = delete; + + //! Copy constructor. + CStrongStringView(const CStrongStringView &other) { *this = other; } + + //! Copy assignment operator. + CStrongStringView &operator =(const CStrongStringView &other) + { + if (other.isOwning()) { m_view = m_string = other.m_string; } else { m_view = other.m_view; m_string.clear(); } + return *this; + } + + //! Destructor. + ~CStrongStringView() = default; + + //! String is empty. + bool isEmpty() const { return view().isEmpty(); } + + //! Does it own its string data? + bool isOwning() const { return !m_string.isNull(); } // important distinction between isNull and isEmpty + + //! Return as a QStringView. + QStringView view() const { return m_view; } + + //! Return a copy as a QString. + QString toQString(bool i18n) const { Q_UNUSED(i18n); return isOwning() ? m_string : m_view.toString(); } + + //! Compare two strings. + friend int compare(const CStrongStringView &a, const CStrongStringView &b) { return a.m_view.compare(b.m_view); } + + //! Hash value. + friend uint qHash(const CStrongStringView &obj, uint seed = 0) { return ::qHash(obj.m_view, seed); } + + //! DBus marshalling. + //! @{ + void marshallToDbus(QDBusArgument &arg) const { if (isOwning()) { arg << m_string; } else { arg << m_view.toString(); } } + void unmarshallFromDbus(const QDBusArgument &arg) { QString s; arg >> s; *this = s; } + //! @} + + //! JSON conversion. + //! @{ + QJsonObject toJson() const { QJsonObject json; json.insert(QStringLiteral("value"), m_view.toString()); return json; } + void convertFromJson(const QJsonObject &json) { *this = json.value(QLatin1String("value")).toString(); } + //! @} + + private: + QString m_string; + QStringView m_view; + }; + /*! * Base class for CStatusMessage and CLogMessage. */ @@ -65,34 +140,83 @@ namespace BlackMisc CMessageBase(const CLogCategoryList &categories, const CLogCategoryList &extra) : CMessageBase(categories) { this->addIfNotExisting(extra); } //! Set the severity and format string. + //! @{ + template + Derived &log(StatusSeverity s, const char16_t (&m)[N]) { m_message = m; m_severity = s; return derived(); } Derived &log(StatusSeverity s, const QString &m) { m_message = m; m_severity = s; return derived(); } + //! @} //! Set the severity to debug. - Derived &debug() { return log(SeverityDebug, ""); } + Derived &debug() { return log(SeverityDebug, QString()); } //! Set the severity to debug, providing a format string. + //! @{ + template + Derived &debug(const char16_t (&format)[N]) { return log(SeverityDebug, format); } Derived &debug(const QString &format) { return log(SeverityDebug, format); } + //! @} //! Set the severity to info, providing a format string. + //! @{ + template + Derived &info(const char16_t (&format)[N]) { return log(SeverityInfo, format); } Derived &info(const QString &format) { return log(SeverityInfo, format); } + //! @} //! Set the severity to warning, providing a format string. + //! @{ + template + Derived &warning(const char16_t (&format)[N]) { return log(SeverityWarning, format); } Derived &warning(const QString &format) { return log(SeverityWarning, format); } + //! @} //! Set the severity to error, providing a format string. + //! @{ + template + Derived &error(const char16_t (&format)[N]) { return log(SeverityError, format); } Derived &error(const QString &format) { return log(SeverityError, format); } + //! @} //! Set the severity to s, providing a format string, and adding the validation category. + //! @{ + template + Derived &validation(StatusSeverity s, const char16_t (&format)[N]) { setValidation(); return log(s, format); } Derived &validation(StatusSeverity s, const QString &format) { setValidation(); return log(s, format); } + //! @} //! Set the severity to info, providing a format string, and adding the validation category. + //! @{ + template + Derived &validationInfo(const char16_t (&format)[N]) { setValidation(); return log(SeverityInfo, format); } Derived &validationInfo(const QString &format) { setValidation(); return log(SeverityInfo, format); } + //! @} //! Set the severity to warning, providing a format string, and adding the validation category. + //! @{ + template + Derived &validationWarning(const char16_t (&format)[N]) { setValidation(); return log(SeverityWarning, format); } Derived &validationWarning(const QString &format) { setValidation(); return log(SeverityWarning, format); } + //! @} //! Set the severity to error, providing a format string, and adding the validation category. + //! @{ + template + Derived &validationError(const char16_t (&format)[N]) { setValidation(); return log(SeverityError, format); } Derived &validationError(const QString &format) { setValidation(); return log(SeverityError, format); } + //! @} + + //! Deleted methods to avoid accidental implicit conversion from Latin-1 or UTF-8 string literals. + //! @{ + Derived &log(StatusSeverity, const char *) = delete; + Derived &debug(const char *) = delete; + Derived &info(const char *) = delete; + Derived &warning(const char *) = delete; + Derived &error(const char *) = delete; + Derived &validation(StatusSeverity, const char *) = delete; + Derived &validationInfo(const char *) = delete; + Derived &validationWarning(const char *) = delete; + Derived &validationError(const char *) = delete; + //! @} //! Streaming operators. //! \details If the format string is empty, the message will consist of all streamed values separated by spaces. @@ -115,6 +239,9 @@ namespace BlackMisc Derived & operator <<(const T &v) { return arg(v.toQString()); } //! @} + //! Message empty + bool isEmpty() const { return this->m_message.isEmpty() && this->m_args.isEmpty(); } + private: void setValidation() { m_categories.remove(CLogCategory::uncategorized()); this->addIfNotExisting(CLogCategory::validation()); } Derived &arg(const QString &value) { m_args.push_back(value); return derived(); } @@ -144,14 +271,15 @@ namespace BlackMisc } } + protected: //! \private //! @{ - QString m_message; + CStrongStringView m_message; QStringList m_args; CLogCategoryList m_categories = CLogCategoryList { CLogCategory::uncategorized() }; StatusSeverity m_severity = SeverityDebug; - QString message() const { return Private::arg(m_message, m_args); } + QString message() const { return Private::arg(m_message.view(), m_args); } //! @} }; @@ -281,9 +409,6 @@ namespace BlackMisc //! Append message void appendMessage(const QString &msg); - //! Message empty - bool isEmpty() const { return this->m_message.isEmpty() && this->m_args.isEmpty(); } - //! Returns true if this message was sent by an instance of class T. template bool isFromClass(const T *pointer = nullptr) const @@ -409,6 +534,7 @@ namespace BlackMisc } } // namespace +Q_DECLARE_METATYPE(BlackMisc::CStrongStringView) Q_DECLARE_METATYPE(BlackMisc::CStatusMessage) Q_DECLARE_METATYPE(BlackMisc::CStatusMessage::StatusSeverity) diff --git a/tests/blackmisc/teststatusmessage/teststatusmessage.cpp b/tests/blackmisc/teststatusmessage/teststatusmessage.cpp index 437fde9ca..108b1a3fd 100644 --- a/tests/blackmisc/teststatusmessage/teststatusmessage.cpp +++ b/tests/blackmisc/teststatusmessage/teststatusmessage.cpp @@ -28,6 +28,8 @@ namespace BlackMiscTest private slots: //! Status message void statusMessage(); + //! Message with arguments + void statusArgs(); }; void CTestStatusMessage::statusMessage() @@ -48,6 +50,27 @@ namespace BlackMiscTest cAssign = s2; QVERIFY(cAssign.getMSecsSinceEpoch() == s2.getMSecsSinceEpoch()); } + + void CTestStatusMessage::statusArgs() + { + auto s1 = CStatusMessage().info(u"literal percent: %1"); + auto s2 = CStatusMessage().info(u"literal percent: %a"); + auto s3 = CStatusMessage().info(u"literal percent: %"); + auto s4 = CStatusMessage().info(u"literal percent: %%"); + auto s5 = CStatusMessage().info(u"literal percents: %%%"); + auto s6 = CStatusMessage().info(u"will be expanded: %1%2") << "foo" << "bar"; + auto s7 = CStatusMessage().info(u"will be expanded: %1+%2") << "foo" << "bar"; + auto s8 = CStatusMessage().info(u"will be expanded: %012") << "foo"; + + QVERIFY(s1.getMessage() == "literal percent: %1"); + QVERIFY(s2.getMessage() == "literal percent: %a"); + QVERIFY(s3.getMessage() == "literal percent: %"); + QVERIFY(s4.getMessage() == "literal percent: %"); + QVERIFY(s5.getMessage() == "literal percents: %%"); + QVERIFY(s6.getMessage() == "will be expanded: foobar"); + QVERIFY(s7.getMessage() == "will be expanded: foo+bar"); + QVERIFY(s8.getMessage() == "will be expanded: foo2"); + } } // namespace //! main