[kdevelop/kdevelop] plugins: Add gdb pretty-printer for QText*Format
David Faure <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 1791125742c92b0372c58e8dda1bd1c79bb79057 by David Faure.
Committed on 03/08/2026 at 09:14.
Pushed by dfaure into branch 'master'.
Add gdb pretty-printer for QText*Format
Only tested with Qt6.
Requires debug info for the private class QTextFormatPrivate,
the unittest skips itself otherwise.
M +3 -0 plugins/debuggercommon/tests/debuggees/CMakeLists.txt
A +40 -0 plugins/debuggercommon/tests/debuggees/qtextformat.cpp *
M +121 -0 plugins/gdb/printers/qt.py
M +56 -0 plugins/gdb/unittests/test_gdbprinters.cpp
M +1 -0 plugins/gdb/unittests/test_gdbprinters.h
The files marked with a * at the end have a non valid license. Please read: https://community.kde.org/Policies/Licensing_Policy and use the headers which are listed at that page.
https://invent.kde.org/kdevelop/kdevelop/-/commit/1791125742c92b0372c58e8dda1bd1c79bb79057
diff --git a/plugins/debuggercommon/tests/debuggees/CMakeLists.txt b/plugins/debuggercommon/tests/debuggees/CMakeLists.txt
index f054611aa3..9adb96bc41 100644
--- a/plugins/debuggercommon/tests/debuggees/CMakeLists.txt
+++ b/plugins/debuggercommon/tests/debuggees/CMakeLists.txt
@@ -123,6 +123,9 @@ target_link_libraries(debuggee_qjson Qt::Core)
add_debuggable_executable(debuggee_qvariant SRCS qvariant.cpp)
target_link_libraries(debuggee_qvariant Qt::Core)
+add_debuggable_executable(debuggee_qtextformat SRCS qtextformat.cpp)
+target_link_libraries(debuggee_qtextformat Qt::Core Qt::Gui)
+
add_debuggable_executable(debuggee_qlistpod SRCS qlistpod.cpp)
target_link_libraries(debuggee_qlistpod Qt::Core)
diff --git a/plugins/debuggercommon/tests/debuggees/qtextformat.cpp b/plugins/debuggercommon/tests/debuggees/qtextformat.cpp
new file mode 100644
index 0000000000..e4523d2085
--- /dev/null
+++ b/plugins/debuggercommon/tests/debuggees/qtextformat.cpp
@@ -0,0 +1,40 @@
+#include <QTextFormat>
+
+int main()
+{
+ QTextFormat invalidFormat;
+ QTextFormat obsoleteFormat(4); // the obsolete QTextFormat::TableFormat
+ QTextFormat userFormat(QTextFormat::UserFormat);
+ QTextFormat userFormat3(QTextFormat::UserFormat + 3);
+
+ QTextCharFormat emptyCharFormat;
+
+ QTextBlockFormat blockFormat;
+ blockFormat.setTopMargin(10);
+
+ QTextCharFormat charFormat;
+ charFormat.setFontItalic(true);
+ charFormat.setToolTip(QStringLiteral("some tooltip"));
+ // FontCapitalization shares its value with the FirstFontProperty alias
+ charFormat.setFontCapitalization(QFont::AllUppercase);
+ // FontSizeAdjustment shares its value with the FontSizeIncrement alias
+ charFormat.setProperty(QTextFormat::FontSizeAdjustment, 1);
+ charFormat.setProperty(QTextFormat::UserProperty + 1, 42); // a property without a name
+
+ QTextListFormat listFormat;
+ listFormat.setIndent(2);
+
+ QTextFrameFormat frameFormat;
+ frameFormat.setBorder(3);
+
+ QTextTableFormat tableFormat;
+ tableFormat.setCellPadding(4);
+
+ QTextImageFormat imageFormat;
+ imageFormat.setName(QStringLiteral("image.png"));
+
+ QTextCharFormat userObjectFormat;
+ userObjectFormat.setObjectType(QTextFormat::UserObject + 1);
+
+ return 0; // line 39
+}
diff --git a/plugins/gdb/printers/qt.py b/plugins/gdb/printers/qt.py
index 72c988918b..3fe5174d30 100644
--- a/plugins/gdb/printers/qt.py
+++ b/plugins/gdb/printers/qt.py
@@ -1568,6 +1568,126 @@ class QVariantPrinter:
return "QVariant(%s, %s)" % (type_str, value_str)
+class QTextFormatPrinter(PrinterBaseType):
+ "Print a QTextFormat, with its properties as children"
+
+ # QTextFormat::FormatType, without InvalidFormat and UserFormat
+ _classNames = {
+ 1: "QTextBlockFormat",
+ 2: "QTextCharFormat",
+ 3: "QTextListFormat",
+ 5: "QTextFrameFormat",
+ }
+
+ # QTextFormat::ObjectTypes, without UserObject
+ _objectTypeNames = {
+ 0: "NoObject",
+ 1: "ImageObject",
+ 2: "TableObject",
+ 3: "TableCellObject",
+ }
+
+ # Enumerators of QTextFormat::Property which are aliases for another property.
+ # Note that OldFontLetterSpacingType and OldFontStretch are real properties, not aliases.
+ _propertyAliases = ('FirstFontProperty', 'LastFontProperty', 'FontSizeIncrement',
+ 'OldFontFamily', 'OldTextUnderlineColor')
+
+ _userFormat = 100 # QTextFormat::UserFormat
+ _objectTypeProperty = 0x2f00 # QTextFormat::ObjectType
+ _userProperty = 0x100000 # QTextFormat::UserProperty
+
+ def __init__(self, val):
+ self._properties = [] # list of (name, value) pairs
+
+ formatType = int(val['format_type'])
+ if formatType == -1: # QTextFormat::InvalidFormat
+ self._string = "QTextFormat(InvalidFormat)"
+ return
+ className = self._classNames.get(formatType)
+ if className is None:
+ if formatType < self._userFormat:
+ self._string = f"QTextFormat(UnknownFormatType={formatType})"
+ return
+ # user formats have no dedicated class
+ offset = formatType - self._userFormat
+ className = "QTextFormat(UserFormat)" if offset == 0 else f"QTextFormat(UserFormat+{offset})"
+
+ d_ptr = pointer_from_possible_totally_ordered_wrapper(val['d']['d'])
+ if d_ptr: # otherwise this is a default-constructed format, without any property
+ # TODO print d_ptr['fnt'] once we have a QFont pretty-printer
+ try:
+ self._properties = self._readProperties(d_ptr['props'])
+ except gdb.error as e:
+ # QTextFormatPrivate is a private Qt class, so its debug info is often missing
+ self._string = f"{className} (cannot read the properties: {e})"
+ return
+
+ self._string = f"{className} (size = {len(self._properties)})"
+
+ @classmethod
+ def _readProperties(cls, props):
+ propertyNames = cls._readPropertyNames()
+ properties = []
+ # props is a QVector in Qt5 and a QList in Qt6, QVectorPrinter handles both
+ for _, prop in QVectorPrinter(props, "QVector").children():
+ key = int(prop['key'])
+ name = propertyNames.get(key) or cls._unnamedProperty(key)
+ properties.append((name, cls._propertyValue(key, prop['value'])))
+ return properties
+
+ @classmethod
+ def _readPropertyNames(cls):
+ "Map QTextFormat::Property values to their enumerator names"
+ names = {}
+ try:
+ enumType = gdb.lookup_type('QTextFormat::Property')
+ except gdb.error:
+ return names
+ for field in enumType.fields():
+ if field.name:
+ name = field.name.split('::')[-1]
+ if name not in cls._propertyAliases:
+ names[int(field.enumval)] = name
+ return names
+
+ @classmethod
+ def _unnamedProperty(cls, key):
+ "Name a property which has no enumerator, i.e. a custom one, or one from a newer Qt version"
+ if key > cls._userProperty:
+ return f"UserProperty+{key - cls._userProperty}"
+ return hex(key)
+
+ @classmethod
+ def _propertyValue(cls, key, value):
+ if key != cls._objectTypeProperty or not cls._isIntVariant(value):
+ return value
+ # the int maps to the QTextFormat::ObjectTypes enum
+ objectType = int(value['d']['data']['data'].cast(gdb.lookup_type('int').pointer()).dereference())
+ if objectType >= 0x1000: # QTextFormat::UserObject
+ return f"UserObject ({objectType})"
+ return cls._objectTypeNames.get(objectType, f"UnknownObjectType ({objectType})")
+
+ @staticmethod
+ def _isIntVariant(variant):
+ "Check that the QVariant really holds an int, before reading its data as such"
+ try:
+ d = variant['d']
+ if not has_field(d, 'packedType'): # Qt5, not supported here
+ return False
+ metaType = (d['packedType'] << 2).cast(gdb.lookup_type("QtPrivate::QMetaTypeInterface").pointer())
+ return metaType['name'].string() == 'int'
+ except gdb.error:
+ return False
+
+ def to_string(self):
+ return self._string
+
+ def num_children(self):
+ return len(self._properties)
+
+ def children(self):
+ return self._properties
+
pretty_printers_dict = {}
def register_qt_printers (obj):
@@ -1613,6 +1733,7 @@ def build_dictionary ():
pretty_printers_dict[re.compile('^QJsonDocument$')] = lambda val: QJsonDocumentPrinter(val)
pretty_printers_dict[re.compile('^QJsonValue$')] = lambda val: QJsonValuePrinter(val)
pretty_printers_dict[re.compile('^QJsonValue(Const|)Ref$')] = lambda val: QJsonValueConstRefPrinter(val)
+ pretty_printers_dict[re.compile('^QText[A-Za-z]*Format$')] = lambda val: QTextFormatPrinter(val)
build_dictionary ()
diff --git a/plugins/gdb/unittests/test_gdbprinters.cpp b/plugins/gdb/unittests/test_gdbprinters.cpp
index 0c545554b4..efac1899b8 100644
--- a/plugins/gdb/unittests/test_gdbprinters.cpp
+++ b/plugins/gdb/unittests/test_gdbprinters.cpp
@@ -1118,6 +1118,62 @@ void QtPrintersTest::testQVariant()
QVERIFY(printNext().contains("QVariant(SomeCustomType, {\n foo = 42\n})"));
}
+void QtPrintersTest::testQTextFormat()
+{
+ GdbProcess gdb(QStringLiteral("debuggee_qtextformat"));
+ gdb.execute("break qtextformat.cpp:39");
+ gdb.execute("run");
+
+ // The printer reads the members of QTextFormatPrivate, which is a private Qt class.
+ if (gdb.execute("ptype QTextFormatPrivate").contains("No symbol")) {
+ // Without that debug info the printer can only print the class name. It must not throw,
+ // waitForPrompt() fails the test if the printer raises a Python exception.
+ QVERIFY(printedValue(gdb, "charFormat").startsWith("QTextCharFormat "));
+ QSKIP("Skipping the rest because the debug info of QtGui is not available");
+ }
+
+ QCOMPARE(printedValue(gdb, "invalidFormat"), "QTextFormat(InvalidFormat)");
+ QCOMPARE(printedValue(gdb, "obsoleteFormat"), "QTextFormat(UnknownFormatType=4)");
+ QCOMPARE(printedValue(gdb, "userFormat"), "QTextFormat(UserFormat) (size = 0)");
+ QCOMPARE(printedValue(gdb, "userFormat3"), "QTextFormat(UserFormat+3) (size = 0)");
+ QCOMPARE(printedValue(gdb, "emptyCharFormat"), "QTextCharFormat (size = 0)");
+
+ QCOMPARE(printedValue(gdb, "blockFormat"), R"(QTextBlockFormat (size = 1) = {
+ BlockTopMargin = QVariant(double, 10)
+})");
+
+ QCOMPARE(printedValue(gdb, "charFormat"), R"(QTextCharFormat (size = 5) = {
+ FontItalic = QVariant(bool, true),
+ TextToolTip = QVariant(QString, "some tooltip"),
+ FontCapitalization = QVariant(int, 1),
+ FontSizeAdjustment = QVariant(int, 1),
+ UserProperty+1 = QVariant(int, 42)
+})");
+
+ QCOMPARE(printedValue(gdb, "userObjectFormat"), R"(QTextCharFormat (size = 1) = {
+ ObjectType = UserObject (4097)
+})");
+
+ // The formats below have additional properties set by their constructor, which vary between Qt versions
+ auto out = printedValue(gdb, "listFormat");
+ QVERIFY(out.startsWith("QTextListFormat (size = "));
+ QVERIFY(out.contains("ListIndent = QVariant(int, 2)"));
+
+ out = printedValue(gdb, "frameFormat");
+ QVERIFY(out.startsWith("QTextFrameFormat (size = "));
+ QVERIFY(out.contains("FrameBorder = QVariant(double, 3)"));
+
+ out = printedValue(gdb, "tableFormat");
+ QVERIFY(out.startsWith("QTextFrameFormat (size = "));
+ QVERIFY(out.contains("ObjectType = TableObject"));
+ QVERIFY(out.contains("TableCellPadding = QVariant(double, 4)"));
+
+ out = printedValue(gdb, "imageFormat");
+ QVERIFY(out.startsWith("QTextCharFormat (size = "));
+ QVERIFY(out.contains("ObjectType = ImageObject"));
+ QVERIFY(out.contains("ImageName = QVariant(QString, \"image.png\")"));
+}
+
void QtPrintersTest::testKTextEditorTypes()
{
GdbProcess gdb(QStringLiteral("debuggee_ktexteditortypes"));
diff --git a/plugins/gdb/unittests/test_gdbprinters.h b/plugins/gdb/unittests/test_gdbprinters.h
index 5284b8c094..9837a03d90 100644
--- a/plugins/gdb/unittests/test_gdbprinters.h
+++ b/plugins/gdb/unittests/test_gdbprinters.h
@@ -42,6 +42,7 @@ private Q_SLOTS:
void testQCbor();
void testQJson();
void testQVariant();
+ void testQTextFormat();
void testKTextEditorTypes();
void testKDevelopTypes();
};