[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();
 };
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.