[graphics/drawy/gsoc2026] src: feat: add list format support

Abdelhadi Wael <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 0c4d4367b02cba301e9eb2ec66d63fa600fce48c by Abdelhadi Wael.
Committed on 26/07/2026 at 23:51.
Pushed by mlaurent into branch 'gsoc2026'.

feat: add list format support

M  +4    -0    src/gui/autotests/data/text/defaultvalue.ref
M  +28   -0    src/gui/autotests/itemutilstest.cpp
M  +3    -0    src/gui/autotests/itemutilstest.hpp
M  +3    -2    src/gui/autotests/textitemtest.cpp
M  +0    -2    src/gui/common/constants.hpp
M  +45   -0    src/gui/item/itemutils.cpp
M  +3    -0    src/gui/item/itemutils.hpp
M  +90   -31   src/gui/item/text.cpp
M  +1    -0    src/gui/item/text.hpp
M  +6    -0    src/gui/properties/property.cpp
M  +8    -5    src/gui/properties/property.hpp
M  +2    -0    src/widgets/CMakeLists.txt
A  +64   -0    src/widgets/properties/widgets/liststylewidget.cpp     [License: LGPL(v2.0+)]
A  +26   -0    src/widgets/properties/widgets/liststylewidget.hpp     [License: LGPL(v2.0+)]
M  +2    -0    src/widgets/properties/widgets/propertymanager.cpp
M  +8    -4    src/widgets/tools/texttool.cpp

https://invent.kde.org/graphics/drawy/-/commit/0c4d4367b02cba301e9eb2ec66d63fa600fce48c

diff --git a/src/gui/autotests/data/text/defaultvalue.ref b/src/gui/autotests/data/text/defaultvalue.ref
index 707cbb15..3651270b 100644
--- a/src/gui/autotests/data/text/defaultvalue.ref
+++ b/src/gui/autotests/data/text/defaultvalue.ref
@@ -25,6 +25,10 @@
         {
             "type": "TextAlignment",
             "value": "AlignLeft"
+        },
+        {
+            "type": "ListStyle",
+            "value": "None"
         }
     ],
     "text": "",
diff --git a/src/gui/autotests/itemutilstest.cpp b/src/gui/autotests/itemutilstest.cpp
index b1b263ec..d4cc7018 100644
--- a/src/gui/autotests/itemutilstest.cpp
+++ b/src/gui/autotests/itemutilstest.cpp
@@ -7,6 +7,7 @@
 #include "item/item.hpp"
 #include "item/itemutils.hpp"
 #include <QTest>
+#include <QTextListFormat>
 QTEST_GUILESS_MAIN(ItemUtilsTest)
 using namespace Qt::Literals::StringLiterals;
 ItemUtilsTest::ItemUtilsTest(QObject *parent)
@@ -158,4 +159,31 @@ void ItemUtilsTest::shouldConvertItemCornerRectangleTypeEnumToString()
     QCOMPARE(ItemUtils::convertItemCornerRectangleTypeEnumToString(Item::CornerRectangleType::Rounded), u"Rounded"_s);
 }
 
+void ItemUtilsTest::shouldConvertListStyleToString()
+{
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListDisc), u"ListDisc"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListCircle), u"ListCircle"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListSquare), u"ListSquare"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListDecimal), u"ListDecimal"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListLowerAlpha), u"ListLowerAlpha"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListUpperAlpha), u"ListUpperAlpha"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListLowerRoman), u"ListLowerRoman"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListUpperRoman), u"ListUpperRoman"_s);
+    QCOMPARE(ItemUtils::convertListStyleToString(QTextListFormat::ListStyleUndefined), u"None"_s);
+}
+
+void ItemUtilsTest::shouldConvertStringToListStyle()
+{
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListDisc"_s), QTextListFormat::ListDisc);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListCircle"_s), QTextListFormat::ListCircle);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListSquare"_s), QTextListFormat::ListSquare);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListDecimal"_s), QTextListFormat::ListDecimal);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListLowerAlpha"_s), QTextListFormat::ListLowerAlpha);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListUpperAlpha"_s), QTextListFormat::ListUpperAlpha);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListLowerRoman"_s), QTextListFormat::ListLowerRoman);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"ListUpperRoman"_s), QTextListFormat::ListUpperRoman);
+    QCOMPARE(ItemUtils::convertStringToListStyle(u"None"_s), QTextListFormat::ListStyleUndefined);
+    QCOMPARE(ItemUtils::convertStringToListStyle(QString()), QTextListFormat::ListStyleUndefined);
+}
+
 #include "moc_itemutilstest.cpp"
diff --git a/src/gui/autotests/itemutilstest.hpp b/src/gui/autotests/itemutilstest.hpp
index 446c3743..d669da80 100644
--- a/src/gui/autotests/itemutilstest.hpp
+++ b/src/gui/autotests/itemutilstest.hpp
@@ -34,4 +34,7 @@ private Q_SLOTS:
 
     void shouldConvertItemCornerRectangleTypeStringToEnum();
     void shouldConvertItemCornerRectangleTypeEnumToString();
+
+    void shouldConvertListStyleToString();
+    void shouldConvertStringToListStyle();
 };
diff --git a/src/gui/autotests/textitemtest.cpp b/src/gui/autotests/textitemtest.cpp
index 2c7fff8c..e117e4d7 100644
--- a/src/gui/autotests/textitemtest.cpp
+++ b/src/gui/autotests/textitemtest.cpp
@@ -22,8 +22,9 @@ void TextItemTest::shouldHaveDefaultValues()
     const TextItem i;
     QCOMPARE(i.formType(), Item::FormType::Text);
     const auto properties = QList<Property::Type>() << Property::Type::FontSize << Property::Type::FontStyle << Property::Type::StrokeColor
-                                                    << Property::Type::Opacity << Property::Type::FontFamily << Property::Type::TextAlignment;
-    QCOMPARE(i.allPropertyTypes().count(), 6);
+                                                    << Property::Type::Opacity << Property::Type::FontFamily << Property::Type::TextAlignment
+                                                    << Property::Type::ListStyle;
+    QCOMPARE(i.allPropertyTypes().count(), 7);
     for (const auto &prop : properties) {
         QVERIFY(i.allPropertyTypes().contains(prop));
     }
diff --git a/src/gui/common/constants.hpp b/src/gui/common/constants.hpp
index 6ef59478..0d852c47 100644
--- a/src/gui/common/constants.hpp
+++ b/src/gui/common/constants.hpp
@@ -43,8 +43,6 @@ inline constexpr QColor darkBackgroundColor{18, 18, 18};
 inline constexpr int maxItemOpacity{255};
 inline constexpr int erasedItemOpacityDecrease{30}; // % to reduce opacity by when erasing items
 
-inline constexpr double defaultTextBoxWidth{5}; // in pixels
-
 inline constexpr int doubleClickInterval{300}; // milliseconds
 
 inline constexpr qreal tabStopDistance{4};
diff --git a/src/gui/item/itemutils.cpp b/src/gui/item/itemutils.cpp
index 3550a757..d18e4626 100644
--- a/src/gui/item/itemutils.cpp
+++ b/src/gui/item/itemutils.cpp
@@ -14,6 +14,7 @@
 #include "item/line.hpp"
 #include "item/rectangle.hpp"
 #include "item/text.hpp"
+#include <QTextListFormat>
 
 using namespace Qt::Literals::StringLiterals;
 Item::FormType ItemUtils::convertItemTypeStringToEnum(const QString &str)
@@ -313,3 +314,47 @@ int ItemUtils::convertStringToFontStyle(const QString &str)
     }
     return style;
 }
+
+QString ItemUtils::convertListStyleToString(const int style)
+{
+    if (style == QTextListFormat::ListDisc) {
+        return u"ListDisc"_s;
+    } else if (style == QTextListFormat::ListCircle) {
+        return u"ListCircle"_s;
+    } else if (style == QTextListFormat::ListSquare) {
+        return u"ListSquare"_s;
+    } else if (style == QTextListFormat::ListDecimal) {
+        return u"ListDecimal"_s;
+    } else if (style == QTextListFormat::ListLowerAlpha) {
+        return u"ListLowerAlpha"_s;
+    } else if (style == QTextListFormat::ListUpperAlpha) {
+        return u"ListUpperAlpha"_s;
+    } else if (style == QTextListFormat::ListLowerRoman) {
+        return u"ListLowerRoman"_s;
+    } else if (style == QTextListFormat::ListUpperRoman) {
+        return u"ListUpperRoman"_s;
+    }
+    return u"None"_s;
+}
+
+int ItemUtils::convertStringToListStyle(const QString &str)
+{
+    if (str == u"ListDisc"_s) {
+        return QTextListFormat::ListDisc;
+    } else if (str == u"ListCircle"_s) {
+        return QTextListFormat::ListCircle;
+    } else if (str == u"ListSquare"_s) {
+        return QTextListFormat::ListSquare;
+    } else if (str == u"ListDecimal"_s) {
+        return QTextListFormat::ListDecimal;
+    } else if (str == u"ListLowerAlpha"_s) {
+        return QTextListFormat::ListLowerAlpha;
+    } else if (str == u"ListUpperAlpha"_s) {
+        return QTextListFormat::ListUpperAlpha;
+    } else if (str == u"ListLowerRoman"_s) {
+        return QTextListFormat::ListLowerRoman;
+    } else if (str == u"ListUpperRoman"_s) {
+        return QTextListFormat::ListUpperRoman;
+    }
+    return QTextListFormat::ListStyleUndefined;
+}
diff --git a/src/gui/item/itemutils.hpp b/src/gui/item/itemutils.hpp
index 8a7b6e08..b6e1dc01 100644
--- a/src/gui/item/itemutils.hpp
+++ b/src/gui/item/itemutils.hpp
@@ -46,6 +46,9 @@ enum class ZorderMove : uint8_t {
 [[nodiscard]] LIBDRAWYGUI_EXPORT QString convertItemCornerRectangleTypeEnumToString(Item::CornerRectangleType type);
 [[nodiscard]] LIBDRAWYGUI_EXPORT Item::CornerRectangleType convertItemCornerRectangleTypeStringToEnum(const QString &str);
 
+[[nodiscard]] LIBDRAWYGUI_EXPORT QString convertListStyleToString(int style);
+[[nodiscard]] LIBDRAWYGUI_EXPORT int convertStringToListStyle(const QString &str);
+
 [[nodiscard]] std::shared_ptr<Item> createItemFromType(Item::FormType type);
 
 } // namespace ItemUtils
diff --git a/src/gui/item/text.cpp b/src/gui/item/text.cpp
index 707d7c92..16d5839e 100644
--- a/src/gui/item/text.cpp
+++ b/src/gui/item/text.cpp
@@ -11,6 +11,8 @@
 #include <QJsonObject>
 #include <QTextBlock>
 #include <QTextLayout>
+#include <QTextList>
+#include <QTextListFormat>
 #include <utility>
 
 #include "common/constants.hpp"
@@ -30,6 +32,7 @@ TextItem::TextItem()
     m_properties[Property::Type::FontStyle] = Property{u"Normal"_s, Property::Type::FontStyle};
     m_properties[Property::Type::FontFamily] = Property{QStringLiteral("Fuzzy Bubbles"), Property::Type::FontFamily};
     m_properties[Property::Type::TextAlignment] = Property{u"AlignLeft"_s, Property::Type::TextAlignment};
+    m_properties[Property::Type::ListStyle] = Property{u"None"_s, Property::Type::ListStyle};
 
     m_document.setDefaultFont(getFont());
 
@@ -50,13 +53,9 @@ TextItem::~TextItem()
 
 void TextItem::createTextBox(const QPointF position)
 {
-    m_boundingBox.setTopLeft(position);
-    m_boundingBox.setWidth(Common::defaultTextBoxWidth);
-
     m_document.setDefaultFont(getFont());
-    const QFontMetricsF metrics{getFont()};
-    m_boundingBox.setHeight(metrics.height());
-
+    updateBoundingBox();
+    m_boundingBox.moveTopLeft(QPointF(position.x() - m_document.indentWidth(), position.y()));
     setDirty(true);
 }
 
@@ -143,17 +142,19 @@ void TextItem::commitTransformation()
     if (m_isHorizontalResize) {
         const qreal width = m_wrapWidth > 0 ? m_wrapWidth : m_boundingBox.width();
         m_wrapWidth = std::max(width * scaleX, minWrapWidth());
+    } else if (m_wrapWidth > 0) {
+        m_wrapWidth = std::max(m_wrapWidth * scaleX, minWrapWidth());
     }
 
     m_boundingBox = filtered.map(m_boundingBox).boundingRect();
 
     if (!qFuzzyCompare(1.0, scaleY)) {
-        scaleTextFragments(scaleY);
         const qreal curFontSize{property(Property::Type::FontSize).value<qreal>()};
+        scaleTextFragments(scaleY);
         const qreal newFontSize{std::max(1.0, curFontSize * scaleY)};
         Item::setProperty(Property::Type::FontSize, Property{newFontSize, Property::Type::FontSize});
     }
-    updateBoundingBox();
+    updateIntendWidth();
     m_isHorizontalResize = false;
 }
 
@@ -161,35 +162,37 @@ void TextItem::scaleTextFragments(const qreal scaleY)
 {
     const QSignalBlocker blocker{m_document};
     m_cursor.beginEditBlock();
-
-    m_document.setTextWidth(-1);
-    const qreal oldIdealWidth = m_document.idealWidth();
-
     QTextBlock block = m_document.firstBlock();
     while (block.isValid()) {
-        for (auto it = block.begin(); !it.atEnd(); ++it) {
-            const QTextFragment fragment = it.fragment();
-            QTextCharFormat fmt = fragment.charFormat();
+        if (block.begin() == block.end()) {
+            // empty block -> could be a bullet point with no text
+            QTextCharFormat fmt = block.charFormat();
             const qreal size = getFontSize(fmt);
-
             fmt.setFontPointSize(std::max(1.0, size * scaleY));
             fmt.setProperty(QTextFormat::FontPixelSize, QVariant());
             fmt.setProperty(QTextFormat::FontSizeAdjustment, QVariant());
-
-            m_cursor.setPosition(fragment.position());
-            m_cursor.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor);
-            m_cursor.mergeCharFormat(fmt);
+            m_cursor.setPosition(block.position());
+            m_cursor.mergeBlockCharFormat(fmt);
+            m_currentFormat.merge(fmt);
+        } else {
+            for (auto it = block.begin(); !it.atEnd(); ++it) {
+                const QTextFragment fragment = it.fragment();
+                QTextCharFormat fmt = fragment.charFormat();
+                const qreal size = getFontSize(fmt);
+
+                fmt.setFontPointSize(std::max(1.0, size * scaleY));
+                fmt.setProperty(QTextFormat::FontPixelSize, QVariant());
+                fmt.setProperty(QTextFormat::FontSizeAdjustment, QVariant());
+
+                m_cursor.setPosition(fragment.position());
+                m_cursor.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor);
+                m_cursor.mergeBlockCharFormat(fmt);
+                m_cursor.mergeCharFormat(fmt);
+                m_currentFormat.merge(fmt);
+            }
         }
         block = block.next();
     }
-
-    m_document.setTextWidth(-1);
-    const qreal newIdealWidth = m_document.idealWidth();
-    const qreal scale = newIdealWidth / oldIdealWidth;
-    if (m_wrapWidth > 0) {
-        m_wrapWidth = std::max(m_wrapWidth * scale, minWrapWidth());
-    }
-
     m_cursor.endEditBlock();
 }
 
@@ -296,6 +299,35 @@ qreal TextItem::getFontSize(const QTextCharFormat &fmt)
     return std::max(size, 1.0);
 }
 
+void TextItem::updateIntendWidth()
+{
+    QTextBlock block = m_document.firstBlock();
+    QFont font = block.charFormat().font();
+    while (block.isValid()) {
+        const qreal size = block.charFormat().fontPointSize();
+        if (size > font.pointSize()) {
+            font = block.charFormat().font();
+        }
+        block = block.next();
+    }
+    const QFontMetricsF metrics(font);
+    qreal indentWidth = 0.0;
+
+    const int style = ItemUtils::convertStringToListStyle(property(Property::Type::ListStyle).value<QString>());
+
+    if (style == QTextListFormat::ListDisc || style == QTextListFormat::ListCircle || style == QTextListFormat::ListSquare) {
+        indentWidth = metrics.horizontalAdvance(QStringLiteral("■ "));
+    } else if (style == QTextListFormat::ListDecimal) {
+        indentWidth = metrics.horizontalAdvance(QStringLiteral("99. "));
+    } else if (style == QTextListFormat::ListLowerRoman || style == QTextListFormat::ListUpperRoman) {
+        indentWidth = metrics.horizontalAdvance(QStringLiteral("VIII. "));
+    } else if (style == QTextListFormat::ListLowerAlpha || style == QTextListFormat::ListUpperAlpha) {
+        indentWidth = metrics.horizontalAdvance(QStringLiteral("W. "));
+    }
+    m_document.setIndentWidth(indentWidth);
+    updateBoundingBox();
+}
+
 QFont TextItem::getFont() const
 {
     QFont font;
@@ -452,6 +484,13 @@ Property TextItem::property(const Property::Type propertyType) const
         const int alignment = cursor.blockFormat().alignment();
         return Property{ItemUtils::convertTextAlignmentToString(alignment), Property::Type::TextAlignment};
     }
+    case Property::Type::ListStyle: {
+        if (m_cursor.currentList()) {
+            return Property{ItemUtils::convertListStyleToString(m_cursor.currentList()->format().style()), Property::Type::ListStyle};
+        } else {
+            return Property{u"None"_s, Property::Type::ListStyle};
+        }
+    }
     default:
         return Item::property(propertyType);
     }
@@ -551,12 +590,26 @@ void TextItem::setProperty(const Property::Type propertyType, const Property new
     case Property::Type::TextAlignment: {
         QTextBlockFormat blockFmt;
         blockFmt.setAlignment(static_cast<Qt::Alignment>(ItemUtils::convertStringToTextAlignment(newObj.value<QString>())));
-        QTextCursor docCursor(&m_document);
-        docCursor.select(QTextCursor::Document);
-        docCursor.mergeBlockFormat(blockFmt);
+        QTextCursor cursor = m_cursor;
+        cursor.select(QTextCursor::Document);
+        cursor.mergeBlockFormat(blockFmt);
         Item::setProperty(propertyType, newObj);
         return;
     }
+    case Property::Type::ListStyle: {
+        const auto style = static_cast<QTextListFormat::Style>(ItemUtils::convertStringToListStyle(newObj.value<QString>()));
+        QTextCursor cursor = m_cursor;
+        cursor.select(QTextCursor::Document);
+        QTextBlockFormat blockFmt;
+        blockFmt.setIndent(0);
+
+        cursor.mergeBlockFormat(blockFmt);
+        QTextListFormat listFmt;
+        listFmt.setStyle(style);
+        cursor.createList(listFmt);
+        cursor.mergeBlockCharFormat(cursor.charFormat());
+        break;
+    }
     default:
         Item::setProperty(propertyType, newObj);
         return;
@@ -566,14 +619,20 @@ void TextItem::setProperty(const Property::Type propertyType, const Property new
     if (m_mode == Mode::Normal) {
         m_cursor.select(QTextCursor::Document);
         m_cursor.mergeCharFormat(fmt);
+        m_cursor.mergeBlockCharFormat(fmt);
         m_cursor.clearSelection();
     } else {
         m_cursor.mergeCharFormat(fmt);
+        if (m_cursor.hasSelection()) {
+            m_cursor.mergeBlockCharFormat(fmt);
+        }
     }
     m_cursor.endEditBlock();
 
     m_currentFormat.merge(fmt);
     Item::setProperty(propertyType, newObj);
+
+    updateIntendWidth();
 }
 
 QJsonObject TextItem::serialize(const int zorder) const
diff --git a/src/gui/item/text.hpp b/src/gui/item/text.hpp
index d8b54815..1db03a6a 100644
--- a/src/gui/item/text.hpp
+++ b/src/gui/item/text.hpp
@@ -81,6 +81,7 @@ private:
 
     LIBDRAWYGUI_NO_EXPORT void scaleTextFragments(const qreal scaleY);
     LIBDRAWYGUI_NO_EXPORT void updateBoundingBox();
+    LIBDRAWYGUI_NO_EXPORT void updateIntendWidth();
 
     QTextDocument m_document;
     SpellCheckHighlighter *const m_highlighter;
diff --git a/src/gui/properties/property.cpp b/src/gui/properties/property.cpp
index 3d3e3491..d6eca3a9 100644
--- a/src/gui/properties/property.cpp
+++ b/src/gui/properties/property.cpp
@@ -56,6 +56,8 @@ Property::Type Property::convertStringToEnum(const QString &str)
         return Property::Type::FontStyle;
     } else if (str == u"CornerRectangleType") {
         return Property::Type::CornerRectangleType;
+    } else if (str == u"ListStyle") {
+        return Property::Type::ListStyle;
     } else {
         qCWarning(DRAWY_GUI_LOG) << "Property::Type is not defined for: " << str;
     }
@@ -100,6 +102,8 @@ QString Property::convertEnumToString(Property::Type type)
         return u"ZOrder"_s;
     case Property::Type::CornerRectangleType:
         return u"CornerRectangleType"_s;
+    case Property::Type::ListStyle:
+        return u"ListStyle"_s;
     case Property::Type::Null:
         qCWarning(DRAWY_GUI_LOG) << "Don't save null property ";
     }
@@ -144,6 +148,8 @@ QString Property::information(Type type)
         return i18n("Change ZOrder");
     case Property::Type::CornerRectangleType:
         return i18n("Change Corner Rectangle Type");
+    case Property::Type::ListStyle:
+        return i18n("Change List Style");
     case Property::Type::Null:
         qCWarning(DRAWY_GUI_LOG) << "Don't save null property ";
     }
diff --git a/src/gui/properties/property.hpp b/src/gui/properties/property.hpp
index e8c997be..bc8e542f 100644
--- a/src/gui/properties/property.hpp
+++ b/src/gui/properties/property.hpp
@@ -35,6 +35,7 @@ public:
         TextAlignment,
         ZOrder,
         CornerRectangleType,
+        ListStyle,
     };
 
     enum FontStyle : int8_t {
@@ -75,16 +76,18 @@ public:
             return 12;
         case Type::TextAlignment:
             return 13;
-        case Type::Alignment:
+        case Type::ListStyle:
             return 14;
-        case Type::ZOrder:
+        case Type::Alignment:
             return 15;
-        case Type::Actions:
+        case Type::ZOrder:
             return 16;
-        case Type::CornerRectangleType:
+        case Type::Actions:
             return 17;
-        case Type::Null:
+        case Type::CornerRectangleType:
             return 18;
+        case Type::Null:
+            return 19;
         }
 
         return 0;
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
index 60cee94e..a1f33876 100644
--- a/src/widgets/CMakeLists.txt
+++ b/src/widgets/CMakeLists.txt
@@ -146,6 +146,8 @@ target_sources(
         properties/widgets/strokestyleactionswidget.cpp
         properties/widgets/textalignmentwidget.hpp
         properties/widgets/textalignmentwidget.cpp
+        properties/widgets/liststylewidget.hpp
+        properties/widgets/liststylewidget.cpp
         properties/widgets/cornerrectangletypewidget.hpp
         properties/widgets/cornerrectangletypewidget.cpp
         tools/arrowtool.cpp
diff --git a/src/widgets/properties/widgets/liststylewidget.cpp b/src/widgets/properties/widgets/liststylewidget.cpp
new file mode 100644
index 00000000..a575b322
--- /dev/null
+++ b/src/widgets/properties/widgets/liststylewidget.cpp
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Abdelhadi Wael <[email protected]>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "liststylewidget.hpp"
+#include "properties/property.hpp"
+#include <KLocalizedString>
+#include <QComboBox>
+#include <QHBoxLayout>
+
+using namespace Qt::Literals::StringLiterals;
+
+ListStyleWidget::ListStyleWidget(QWidget *parent)
+    : PropertyWidget{parent}
+    , m_comboBox{new QComboBox}
+{
+    m_widget = new QWidget(parent);
+    const auto layout = new QHBoxLayout(m_widget);
+    layout->setContentsMargins({});
+
+    m_comboBox->addItem(i18n("None"), u"None"_s);
+    m_comboBox->addItem(i18n("Bullet (Disc)"), u"ListDisc"_s);
+    m_comboBox->addItem(i18n("Bullet (Circle)"), u"ListCircle"_s);
+    m_comboBox->addItem(i18n("Bullet (Square)"), u"ListSquare"_s);
+    m_comboBox->addItem(i18n("Numbered (Decimal)"), u"ListDecimal"_s);
+    m_comboBox->addItem(i18n("Numbered (Lower Alpha)"), u"ListLowerAlpha"_s);
+    m_comboBox->addItem(i18n("Numbered (Upper Alpha)"), u"ListUpperAlpha"_s);
+    m_comboBox->addItem(i18n("Numbered (Lower Roman)"), u"ListLowerRoman"_s);
+    m_comboBox->addItem(i18n("Numbered (Upper Roman)"), u"ListUpperRoman"_s);
+    layout->addWidget(m_comboBox);
+
+    connect(m_comboBox, &QComboBox::currentIndexChanged, this, [this] {
+        Q_EMIT changed(value());
+    });
+
+    m_widget->hide();
+}
+
+void ListStyleWidget::setValue(const QVariant &val)
+{
+    const QSignalBlocker blocker(m_comboBox);
+
+    const QString style = val.toString();
+    const int index = m_comboBox->findData(style);
+    if (index != -1) {
+        m_comboBox->setCurrentIndex(index);
+    } else {
+        m_comboBox->setCurrentIndex(0);
+    }
+}
+
+QString ListStyleWidget::name() const
+{
+    return i18n("List Style");
+}
+
+Property ListStyleWidget::value() const
+{
+    return Property(m_comboBox->currentData().toString(), Property::Type::ListStyle);
+}
+
+#include "moc_liststylewidget.cpp"
diff --git a/src/widgets/properties/widgets/liststylewidget.hpp b/src/widgets/properties/widgets/liststylewidget.hpp
new file mode 100644
index 00000000..8b18a468
--- /dev/null
+++ b/src/widgets/properties/widgets/liststylewidget.hpp
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Abdelhadi Wael <[email protected]>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+#include "libdrawywidgets_private_export.h"
+#include "propertywidget.hpp"
+
+class QComboBox;
+
+class LIBDRAWYWIDGETS_TESTS_EXPORT ListStyleWidget : public PropertyWidget
+{
+    Q_OBJECT
+public:
+    explicit ListStyleWidget(QWidget *parent = nullptr);
+
+    [[nodiscard]] QString name() const override;
+    [[nodiscard]] Property value() const override;
+
+    void setValue(const QVariant &val) override;
+
+private:
+    QComboBox *const m_comboBox;
+};
diff --git a/src/widgets/properties/widgets/propertymanager.cpp b/src/widgets/properties/widgets/propertymanager.cpp
index 6f1c20ce..2e25391a 100644
--- a/src/widgets/properties/widgets/propertymanager.cpp
+++ b/src/widgets/properties/widgets/propertymanager.cpp
@@ -16,6 +16,7 @@
 #include "properties/widgets/alignmentwidget.hpp"
 #include "properties/widgets/backgroundstylewidget.hpp"
 #include "properties/widgets/cornerrectangletypewidget.hpp"
+#include "properties/widgets/liststylewidget.hpp"
 #include "properties/widgets/stokestylewidget.hpp"
 #include "properties/widgets/textalignmentwidget.hpp"
 #include "properties/widgets/zorderwidget.hpp"
@@ -42,6 +43,7 @@ PropertyManager::PropertyManager(ActionManager *actionManager, QWidget *parent)
     m_widgets[Property::Type::StartArrowType] = new StartArrowTypeWidget(parent);
     m_widgets[Property::Type::EndArrowType] = new EndArrowTypeWidget(parent);
     m_widgets[Property::Type::TextAlignment] = new TextAlignmentWidget(parent);
+    m_widgets[Property::Type::ListStyle] = new ListStyleWidget(parent);
     m_widgets[Property::Type::CornerRectangleType] = new CornerRectangleTypeWidget(parent);
 
     for (auto i = m_widgets.cbegin(), end = m_widgets.cend(); i != end; ++i) {
diff --git a/src/widgets/tools/texttool.cpp b/src/widgets/tools/texttool.cpp
index 3ee3e0f9..5f315bb8 100644
--- a/src/widgets/tools/texttool.cpp
+++ b/src/widgets/tools/texttool.cpp
@@ -30,7 +30,6 @@
 #include <QInputMethod>
 #include <QTextBlock>
 #include <QTextCharFormat>
-#include <QTextLayout>
 #include <QToolTip>
 
 using namespace Qt::Literals::StringLiterals;
@@ -45,7 +44,8 @@ TextTool::TextTool(ApplicationContext *context)
                     Property::Type::FontSize,
                     Property::Type::FontStyle,
                     Property::Type::FontFamily,
-                    Property::Type::TextAlignment};
+                    Property::Type::TextAlignment,
+                    Property::Type::ListStyle};
 }
 
 void TextTool::mousePressed(ApplicationContext *context)
@@ -74,6 +74,7 @@ void TextTool::mousePressed(ApplicationContext *context)
                 m_curItem->setProperty(Property::Type::FontStyle, uiContext->propertyManager()->value(Property::Type::FontStyle));
                 m_curItem->setProperty(Property::Type::FontFamily, uiContext->propertyManager()->value(Property::Type::FontFamily));
                 m_curItem->setProperty(Property::Type::TextAlignment, uiContext->propertyManager()->value(Property::Type::TextAlignment));
+                m_curItem->setProperty(Property::Type::ListStyle, uiContext->propertyManager()->value(Property::Type::ListStyle));
 
                 m_curItem->createTextBox(transformer.viewToWorld(uiContext->appEvent()->pos()));
 
@@ -288,8 +289,11 @@ void TextTool::processKey(const Event *ev) const
     switch (ev->key()) {
     case Qt::Key_Return:
     case Qt::Key_Enter:
-        // QChar::LineSeparator
-        cursor.insertText(u"\u2028"_s);
+        if (shift) {
+            cursor.insertText(u"\u2028"_s);
+        } else {
+            cursor.insertText(u"\n"_s);
+        }
         return;
     case Qt::Key_Left:
         cursor.movePosition(ctrl ? QTextCursor::WordLeft : QTextCursor::Left, moveMode);
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.