View Issue Details

IDProjectCategoryView StatusLast Update
0017243ScribusText Frames / Story Editorpublic2026-09-09 01:15
Reporterale Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Product Version1.6.3.svn 
Summary0017243: Left indent for numbered lists
DescriptionThe Scribus Paragraph Effects now have a list.

Sadly, it's not possible to create a correct hanging indent, because the existing field "offset" does not start from the left (or right for RTL) margin but from the suffix.

This is a bigger issue at least for numbered lists with more than 9 elements (and for shorter lists that are not in arab numbers).

See this forum thread for a complaint about this:

Aligning Numbered Lists Vertically
 https://forums.scribus.net/index.php/topic,5213.msg23805.html#msg23805

I've checked how Libreoffice does it:
From what I can tell, it simply adds a tab character after the suffix. Automatically. Always.
If no tab is defined, the cursor will jump to the first default tab.

This having been said that, I wonder what's the use case for the offset that starts from the number / suffix.
Is there any?

If there is no known usage for it, I guess that the simplest solution is to repurpose this offset to start from the left margin.
The feature is pretty new in the stable version and -- if nobody if it is has no real purpose in its current implementation, i don't see why somebody would have used it...

The better solution is probably:

- The "offset" field is deprecated (and in the long term completely removed from Scribus; except if somebody comes up with a use case for the current implementation...).
- By default the list paragraph effects jump to the first existing tab (the default one if there is none) by inserting an automatic tab character after the suffix
- It might be possible to disable the automatic tab (if somebody comes up with a use case where this is useful)
- If there is a way to disable the automatic tab and the offset gets deprecated, allow multiple chars in the suffix (in order to allow a space after the dot)
TagsNo tags attached.
PatchNo

Relationships

related to 0016985 new Numbered list: there is no way to add a tab after the suffix 
related to 0016963 closedFahad second line is indented on the left, when RTL drop caps is enabled 
related to 0013488 confirmed Position of the number in numbered lists 

Activities

ale

2025-02-20 09:15

manager   ~0052074

this should be fixed as soon as possible.

people are starting using the numbering and, as soon as the number grows over 9 (or the width of the digits are not somehow the same), the entries WILL NOT BE ALIGNED!

the current way of doing the auto-indent is almost always wrong and shall be deprecated / replaced by one that does work!

the solution seems pretty simple to me:

- the "auto-indent" checkbox should be used to add a tab before the number and one after the dot.
- the "Character offset" field should be used to define an hanging indent and a left tab.
  on top of, it we need a right tab slightly to left, of it for aligning the number and the dot.
- if the user has defined an (hanging) indent or tabs for the paragraph, that indent should be used and no new tab should be "defined".
  the "Character offset" field should be disabled and a tooltip should explain why ("can only be filled when no tabs or indent is defined").
autoindent.png (7,871 bytes)   
autoindent.png (7,871 bytes)   
autoindent-correct.png (13,748 bytes)   
autoindent-correct.png (13,748 bytes)   

ale

2025-02-20 14:10

manager   ~0052075

i have to correct myself, it seems to be possible to get an acceptable result by setting the left indent for the paragraph, the automatic indent and a small character offset.

but, then, the numbers are right aligned.

a solution that would use the tabs and hanging indents would be much more flexible and, probably, easier to discover for the user.
numbering-nitramr.png (119,211 bytes)   
numbering-nitramr.png (119,211 bytes)   

ale

2025-02-20 15:31

manager   ~0052076

Last edited: 2025-02-20 15:32

the code for applying the distances in the list seems to be very intricate.

the PageItem_TextFrame::layout() function that probably applies the layout of the lists (among a lot of other things) is almost 2000 lines of code with specific instruction for the various supported types of text elements.
there might be performance reasons to keep all the code together, but i fear that the numerious conditions and loops also hide bugs and inefficient code that could be avoided by refactoring the code in function and structures with telling names and one concern.
also, by having separate functions for each concern, it might be possible to add tests and be more confident that the code does the right thing!

as an example (i swear, it was in in the 40 lines of code currently on my screen!) this complex conditions set got my attention:

            if ((current.isEndOfLine(style.rightMargin() + hyphWidth)) || current.isEndOfCol(realDesc) || SpecialChars::isBreak(itemText.text(a), m_columns > 1) || (current.xPos - current.maxShrink + hyphWidth) >= current.mustLineEnd)
            {
                //end of row reached - right column, end of column, break char or line must end
                if (current.isEmpty && !current.afterOverflow && !SpecialChars::isBreak(itemText.text(a), m_columns > 1))
                {
                    //no glyphs in line, so start new row
                    if (SpecialChars::isBreak(itemText.text(a), m_columns > 1))


this snippet

- is calling three times the function `SpecialChars::isBreak(itemText.text(a), m_columns > 1)`. no idea how expensive that is...
- and -- if i understand it correctly -- the last one is useless, since the preceding line checks for `!SpecialChars::isBreak(itemText.text(a), m_columns > 1)` .

but the code is so complex and not self documenting that i'm not even 100% that the last "if" will always be false!

as an example having

            auto breakAtEndOfColumn = SpecialChars::isBreak(itemText.text(a), m_columns > 1) 
            if ((current.isEndOfLine(style.rightMargin() + hyphWidth)) || current.isEndOfCol(realDesc) || breakAtEndOfColumn || (current.xPos - current.maxShrink + hyphWidth) >= current.mustLineEnd)
            {
                //end of row reached - right column, end of column, break char or line must end
                if (current.isEmpty && !current.afterOverflow && !breakAtEndOfColumn))
                {
                    //no glyphs in line, so start new row
                    if (breakAtEndOfColumn))


would already be a bit easier to parse.
(of course, i'm not at all sure that name of the new variable above is correct!.)

all this because i wanted to tell you, that i wanted to check if i can manage to get scribus to respect the tabs pasted inside prefix and suffix, i've probably even found (one of) the line(s) that make it hard, and gave up the task.

ale

2025-02-20 20:47

manager   ~0052079

To be even more clear: a user wanted to achieve the attached result and this seems not to be possible, when using the Scribus lists.
enumarte-left-aligned.png (35,060 bytes)   
enumarte-left-aligned.png (35,060 bytes)   

nitramr

2025-02-20 22:03

developer   ~0052080

Last edited: 2025-02-20 22:04

I could recreate the screenshot with a numbered list in Scribus.

1. Create a new paragraph style
2. enable numbered list (no left indent, no auto indent)
3. Add a new tab (right)
4. set fill char "dot"

Text should be formatted like "Chapter I <tab> 1"
The paragraph style will format it like "1. Chapter I..........1"

Attached file has been created with 1.7.1 svn

What I see as a missing feature is a flexible tabulator position to push the number always to the right edge of the text frame. Each time you resize text frame the numbers stay at the same position because of the absolute tabulator position.
Document-1.sla (22,279 bytes)   
<?xml version="1.0" encoding="UTF-8"?>
<SCRIBUSUTF8NEW Version="1.7.1.svn">
    <DOCUMENT ANZPAGES="1" PAGEWIDTH="595.275590551181" PAGEHEIGHT="841.889763779528" BORDERLEFT="40" BORDERRIGHT="40" BORDERTOP="40" BORDERBOTTOM="40" PRESET="0" BleedTop="0" BleedLeft="0" BleedRight="0" BleedBottom="0" ORIENTATION="0" PAGESIZE="IsoA_A04" FIRSTNUM="1" BOOK="0" AUTOSPALTEN="1" ABSTSPALTEN="11" UNITS="1" DFONT="Arial Regular" DSIZE="12" DCOL="1" DGAP="0" TabFill="" TabWidth="36" TextDistLeft="0" TextDistRight="0" TextDistBottom="0" TextDistTop="0" FirstLineOffset="1" AUTHOR="" COMMENTS="" KEYWORDS="" PUBLISHER="" DOCDATE="" DOCTYPE="" DOCFORMAT="" DOCIDENT="" DOCSOURCE="" DOCLANGINFO="" DOCRELATION="" DOCCOVER="" DOCRIGHTS="" DOCCONTRIB="" TITLE="" SUBJECT="" VHOCH="33" VHOCHSC="66" VTIEF="33" VTIEFSC="66" VKAPIT="75" BASEGRID="14.4" BASEO="0" AUTOL="100" UnderlinePos="-1" UnderlineWidth="-1" StrikeThruPos="-1" StrikeThruWidth="-1" GROUPC="1" HCMS="0" DPSo="0" DPSFo="0" DPuse="0" DPgam="0" DPbla="1" DPPr="ISO Coated v2 300% (basICColor)" DPIn="sRGB display profile (ICC v2.2)" DPInCMYK="ISO Coated v2 300% (basICColor)" DPIn2="sRGB display profile (ICC v2.2)" DPIn3="ISO Coated v2 300% (basICColor)" DISc="1" DIIm="0" ALAYER="0" LANGUAGE="en_US" AUTOMATIC="1" AUTOCHECK="0" GUIDELOCK="0" SnapToGuides="0" SnapToGrid="0" SnapToElement="0" MINGRID="20.001" MAJGRID="100.001" SHOWGRID="0" SHOWGUIDES="1" showcolborders="1" SHOWFRAME="1" SHOWControl="0" SHOWLAYERM="0" SHOWMARGIN="1" SHOWBASE="0" SHOWPICT="1" SHOWLINK="0" rulerMode="1" showrulers="1" showBleed="1" rulerXoffset="0" rulerYoffset="0" GuideRad="10" GRAB="4" POLYC="4" POLYF="0.502" POLYR="0" POLYIR="0" POLYCUR="0" POLYOCUR="0" POLYS="0" arcStartAngle="30" arcSweepAngle="300" spiralStartAngle="0" spiralEndAngle="1080" spiralFactor="1.2" AutoSave="1" AutoSaveTime="600000" AutoSaveCount="1" AutoSaveKeep="0" AUtoSaveInDocDir="1" AutoSaveDir="" ScratchBottom="508.031" ScratchLeft="1000.001" ScratchRight="1000.001" ScratchTop="508.031" GapHorizontal="0" GapVertical="1000.001" StartArrow="0" EndArrow="0" PEN="Black" BRUSH="None" PENLINE="Black" PENTEXT="Black" StrokeText="Black" TextBackGround="None" TextLineColor="None" TextBackGroundShade="100" TextLineShade="100" TextPenShade="100" TextStrokeShade="100" STIL="1" STILLINE="1" WIDTH="1" WIDTHLINE="1" PENSHADE="100" LINESHADE="100" BRUSHSHADE="100" CPICT="None" PICTSHADE="100" CSPICT="None" PICTSSHADE="100" PICTSCX="1" PICTSCY="1" PSCALE="1" PASPECT="1" EmbeddedPath="0" HalfRes="1" dispX="10.001" dispY="10.001" constrain="15" MINORC="#00ff00" MAJORC="#00ff00" GuidesColor="#000080" BaselineGridColor="#c0c0c0" renderStack="2 0 4 1 3" GridType="0" PAGEC="#ffffff" MARGC="#0000ff" RANDF="0" currentProfile="PDF 1.4" calligraphicPenFillColor="Black" calligraphicPenLineColor="Black" calligraphicPenFillColorShade="100" calligraphicPenLineColorShade="100" calligraphicPenLineWidth="1" calligraphicPenAngle="0" calligraphicPenWidth="10" calligraphicPenStyle="1">
        <CheckProfile Name="PDF 1.3" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="1" minResolution="144" maxResolution="2400" checkAnnotations="0" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="1" checkFontIsOpenType="1" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF 1.4" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="0" minResolution="144" maxResolution="2400" checkAnnotations="0" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="1" checkFontIsOpenType="1" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF 1.5" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="0" minResolution="144" maxResolution="2400" checkAnnotations="0" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="1" checkFontIsOpenType="1" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF 1.6" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="0" minResolution="144" maxResolution="2400" checkAnnotations="0" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="1" checkFontIsOpenType="0" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF/X-1a" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="1" minResolution="144" maxResolution="2400" checkAnnotations="1" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="1" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="1" checkFontIsOpenType="1" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF/X-3" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="1" minResolution="144" maxResolution="2400" checkAnnotations="1" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="1" checkFontNotEmbedded="1" checkFontIsOpenType="1" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PDF/X-4" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="0" minResolution="144" maxResolution="2400" checkAnnotations="1" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="1" checkFontNotEmbedded="1" checkFontIsOpenType="0" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <CheckProfile Name="PostScript" ignoreErrors="0" autoCheck="1" checkGlyphs="1" checkOrphans="1" checkOverflow="1" checkPictures="1" checkPartFilledImageFrames="0" checkResolution="1" checkTransparency="1" minResolution="144" maxResolution="2400" checkAnnotations="0" checkRasterPDF="1" checkForGIF="1" ignoreOffLayers="0" checkNotCMYKOrSpot="0" checkDeviceColorsAndOutputIntent="0" checkFontNotEmbedded="0" checkFontIsOpenType="0" checkAppliedMasterDifferentSide="1" checkEmptyTextFrames="1"/>
        <COLOR NAME=" Aluminum 1" SPACE="RGB" R="238" G="238" B="236"/>
        <COLOR NAME=" Aluminum 2" SPACE="RGB" R="211" G="215" B="207"/>
        <COLOR NAME=" Aluminum 3" SPACE="RGB" R="186" G="189" B="182"/>
        <COLOR NAME=" Aluminum 4" SPACE="RGB" R="136" G="138" B="133"/>
        <COLOR NAME=" Aluminum 5" SPACE="RGB" R="85" G="87" B="83"/>
        <COLOR NAME=" Aluminum 6" SPACE="RGB" R="46" G="52" B="54"/>
        <COLOR NAME=" Blue 1" SPACE="RGB" R="191" G="206" B="234"/>
        <COLOR NAME=" Blue 2" SPACE="RGB" R="67" G="116" B="183"/>
        <COLOR NAME=" Blue 3" SPACE="RGB" R="33" G="65" B="132"/>
        <COLOR NAME=" Blue 4" SPACE="RGB" R="16" G="37" B="72"/>
        <COLOR NAME=" Butter 1" SPACE="RGB" R="252" G="233" B="79"/>
        <COLOR NAME=" Butter 2" SPACE="RGB" R="237" G="212" B="0"/>
        <COLOR NAME=" Butter 3" SPACE="RGB" R="196" G="160" B="0"/>
        <COLOR NAME=" CC Green 1" SPACE="RGB" R="224" G="228" B="225"/>
        <COLOR NAME=" CC Green 2" SPACE="RGB" R="171" G="184" B="175"/>
        <COLOR NAME=" CC Green 3" SPACE="RGB" R="67" G="89" B="74"/>
        <COLOR NAME=" CC Green 4" SPACE="RGB" R="42" G="55" B="47"/>
        <COLOR NAME=" Chameleon 1" SPACE="RGB" R="138" G="226" B="52"/>
        <COLOR NAME=" Chameleon 2" SPACE="RGB" R="115" G="210" B="22"/>
        <COLOR NAME=" Chameleon 3" SPACE="RGB" R="78" G="154" B="6"/>
        <COLOR NAME=" Chocolate 1" SPACE="RGB" R="233" G="185" B="110"/>
        <COLOR NAME=" Chocolate 2" SPACE="RGB" R="193" G="125" B="17"/>
        <COLOR NAME=" Chocolate 3" SPACE="RGB" R="143" G="89" B="2"/>
        <COLOR NAME=" Gray 1" SPACE="RGB" R="237" G="237" B="238"/>
        <COLOR NAME=" Gray 2" SPACE="RGB" R="220" G="221" B="222"/>
        <COLOR NAME=" Gray 3" SPACE="RGB" R="167" G="169" B="172"/>
        <COLOR NAME=" Gray 4" SPACE="RGB" R="99" G="100" B="102"/>
        <COLOR NAME=" Gray 5" SPACE="RGB" R="57" G="56" B="57"/>
        <COLOR NAME=" Gray 6" SPACE="RGB" R="18" G="21" B="19"/>
        <COLOR NAME=" Green 1" SPACE="RGB" R="228" G="235" B="160"/>
        <COLOR NAME=" Green 2" SPACE="RGB" R="152" G="200" B="7"/>
        <COLOR NAME=" Green 3" SPACE="RGB" R="98" G="125" B="15"/>
        <COLOR NAME=" Green 4" SPACE="RGB" R="61" G="80" B="36"/>
        <COLOR NAME=" Magenta 1" SPACE="RGB" R="247" G="178" B="204"/>
        <COLOR NAME=" Magenta 2" SPACE="RGB" R="182" G="43" B="110"/>
        <COLOR NAME=" Magenta 3" SPACE="RGB" R="123" G="27" B="83"/>
        <COLOR NAME=" Magenta 4" SPACE="RGB" R="79" G="16" B="53"/>
        <COLOR NAME=" Orange 1" SPACE="RGB" R="252" G="175" B="62"/>
        <COLOR NAME=" Orange 2" SPACE="RGB" R="245" G="121" B="0"/>
        <COLOR NAME=" Orange 3" SPACE="RGB" R="206" G="92" B="0"/>
        <COLOR NAME=" Orange 4" SPACE="RGB" R="84" G="61" B="3"/>
        <COLOR NAME=" Plum 1" SPACE="RGB" R="173" G="127" B="168"/>
        <COLOR NAME=" Plum 2" SPACE="RGB" R="117" G="80" B="123"/>
        <COLOR NAME=" Plum 3" SPACE="RGB" R="92" G="53" B="102"/>
        <COLOR NAME=" Purple 1" SPACE="RGB" R="214" G="180" B="214"/>
        <COLOR NAME=" Purple 2" SPACE="RGB" R="150" G="40" B="198"/>
        <COLOR NAME=" Purple 3" SPACE="RGB" R="102" G="41" B="120"/>
        <COLOR NAME=" Purple 4" SPACE="RGB" R="66" G="25" B="79"/>
        <COLOR NAME=" Red 1" SPACE="RGB" R="250" G="180" B="153"/>
        <COLOR NAME=" Red 2" SPACE="RGB" R="209" G="56" B="20"/>
        <COLOR NAME=" Red 3" SPACE="RGB" R="145" G="43" B="16"/>
        <COLOR NAME=" Red 4" SPACE="RGB" R="90" G="28" B="14"/>
        <COLOR NAME=" Sand 1" SPACE="RGB" R="235" G="224" B="178"/>
        <COLOR NAME=" Sand 2" SPACE="RGB" R="177" G="162" B="74"/>
        <COLOR NAME=" Sand 3" SPACE="RGB" R="109" G="97" B="24"/>
        <COLOR NAME=" Sand 4" SPACE="RGB" R="68" G="65" B="31"/>
        <COLOR NAME=" Scarlet Red 1" SPACE="RGB" R="239" G="41" B="41"/>
        <COLOR NAME=" Scarlet Red 2" SPACE="RGB" R="204" G="0" B="0"/>
        <COLOR NAME=" Scarlet Red 3" SPACE="RGB" R="164" G="0" B="0"/>
        <COLOR NAME=" Sky Blue 1" SPACE="RGB" R="114" G="159" B="207"/>
        <COLOR NAME=" Sky Blue 2" SPACE="RGB" R="52" G="101" B="164"/>
        <COLOR NAME=" Sky Blue 3" SPACE="RGB" R="32" G="74" B="135"/>
        <COLOR NAME=" Yellow 1" SPACE="RGB" R="255" G="243" B="162"/>
        <COLOR NAME=" Yellow 2" SPACE="RGB" R="237" G="216" B="18"/>
        <COLOR NAME=" Yellow 3" SPACE="RGB" R="176" G="154" B="17"/>
        <COLOR NAME=" Yellow 4" SPACE="RGB" R="78" G="73" B="23"/>
        <COLOR NAME="Black" SPACE="CMYK" C="0" M="0" Y="0" K="100"/>
        <COLOR NAME="Registration" SPACE="CMYK" C="100" M="100" Y="100" K="100" Register="1"/>
        <COLOR NAME="White" SPACE="CMYK" C="0" M="0" Y="0" K="0"/>
        <HYPHEN/>
        <CHARSTYLE CNAME="Default Character Style" DefaultStyle="1" FONT="Arial Regular" FONTSIZE="12" FONTFEATURES="" FEATURES="inherit" FCOLOR="Black" FSHADE="100" HyphenWordMin="3" SCOLOR="Black" BGCOLOR="None" BGSHADE="100" SSHADE="100" TXTSHX="5" TXTSHY="-5" TXTOUT="1" TXTULP="-0.1" TXTULW="-0.1" TXTSTP="-0.1" TXTSTW="-0.1" SCALEH="100" SCALEV="100" BASEO="0" KERN="0" LANGUAGE="en_US"/>
        <STYLE NAME="Default Paragraph Style" DefaultStyle="1" ALIGN="0" DIRECTION="0" LINESPMode="0" LINESP="15" INDENT="0" RMARGIN="0" FIRST="0" VOR="0" NACH="0" ParagraphEffectOffset="0" DROP="0" DROPLIN="2" Bullet="0" Numeration="0" HyphenConsecutiveLines="2" BCOLOR="None" BSHADE="100"/>
        <STYLE NAME="TOC List" ParagraphEffectOffset="5.66929133858268" ParagraphEffectIndent="0" DROP="0" Bullet="0" Numeration="1" OpticalMarginSetId="preset_0" LANGUAGE="en_US">
            <Tabs Type="1" Pos="340" Fill="."/>
        </STYLE>
        <TableStyle NAME="Default Table Style" DefaultStyle="1" FillColor="None" FillShade="100">
            <TableBorderLeft>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderLeft>
            <TableBorderRight>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderRight>
            <TableBorderTop>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderTop>
            <TableBorderBottom>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderBottom>
        </TableStyle>
        <CellStyle NAME="Default Cell Style" DefaultStyle="1" FillColor="None" FillShade="100" LeftPadding="1" RightPadding="1" TopPadding="1" BottomPadding="1">
            <TableBorderLeft>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderLeft>
            <TableBorderRight>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderRight>
            <TableBorderTop>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderTop>
            <TableBorderBottom>
                <TableBorderLine Width="1" PenStyle="1" Color="Black" Shade="100"/>
            </TableBorderBottom>
        </CellStyle>
        <LAYERS NUMMER="0" LEVEL="0" NAME="Background" SICHTBAR="1" DRUCKEN="1" EDIT="1" SELECT="0" FLOW="1" TRANS="1" BLEND="0" OUTL="0" LAYERC="#000000"/>
        <Printer firstUse="1" toFile="0" useAltPrintCommand="0" outputSeparations="0" useSpotColors="1" useColor="1" mirrorH="0" mirrorV="0" useICC="0" doGCR="0" doClip="0" setDevParam="0" useDocBleeds="1" cropMarks="0" bleedMarks="0" registrationMarks="0" colorMarks="0" includePDFMarks="1" PSLevel="3" PrintEngine="3" markLength="20.0013" markOffset="0" BleedTop="0" BleedLeft="0" BleedRight="0" BleedBottom="0" printer="File" filename="" separationName="All" printerCommand=""/>
        <PDF firstUse="1" Thumbnails="0" Articles="0" Bookmarks="0" Compress="1" CMethod="0" Quality="0" EmbedPDF="0" MirrorH="0" MirrorV="0" Clip="0" rangeSel="0" rangeTxt="" RotateDeg="0" PresentMode="0" RecalcPic="0" FontEmbedding="0" Grayscale="0" RGBMode="1" UseProfiles="0" UseProfiles2="0" Binding="0" PicRes="300" Resolution="300" Version="14" Intent="1" Intent2="0" SolidP="sRGB display profile (ICC v2.2)" ImageP="sRGB display profile (ICC v2.2)" PrintP="ISO Coated v2 300% (basICColor)" InfoString="" BTop="0" BLeft="0" BRight="0" BBottom="0" useDocBleeds="1" cropMarks="0" bleedMarks="0" registrationMarks="0" colorMarks="0" docInfoMarks="0" markLength="20.0012598425197" markOffset="0" ImagePr="0" PassOwner="" PassUser="" Permissions="-4" Encrypt="0" UseLayers="0" UseLpi="0" UseSpotColors="1" doMultiFile="0" displayBookmarks="0" displayFullscreen="0" displayLayers="0" displayThumbs="0" hideMenuBar="0" hideToolBar="0" fitWindow="0" openAfterExport="0" PageLayout="0" openAction="">
            <LPI Color="Black" Frequency="133" Angle="45" SpotFunction="3"/>
            <LPI Color="Cyan" Frequency="133" Angle="105" SpotFunction="3"/>
            <LPI Color="Magenta" Frequency="133" Angle="75" SpotFunction="3"/>
            <LPI Color="Yellow" Frequency="133" Angle="90" SpotFunction="3"/>
        </PDF>
        <DocItemAttributes/>
        <Indexes/>
        <TablesOfContents/>
        <NotesStyles>
            <notesStyle Name="Default" Start="1" Endnotes="0" Type="Type_1_2_3" Range="0" Prefix="" Suffix=")" AutoHeight="1" AutoWidth="1" AutoRemove="1" AutoWeld="1" SuperNote="1" SuperMaster="1" MarksStyle="" NotesStyle=""/>
        </NotesStyles>
        <OpticalMarginSets>
            <Set Id="preset_0" Type="preset" Name="Default">
                <Rules>
                    <Rule Left="0" Right="0.5" Unit="7" Characters="U+003A,U+003B,U+2013,U+2033"/>
                    <Rule Left="0" Right="0.75" Unit="7" Characters="U+002C,U+002D,U+002E,U+2010,U+2032"/>
                    <Rule Left="0.25" Right="0.25" Unit="7" Characters="U+2014"/>
                    <Rule Left="0.5" Right="0" Unit="7" Characters="U+2036"/>
                    <Rule Left="0.5" Right="0.5" Unit="7" Characters="U+0022,U+0028,U+0029,U+005B,U+005D,U+00AB,U+00BB,U+201C,U+201D,U+201E,U+201F"/>
                    <Rule Left="0.75" Right="0" Unit="7" Characters="U+2035"/>
                    <Rule Left="0.75" Right="0.75" Unit="7" Characters="U+0027,U+002A,U+0060,U+007E,U+00B4,U+2018,U+2019,U+201A,U+201B,U+2039,U+203A"/>
                </Rules>
            </Set>
        </OpticalMarginSets>
        <PageSets>
            <Set Name="Single Page" FirstPage="0" Rows="1" Columns="1"/>
            <Set Name="Facing Pages" FirstPage="1" Rows="1" Columns="2">
                <PageNames Name="Left Page"/>
                <PageNames Name="Right Page"/>
            </Set>
            <Set Name="3-Fold" FirstPage="0" Rows="1" Columns="3">
                <PageNames Name="Left Page"/>
                <PageNames Name="Middle"/>
                <PageNames Name="Right Page"/>
            </Set>
            <Set Name="4-Fold" FirstPage="0" Rows="1" Columns="4">
                <PageNames Name="Left Page"/>
                <PageNames Name="Middle Left"/>
                <PageNames Name="Middle Right"/>
                <PageNames Name="Right Page"/>
            </Set>
        </PageSets>
        <Sections>
            <Section Number="0" Name="Section 1" From="0" To="0" Type="Type_1_2_3" Start="1" Reversed="0" Active="1" FillChar="0" FieldWidth="0"/>
        </Sections>
        <MASTERPAGE PAGEXPOS="1000.001" PAGEYPOS="508.031" PAGEWIDTH="595.275590551181" PAGEHEIGHT="841.889763779528" BORDERLEFT="40" BORDERRIGHT="40" BORDERTOP="40" BORDERBOTTOM="40" NUM="0" NAM="Normal" MNAM="" Size="IsoA_A04" Orientation="0" LEFT="0" PRESET="0" VerticalGuides="" HorizontalGuides="" AGhorizontalAutoGap="0" AGverticalAutoGap="0" AGhorizontalAutoCount="0" AGverticalAutoCount="0" AGhorizontalAutoRefer="0" AGverticalAutoRefer="0" AGSelection="0 0 0 0" pageEffectDuration="1" pageViewDuration="1" effectType="0" Dm="0" M="0" Di="0"/>
        <PAGE PAGEXPOS="1000.001" PAGEYPOS="508.031" PAGEWIDTH="595.275590551181" PAGEHEIGHT="841.889763779528" BORDERLEFT="40" BORDERRIGHT="40" BORDERTOP="40" BORDERBOTTOM="40" NUM="0" NAM="" MNAM="Normal" Size="IsoA_A04" Orientation="0" LEFT="0" PRESET="0" VerticalGuides="" HorizontalGuides="" AGhorizontalAutoGap="0" AGverticalAutoGap="0" AGhorizontalAutoCount="0" AGverticalAutoCount="0" AGhorizontalAutoRefer="0" AGverticalAutoRefer="0" AGSelection="0 0 0 0" pageEffectDuration="1" pageViewDuration="1" effectType="0" Dm="0" M="0" Di="0"/>
        <PAGEOBJECT XPOS="1115.52068503937" YPOS="582.75" OwnPage="0" ItemID="1236526944" PTYPE="4" WIDTH="343.22931496063" HEIGHT="444.75" FRTYPE="0" CLIPEDIT="0" PWIDTH="1" PLINEART="1" LOCALSCX="1" LOCALSCY="1" LOCALX="0" LOCALY="0" LOCALROT="0" PICART="1" SCALETYPE="1" RATIO="1" COLUMNS="1" COLGAP="0" AUTOTEXT="0" EXTRA="0" TEXTRA="0" BEXTRA="0" REXTRA="0" VAlign="0" FLOP="1" PLTSHOW="0" BASEOF="0" textPathType="0" textPathFlipped="0" path="M0 0 L343.229 0 L343.229 444.75 L0 444.75 L0 0 Z" copath="M0 0 L343.229 0 L343.229 444.75 L0 444.75 L0 0 Z" gXpos="1115.52068503937" gYpos="582.75" gWidth="0" gHeight="0" PSTYLE="TOC List" LAYER="0" NEXTITEM="-1" BACKITEM="-1">
            <StoryText>
                <DefaultStyle PARENT="TOC List"/>
                <ITEXT CH="Chapter I"/>
                <tab/>
                <ITEXT CH="1"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter II"/>
                <tab/>
                <ITEXT CH="14"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter III"/>
                <tab/>
                <ITEXT CH="25"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter IV"/>
                <tab/>
                <ITEXT CH="41"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter V"/>
                <tab/>
                <ITEXT CH="49"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter VI"/>
                <tab/>
                <ITEXT CH="59"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter VII"/>
                <tab/>
                <ITEXT CH="68"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter VIII"/>
                <tab/>
                <ITEXT CH="78"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter IX"/>
                <tab/>
                <ITEXT CH="91"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter X"/>
                <tab/>
                <ITEXT CH="98"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter XI"/>
                <tab/>
                <ITEXT CH="110"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter XII"/>
                <tab/>
                <ITEXT CH="118"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter XIII"/>
                <tab/>
                <ITEXT CH="125"/>
                <para PARENT="TOC List"/>
                <ITEXT CH="Chapter XIV"/>
                <tab/>
                <ITEXT CH="125"/>
                <para PARENT="TOC List"/>
            </StoryText>
        </PAGEOBJECT>
    </DOCUMENT>
</SCRIBUSUTF8NEW>
Document-1.sla (22,279 bytes)   

ale

2025-02-21 17:36

manager   ~0052083

I mean: the above screenshot shows the bad result.
The "Chapter"s are not aligned.

They are probably almost aligned until the ninth.
Then it gets really bad.

Sorry, I thought in the context it would have been clear.

nitramr

2025-02-21 18:59

developer   ~0052086

Ok, I misunderstood it. So what we need is a text alignment option for the numbering (left/right/center?)

With such an option, you can create a numbered list with "left indent" and "auto-indent" as I described above.

ale

2025-02-22 09:26

manager   ~0052093

yes, exactly.

i've just found this ticket about the same topic: 0013488

qirat

2026-02-26 15:50

reporter   ~0053542

I recently filed a similar ticket to request an option to align suffix. Is it easy to program so that whatever char is typed in the suffix filed is aligned, than other ways of handling this?

qirat

2026-02-26 15:50

reporter   ~0053543

suffix field*

Fahad

2026-07-06 03:59

developer   ~0053980

@nitramr @ale
as far as I understand this issue. the requirement of a text alignment option for the numbering (left/right/center) not required for suffix field only but also the text after the suffix. Am I right? or it is enough suffix alignment? if so how the text get alignment?

qirat

2026-07-06 04:55

reporter   ~0053985

Report 0017768 is either related or a duplicate.

Fahad

2026-07-09 08:14

developer   ~0054001

Hi all, here is a patch to implement this feature. It does the following:

- Removes the old Auto-Indent entirely
- Adds a new SuffixAlignment enum (SuffixAlign_Left, SuffixAlign_Center, SuffixAlign_Right)
- Add suffix align to propertywidget_pareffect & Style Manager
- Support RTL.

@ale & @qirat @nitramr please test.
suffixalignment v1.patch (76,522 bytes)   
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27701)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -1075,7 +1075,47 @@
 	return style.lineSpacing();
 }
 
+/**
+ * @brief Pre-pass to compute the maximum paragraph effect width.
+ */
+static double maxParagraphEffectWidth(PageItem* item)
+{
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
 
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum())
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1391,8 +1431,8 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		double cachedMaxParEffectWidth = -1.0;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1425,16 +1465,6 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1807,7 +1837,7 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1816,33 +1846,31 @@
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							if (cachedMaxParEffectWidth < 0.0)
+								cachedMaxParEffectWidth = maxParagraphEffectWidth(this);
 
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - cachedMaxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								indentAdjust = style.parEffectOffset() + (effectWidth - cachedMaxParEffectWidth) / 2.0;
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6679,8 +6679,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6717,6 +6715,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27701)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27701)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27701)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27701)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27701)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -45,6 +45,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27701)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27701)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -1,568 +1,575 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>PropertyWidget_ParEffectBase</class>
- <widget class="QFrame" name="PropertyWidget_ParEffectBase">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>295</width>
-    <height>274</height>
-   </rect>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <property name="spacing">
-    <number>4</number>
-   </property>
-   <property name="leftMargin">
-    <number>8</number>
-   </property>
-   <property name="topMargin">
-    <number>8</number>
-   </property>
-   <property name="rightMargin">
-    <number>8</number>
-   </property>
-   <property name="bottomMargin">
-    <number>8</number>
-   </property>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_6">
-     <item>
-      <widget class="FormWidget" name="peLabel">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="label" stdset="0">
-        <string/>
-       </property>
-       <layout class="QHBoxLayout" name="horizontalLayout_7">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="QComboBox" name="peCombo"/>
-        </item>
-       </layout>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_6">
-       <property name="orientation">
-        <enum>Qt::Orientation::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>20</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <widget class="StackedContainer" name="stackedWidget">
-     <property name="currentIndex">
-      <number>2</number>
-     </property>
-     <widget class="QWidget" name="pageDropCaps">
-      <layout class="QGridLayout" name="gridLayout">
-       <property name="leftMargin">
-        <number>0</number>
-       </property>
-       <property name="topMargin">
-        <number>4</number>
-       </property>
-       <property name="rightMargin">
-        <number>0</number>
-       </property>
-       <property name="bottomMargin">
-        <number>4</number>
-       </property>
-       <property name="horizontalSpacing">
-        <number>4</number>
-       </property>
-       <property name="verticalSpacing">
-        <number>8</number>
-       </property>
-       <item row="0" column="0">
-        <widget class="QLabel" name="dropCapLinesLabel">
-         <property name="toolTip">
-          <string>Number of lines used by the drop cap</string>
-         </property>
-         <property name="text">
-          <string>Lines:</string>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1">
-        <widget class="QSpinBox" name="dropCapLines">
-         <property name="toolTip">
-          <string>Drop Cap Lines</string>
-         </property>
-         <property name="minimum">
-          <number>2</number>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="2">
-        <spacer name="horizontalSpacer">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>40</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="pageBulletedList">
-      <layout class="QGridLayout" name="gridLayout_6" columnstretch="0,0,0,1">
-       <property name="leftMargin">
-        <number>0</number>
-       </property>
-       <property name="topMargin">
-        <number>4</number>
-       </property>
-       <property name="rightMargin">
-        <number>0</number>
-       </property>
-       <property name="bottomMargin">
-        <number>4</number>
-       </property>
-       <property name="horizontalSpacing">
-        <number>4</number>
-       </property>
-       <property name="verticalSpacing">
-        <number>8</number>
-       </property>
-       <item row="0" column="2">
-        <widget class="QPushButton" name="bulletCharTableButton">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="toolTip">
-          <string>Enhanced Character Table to choose bullet characters</string>
-         </property>
-         <property name="text">
-          <string>Char Table</string>
-         </property>
-         <property name="checkable">
-          <bool>true</bool>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="0">
-        <widget class="QLabel" name="bulletCharLabel">
-         <property name="text">
-          <string>Bullet Char(s):</string>
-         </property>
-         <property name="buddy">
-          <cstring>bulletStrEdit</cstring>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1">
-        <widget class="QComboBox" name="bulletStrEdit">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="editable">
-          <bool>true</bool>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="3">
-        <spacer name="horizontalSpacer_7">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>0</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="pageNumberedList">
-      <layout class="QGridLayout" name="gridLayout_5" columnstretch="0,0,0,0,0,0">
-       <property name="leftMargin">
-        <number>0</number>
-       </property>
-       <property name="topMargin">
-        <number>4</number>
-       </property>
-       <property name="rightMargin">
-        <number>0</number>
-       </property>
-       <property name="bottomMargin">
-        <number>4</number>
-       </property>
-       <property name="horizontalSpacing">
-        <number>4</number>
-       </property>
-       <property name="verticalSpacing">
-        <number>8</number>
-       </property>
-       <item row="2" column="5">
-        <spacer name="horizontalSpacer_4">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>0</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item row="2" column="1">
-        <widget class="QSpinBox" name="numLevelSpin">
-         <property name="minimum">
-          <number>1</number>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="0">
-        <widget class="QLabel" name="numComboLabel">
-         <property name="text">
-          <string>Set:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="3">
-        <widget class="QLabel" name="numStartLabel">
-         <property name="text">
-          <string>Start:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="0">
-        <widget class="QLabel" name="numPrefixLabel">
-         <property name="text">
-          <string>Prefix:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-         <property name="buddy">
-          <cstring>numPrefix</cstring>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="3">
-        <widget class="QLabel" name="numSuffixLabel">
-         <property name="text">
-          <string>Suffix:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-         <property name="buddy">
-          <cstring>numSuffix</cstring>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="2">
-        <spacer name="horizontalSpacer_3">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeType">
-          <enum>QSizePolicy::Policy::Fixed</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>16</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item row="1" column="0">
-        <widget class="QLabel" name="numFormatLabel">
-         <property name="text">
-          <string>Style:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-         <property name="buddy">
-          <cstring>numFormatCombo</cstring>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="0">
-        <widget class="QLabel" name="numLevelLabel">
-         <property name="text">
-          <string>Level:</string>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
-         </property>
-         <property name="buddy">
-          <cstring>numLevelSpin</cstring>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="4">
-        <widget class="QSpinBox" name="numStart">
-         <property name="minimum">
-          <number>1</number>
-         </property>
-         <property name="maximum">
-          <number>999</number>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="1">
-        <widget class="QLineEdit" name="numPrefix"/>
-       </item>
-       <item row="3" column="4">
-        <widget class="QLineEdit" name="numSuffix"/>
-       </item>
-       <item row="1" column="1" colspan="4">
-        <widget class="NumFormatCombo" name="numFormatCombo">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1" colspan="4">
-        <widget class="QComboBox" name="numComboBox">
-         <property name="currentIndex">
-          <number>-1</number>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-    </widget>
-   </item>
-   <item>
-    <widget class="QWidget" name="peGroup" native="true">
-     <layout class="QVBoxLayout" name="verticalLayout_2">
-      <property name="spacing">
-       <number>8</number>
-      </property>
-      <property name="leftMargin">
-       <number>0</number>
-      </property>
-      <property name="topMargin">
-       <number>0</number>
-      </property>
-      <property name="rightMargin">
-       <number>0</number>
-      </property>
-      <property name="bottomMargin">
-       <number>0</number>
-      </property>
-      <item>
-       <layout class="QHBoxLayout" name="horizontalLayout_5">
-        <property name="spacing">
-         <number>20</number>
-        </property>
-        <item>
-         <widget class="FormWidget" name="peOffsetLabel">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="label" stdset="0">
-           <string/>
-          </property>
-          <layout class="QHBoxLayout" name="horizontalLayout_2">
-           <property name="leftMargin">
-            <number>0</number>
-           </property>
-           <property name="topMargin">
-            <number>0</number>
-           </property>
-           <property name="rightMargin">
-            <number>0</number>
-           </property>
-           <property name="bottomMargin">
-            <number>0</number>
-           </property>
-           <item>
-            <widget class="ScrSpinBox" name="peOffset">
-             <property name="toolTip">
-              <string>Paragraph Effects characters offset</string>
-             </property>
-             <property name="minimum">
-              <double>-3000.000000000000000</double>
-             </property>
-             <property name="maximum">
-              <double>3000.000000000000000</double>
-             </property>
-            </widget>
-           </item>
-          </layout>
-         </widget>
-        </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
-          <property name="text">
-           <string>Auto-Indent</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <spacer name="horizontalSpacer_5">
-          <property name="orientation">
-           <enum>Qt::Orientation::Horizontal</enum>
-          </property>
-          <property name="sizeHint" stdset="0">
-           <size>
-            <width>0</width>
-            <height>20</height>
-           </size>
-          </property>
-         </spacer>
-        </item>
-       </layout>
-      </item>
-      <item>
-       <layout class="QHBoxLayout" name="horizontalLayout">
-        <property name="spacing">
-         <number>4</number>
-        </property>
-        <item>
-         <widget class="FormWidget" name="peCharStyleLabel">
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <property name="label" stdset="0">
-           <string/>
-          </property>
-          <layout class="QHBoxLayout" name="horizontalLayout_3">
-           <property name="leftMargin">
-            <number>0</number>
-           </property>
-           <property name="topMargin">
-            <number>0</number>
-           </property>
-           <property name="rightMargin">
-            <number>0</number>
-           </property>
-           <property name="bottomMargin">
-            <number>0</number>
-           </property>
-           <item>
-            <widget class="CharStyleComboBox" name="peCharStyleCombo">
-             <property name="toolTip">
-              <string>Choose Character Style or leave blank to use default Paragraph Style</string>
-             </property>
-            </widget>
-           </item>
-          </layout>
-         </widget>
-        </item>
-        <item>
-         <spacer name="horizontalSpacer_2">
-          <property name="orientation">
-           <enum>Qt::Orientation::Horizontal</enum>
-          </property>
-          <property name="sizeHint" stdset="0">
-           <size>
-            <width>0</width>
-            <height>20</height>
-           </size>
-          </property>
-         </spacer>
-        </item>
-       </layout>
-      </item>
-     </layout>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>FormWidget</class>
-   <extends>QWidget</extends>
-   <header>ui/widgets/form_widget.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>ScrSpinBox</class>
-   <extends>QDoubleSpinBox</extends>
-   <header location="global">ui/scrspinbox.h</header>
-  </customwidget>
-  <customwidget>
-   <class>CharStyleComboBox</class>
-   <extends>QComboBox</extends>
-   <header>ui/stylecombos.h</header>
-  </customwidget>
-  <customwidget>
-   <class>NumFormatCombo</class>
-   <extends>QComboBox</extends>
-   <header>ui/numformatcombo.h</header>
-  </customwidget>
-  <customwidget>
-   <class>StackedContainer</class>
-   <extends>QStackedWidget</extends>
-   <header>ui/widgets/stacked_container.h</header>
-   <container>1</container>
-  </customwidget>
- </customwidgets>
- <tabstops>
-  <tabstop>peCombo</tabstop>
-  <tabstop>numComboBox</tabstop>
-  <tabstop>numFormatCombo</tabstop>
-  <tabstop>numLevelSpin</tabstop>
-  <tabstop>numStart</tabstop>
-  <tabstop>numPrefix</tabstop>
-  <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
-  <tabstop>peCharStyleCombo</tabstop>
-  <tabstop>dropCapLines</tabstop>
-  <tabstop>bulletStrEdit</tabstop>
-  <tabstop>bulletCharTableButton</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PropertyWidget_ParEffectBase</class>
+ <widget class="QFrame" name="PropertyWidget_ParEffectBase">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>295</width>
+    <height>274</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>4</number>
+   </property>
+   <property name="leftMargin">
+    <number>8</number>
+   </property>
+   <property name="topMargin">
+    <number>8</number>
+   </property>
+   <property name="rightMargin">
+    <number>8</number>
+   </property>
+   <property name="bottomMargin">
+    <number>8</number>
+   </property>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_6">
+     <item>
+      <widget class="FormWidget" name="peLabel">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="label" stdset="0">
+        <string/>
+       </property>
+       <layout class="QHBoxLayout" name="horizontalLayout_7">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QComboBox" name="peCombo"/>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_6">
+       <property name="orientation">
+        <enum>Qt::Orientation::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>0</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="StackedContainer" name="stackedWidget">
+     <property name="currentIndex">
+      <number>2</number>
+     </property>
+     <widget class="QWidget" name="pageDropCaps">
+      <layout class="QGridLayout" name="gridLayout">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>4</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>4</number>
+       </property>
+       <property name="horizontalSpacing">
+        <number>4</number>
+       </property>
+       <property name="verticalSpacing">
+        <number>8</number>
+       </property>
+       <item row="0" column="0">
+        <widget class="QLabel" name="dropCapLinesLabel">
+         <property name="toolTip">
+          <string>Number of lines used by the drop cap</string>
+         </property>
+         <property name="text">
+          <string>Lines:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QSpinBox" name="dropCapLines">
+         <property name="toolTip">
+          <string>Drop Cap Lines</string>
+         </property>
+         <property name="minimum">
+          <number>2</number>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="2">
+        <spacer name="horizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Orientation::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="pageBulletedList">
+      <layout class="QGridLayout" name="gridLayout_6" columnstretch="0,0,0,1">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>4</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>4</number>
+       </property>
+       <property name="horizontalSpacing">
+        <number>4</number>
+       </property>
+       <property name="verticalSpacing">
+        <number>8</number>
+       </property>
+       <item row="0" column="2">
+        <widget class="QPushButton" name="bulletCharTableButton">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="toolTip">
+          <string>Enhanced Character Table to choose bullet characters</string>
+         </property>
+         <property name="text">
+          <string>Char Table</string>
+         </property>
+         <property name="checkable">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QLabel" name="bulletCharLabel">
+         <property name="text">
+          <string>Bullet Char(s):</string>
+         </property>
+         <property name="buddy">
+          <cstring>bulletStrEdit</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QComboBox" name="bulletStrEdit">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="editable">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="3">
+        <spacer name="horizontalSpacer_7">
+         <property name="orientation">
+          <enum>Qt::Orientation::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="pageNumberedList">
+      <layout class="QGridLayout" name="gridLayout_5" columnstretch="0,0,0,0,0,0">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>4</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>4</number>
+       </property>
+       <property name="horizontalSpacing">
+        <number>4</number>
+       </property>
+       <property name="verticalSpacing">
+        <number>8</number>
+       </property>
+       <item row="2" column="5">
+        <spacer name="horizontalSpacer_4">
+         <property name="orientation">
+          <enum>Qt::Orientation::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="2" column="1">
+        <widget class="QSpinBox" name="numLevelSpin">
+         <property name="minimum">
+          <number>1</number>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QLabel" name="numComboLabel">
+         <property name="text">
+          <string>Set:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="3">
+        <widget class="QLabel" name="numStartLabel">
+         <property name="text">
+          <string>Start:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="0">
+        <widget class="QLabel" name="numPrefixLabel">
+         <property name="text">
+          <string>Prefix:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+         <property name="buddy">
+          <cstring>numPrefix</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="3">
+        <widget class="QLabel" name="numSuffixLabel">
+         <property name="text">
+          <string>Suffix:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+         <property name="buddy">
+          <cstring>numSuffix</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="2">
+        <spacer name="horizontalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Orientation::Horizontal</enum>
+         </property>
+         <property name="sizeType">
+          <enum>QSizePolicy::Policy::Fixed</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>16</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="1" column="0">
+        <widget class="QLabel" name="numFormatLabel">
+         <property name="text">
+          <string>Style:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+         <property name="buddy">
+          <cstring>numFormatCombo</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QLabel" name="numLevelLabel">
+         <property name="text">
+          <string>Level:</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+         </property>
+         <property name="buddy">
+          <cstring>numLevelSpin</cstring>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="4">
+        <widget class="QSpinBox" name="numStart">
+         <property name="minimum">
+          <number>1</number>
+         </property>
+         <property name="maximum">
+          <number>999</number>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="1">
+        <widget class="QLineEdit" name="numPrefix"/>
+       </item>
+       <item row="3" column="4">
+        <widget class="QLineEdit" name="numSuffix"/>
+       </item>
+       <item row="1" column="1" colspan="4">
+        <widget class="NumFormatCombo" name="numFormatCombo">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1" colspan="4">
+        <widget class="QComboBox" name="numComboBox">
+         <property name="currentIndex">
+          <number>-1</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="peGroup" native="true">
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <property name="spacing">
+       <number>8</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_5">
+        <property name="spacing">
+         <number>20</number>
+        </property>
+        <item>
+         <widget class="FormWidget" name="peOffsetLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="label" stdset="0">
+           <string/>
+          </property>
+          <layout class="QHBoxLayout" name="horizontalLayout_2">
+           <property name="leftMargin">
+            <number>0</number>
+           </property>
+           <property name="topMargin">
+            <number>0</number>
+           </property>
+           <property name="rightMargin">
+            <number>0</number>
+           </property>
+           <property name="bottomMargin">
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="ScrSpinBox" name="peOffset">
+             <property name="toolTip">
+              <string>Paragraph Effects characters offset</string>
+             </property>
+             <property name="minimum">
+              <double>-3000.000000000000000</double>
+             </property>
+             <property name="maximum">
+              <double>3000.000000000000000</double>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
+          <property name="text">
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_5">
+          <property name="orientation">
+           <enum>Qt::Orientation::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>0</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <item>
+         <widget class="FormWidget" name="peCharStyleLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="label" stdset="0">
+           <string/>
+          </property>
+          <layout class="QHBoxLayout" name="horizontalLayout_3">
+           <property name="leftMargin">
+            <number>0</number>
+           </property>
+           <property name="topMargin">
+            <number>0</number>
+           </property>
+           <property name="rightMargin">
+            <number>0</number>
+           </property>
+           <property name="bottomMargin">
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="CharStyleComboBox" name="peCharStyleCombo">
+             <property name="toolTip">
+              <string>Choose Character Style or leave blank to use default Paragraph Style</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Orientation::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>0</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>FormWidget</class>
+   <extends>QWidget</extends>
+   <header>ui/widgets/form_widget.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>ScrSpinBox</class>
+   <extends>QDoubleSpinBox</extends>
+   <header location="global">ui/scrspinbox.h</header>
+  </customwidget>
+  <customwidget>
+   <class>CharStyleComboBox</class>
+   <extends>QComboBox</extends>
+   <header>ui/stylecombos.h</header>
+  </customwidget>
+  <customwidget>
+   <class>NumFormatCombo</class>
+   <extends>QComboBox</extends>
+   <header>ui/numformatcombo.h</header>
+  </customwidget>
+  <customwidget>
+   <class>StackedContainer</class>
+   <extends>QStackedWidget</extends>
+   <header>ui/widgets/stacked_container.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>peCombo</tabstop>
+  <tabstop>numComboBox</tabstop>
+  <tabstop>numFormatCombo</tabstop>
+  <tabstop>numLevelSpin</tabstop>
+  <tabstop>numStart</tabstop>
+  <tabstop>numPrefix</tabstop>
+  <tabstop>numSuffix</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
+  <tabstop>peCharStyleCombo</tabstop>
+  <tabstop>dropCapLines</tabstop>
+  <tabstop>bulletStrEdit</tabstop>
+  <tabstop>bulletCharTableButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27701)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27701)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27701)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27701)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27701)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignment v1.patch (76,522 bytes)   
suffix-align.png (75,581 bytes)   
suffix-align.png (75,581 bytes)   

qirat

2026-07-09 12:19

reporter   ~0054002

I tested, @Fahad. It seems even selecting "right" does not align the suffix perfectly. See attached screenshot (notice the guideline).

Would you kindly give the patch a try that I attached to issue 0017768 ?

It has 3 options (in case suffix is not used) as you have and a 4th option "Suffix" which seem to align it better. Your thoughts are welcome.
align_suffix_test.png (59,655 bytes)   
align_suffix_test.png (59,655 bytes)   

qirat

2026-07-09 13:10

reporter   ~0054003

I didnt know that about auto-ubdent UI. Here is the updated one.

I do not know much of the coding side. So kindly take that as a grain of salt but this works great for me.
list-marker-alignment-v1.2-r27703-remove-autoindent-ui.patch (44,571 bytes)   
Index: scribus/pageitem.cpp
===================================================================
--- scribus/pageitem.cpp	(revision 27703)
+++ scribus/pageitem.cpp	(working copy)
@@ -6998,7 +6998,19 @@
 		return;
 	}
 
-	int pos = is->getInt("POS");
+	// Paragraph-style undo states used to be anchored only by raw text
+	// positions. Numbered/bulleted-list marker characters can be inserted or
+	// removed while applying paragraph styles, so those positions may shift
+	// before undo/redo is replayed. Prefer the stored paragraph index and
+	// resolve it against the current story; keep POS as a fallback for older
+	// undo states.
+	int pos = qBound(0, is->getInt("POS"), itemText.length());
+	if (is->contains("PARA"))
+	{
+		const int maxPara = static_cast<int>(itemText.nrOfParagraphs());
+		const int para = qBound(0, is->getInt("PARA"), maxPara);
+		pos = itemText.endOfParagraph(static_cast<uint>(para));
+	}
 	if (isUndo)
 	{
 		itemText.eraseStyle(pos, is->getNewState());

Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27703)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -24,8 +24,10 @@
 #include "pageitem_textframe.h"
 
 #include <QDebug>
+#include <QHash>
 #include <QList>
 #include <QTransform>
+#include <QVector>
 #include <QPalette>
 #include <QPoint>
 #include <QPolygon>
@@ -76,6 +78,65 @@
 
 using namespace std;
 
+namespace
+{
+	struct NumberedListMarkerMetrics
+	{
+		double width { 0.0 };
+		double suffixAnchor { 0.0 };
+		double boxWidth { 0.0 };
+		double alignShift { 0.0 };
+	};
+
+	QString numberedListMarkerAlignmentKey(const ParagraphStyle& style)
+	{
+		return QString("%1\x1f%2\x1f%3\x1f%4\x1f%5")
+			.arg(style.numName())
+			.arg(style.numLevel())
+			.arg(style.numFormat())
+			.arg(style.numPrefix())
+			.arg(style.numSuffix());
+	}
+
+	double numberedListMarkerClusterWidth(const GlyphCluster& cluster)
+	{
+		double width = 0.0;
+		for (const GlyphLayout& glyph : cluster.glyphs())
+			width += glyph.xadvance * cluster.scaleH();
+		return width;
+	}
+
+	NumberedListMarkerMetrics numberedListMarkerMetrics(const QList<GlyphCluster>& glyphClusters, int markerStart, int markerEnd, int suffixLength)
+	{
+		NumberedListMarkerMetrics metrics;
+		QVector<double> glyphOrigins;
+		int glyphCount = 0;
+
+		for (int i = markerStart; i < markerEnd; ++i)
+			glyphCount += glyphClusters[i].glyphs().count();
+		glyphOrigins.reserve(glyphCount);
+
+		for (int i = markerStart; i < markerEnd; ++i)
+		{
+			const GlyphCluster& cluster = glyphClusters[i];
+			for (const GlyphLayout& glyph : cluster.glyphs())
+			{
+				glyphOrigins.append(metrics.width + (glyph.xoffset * cluster.scaleH()));
+				metrics.width += glyph.xadvance * cluster.scaleH();
+			}
+		}
+
+		metrics.suffixAnchor = metrics.width;
+		if (suffixLength > 0 && !glyphOrigins.isEmpty())
+		{
+			const int suffixGlyphIndex = qMax(0, glyphOrigins.count() - suffixLength);
+			metrics.suffixAnchor = glyphOrigins.at(suffixGlyphIndex);
+		}
+
+		return metrics;
+	}
+}
+
 PageItem_TextFrame::PageItem_TextFrame(ScribusDoc *pa, double x, double y, double w, double h, double w2, const QString& fill, const QString& outline)
 	: PageItem(pa, PageItem::TextFrame, x, y, w, h, w2, fill, outline)
 {
@@ -1330,6 +1391,77 @@
 		ShapedTextFeed shapedText(&itemText, firstInFrame(), context);
 
 		QList<GlyphCluster> glyphClusters; // = textShaper.shape();
+		QHash<int, NumberedListMarkerMetrics> numberedListMarkerMetricsByPos;
+		bool hasAlignedNumberedListMarkers = false;
+		for (int pos = 0; pos < itLen; ++pos)
+		{
+			if (!itemText.isBlockStart(pos))
+				continue;
+			const ParagraphStyle& blockStyle = itemText.paragraphStyle(pos);
+			if (blockStyle.hasNum() && blockStyle.numAlignment() != ParagraphStyle::NumLeftAligned)
+			{
+				hasAlignedNumberedListMarkers = true;
+				break;
+			}
+		}
+		if (hasAlignedNumberedListMarkers)
+		{
+			for (int clusterIndex = 0; shapedText.haveMoreText(clusterIndex, glyphClusters); ++clusterIndex)
+			{
+			}
+
+			QHash<QString, double> maxMarkerWidthForList;
+			QHash<QString, double> maxSuffixAnchorForList;
+			QHash<int, QString> markerListKey;
+			for (int clusterIndex = 0; clusterIndex < glyphClusters.count();)
+			{
+				const int markerPos = glyphClusters[clusterIndex].firstChar();
+				const ParagraphStyle& blockStyle = itemText.paragraphStyle(markerPos);
+				if (!itemText.isBlockStart(markerPos) || !blockStyle.hasNum() || blockStyle.numAlignment() == ParagraphStyle::NumLeftAligned)
+				{
+					++clusterIndex;
+					continue;
+				}
+
+				int markerEnd = clusterIndex;
+				while (markerEnd < glyphClusters.count() && glyphClusters[markerEnd].firstChar() == markerPos)
+					++markerEnd;
+
+				NumberedListMarkerMetrics metrics = numberedListMarkerMetrics(glyphClusters, clusterIndex, markerEnd, blockStyle.numSuffix().length());
+				const QString listKey = numberedListMarkerAlignmentKey(blockStyle);
+				markerListKey.insert(markerPos, listKey);
+				numberedListMarkerMetricsByPos.insert(markerPos, metrics);
+				maxMarkerWidthForList[listKey] = qMax(maxMarkerWidthForList.value(listKey, 0.0), metrics.width);
+				maxSuffixAnchorForList[listKey] = qMax(maxSuffixAnchorForList.value(listKey, 0.0), metrics.suffixAnchor);
+				clusterIndex = markerEnd;
+			}
+
+			QHash<QString, double> markerBoxWidthForList;
+			for (auto it = markerListKey.cbegin(); it != markerListKey.cend(); ++it)
+			{
+				const int markerPos = it.key();
+				const QString& listKey = it.value();
+				const ParagraphStyle& blockStyle = itemText.paragraphStyle(markerPos);
+				NumberedListMarkerMetrics metrics = numberedListMarkerMetricsByPos.value(markerPos);
+				if (blockStyle.numAlignment() == ParagraphStyle::NumSuffixAligned)
+					metrics.alignShift = qMax(0.0, maxSuffixAnchorForList.value(listKey, metrics.suffixAnchor) - metrics.suffixAnchor);
+				markerBoxWidthForList[listKey] = qMax(markerBoxWidthForList.value(listKey, 0.0), metrics.width + metrics.alignShift);
+				numberedListMarkerMetricsByPos.insert(markerPos, metrics);
+			}
+
+			for (auto it = markerListKey.cbegin(); it != markerListKey.cend(); ++it)
+			{
+				const int markerPos = it.key();
+				const QString& listKey = it.value();
+				const ParagraphStyle& blockStyle = itemText.paragraphStyle(markerPos);
+				NumberedListMarkerMetrics metrics = numberedListMarkerMetricsByPos.value(markerPos);
+				if (blockStyle.numAlignment() == ParagraphStyle::NumSuffixAligned)
+					metrics.boxWidth = markerBoxWidthForList.value(listKey, metrics.width + metrics.alignShift);
+				else
+					metrics.boxWidth = maxMarkerWidthForList.value(listKey, metrics.width);
+				numberedListMarkerMetricsByPos.insert(markerPos, metrics);
+			}
+		}
 		// std::sort(glyphClusters.begin(), glyphClusters.end(), logicalGlyphRunComp);
 
 		LineControl current(m_width, m_height, m_textDistanceMargins, lineCorr, m_Doc, context, columnWidth(), m_columnGap);
@@ -1826,6 +1958,8 @@
 									break;
 								effectWidth += glyph.width();
 							}
+							if (BulNumMode && style.hasNum() && numberedListMarkerMetricsByPos.contains(a))
+								effectWidth = numberedListMarkerMetricsByPos.value(a).boxWidth;
 
 							if (style.direction() == ParagraphStyle::RTL)
 							{
@@ -2440,7 +2574,34 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				double parEffectExtraWidth = style.parEffectOffset();
+				double markerAlignShift = 0.0;
+				if (BulNumMode && style.hasNum() && numberedListMarkerMetricsByPos.contains(a))
+				{
+					const bool lastMarkerCluster = (i + 1 >= glyphClusters.count()) || (glyphClusters[i + 1].firstChar() != a);
+					if (lastMarkerCluster)
+					{
+						double markerWidth = 0.0;
+						for (int j = 0; j < current.glyphs.size(); ++j)
+						{
+							const GlyphCluster& currentGlyph = current.glyphs[j];
+							if (currentGlyph.firstChar() == a)
+								markerWidth += numberedListMarkerClusterWidth(currentGlyph);
+						}
+
+						const NumberedListMarkerMetrics markerMetrics = numberedListMarkerMetricsByPos.value(a);
+						const double markerBoxWidth = markerMetrics.boxWidth;
+						const double markerAlignPad = qMax(0.0, markerBoxWidth - markerWidth);
+						parEffectExtraWidth += markerAlignPad;
+						if (style.numAlignment() == ParagraphStyle::NumRightAligned)
+							markerAlignShift = markerAlignPad;
+						else if (style.numAlignment() == ParagraphStyle::NumCentered)
+							markerAlignShift = markerAlignPad / 2.0;
+						else if (style.numAlignment() == ParagraphStyle::NumSuffixAligned)
+							markerAlignShift = markerMetrics.alignShift;
+					}
+				}
+				current.xPos += parEffectExtraWidth;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2454,14 +2615,23 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += parEffectExtraWidth;
+				if (markerAlignShift != 0.0)
+				{
+					for (int j = 0; j < current.glyphs.size(); ++j)
+					{
+						GlyphCluster& currentGlyph = current.glyphs[j];
+						if (currentGlyph.firstChar() == a)
+							currentGlyph.xoffset += markerAlignShift;
+					}
+				}
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += parEffectExtraWidth;
 
 				if (DropCmode)
 				{

Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3163,6 +3163,10 @@
 	if (attrs.hasAttribute(NumerationFormat))
 		newStyle.setNumFormat(attrs.valueAsInt(NumerationFormat));
 
+	static const QString NumerationAlignment("NumerationAlignment");
+	if (attrs.hasAttribute(NumerationAlignment))
+		newStyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt(NumerationAlignment)));
+
 	static const QString NumerationLevel("NumerationLevel");
 	if (attrs.hasAttribute(NumerationLevel))
 		newStyle.setNumLevel(attrs.valueAsInt(NumerationLevel));
@@ -5515,6 +5519,8 @@
 		pstyle.setNumName(attrs.valueAsString("NumerationName"));
 	if (attrs.hasAttribute("NumerationFormat"))
 		pstyle.setNumFormat(attrs.valueAsInt("NumerationFormat"));
+	if (attrs.hasAttribute("NumerationAlignment"))
+		pstyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt("NumerationAlignment")));
 	if (attrs.hasAttribute("NumerationLevel"))
 		pstyle.setNumLevel(attrs.valueAsInt("NumerationLevel"));
 	if (attrs.hasAttribute("NumerationStart"))

Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -856,6 +856,8 @@
 		docu.writeAttribute("Numeration", static_cast<int>(style.hasNum()));
 	if ( ! style.isInhNumFormat())
 		docu.writeAttribute("NumerationFormat", style.numFormat());
+	if ( ! style.isInhNumAlignment())
+		docu.writeAttribute("NumerationAlignment", static_cast<int>(style.numAlignment()));
 	if ( ! style.isInhNumName())
 		docu.writeAttribute("NumerationName", style.numName());
 	if ( ! style.isInhNumLevel())

Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3171,6 +3171,10 @@
 	if (attrs.hasAttribute(NumerationFormat))
 		newStyle.setNumFormat(attrs.valueAsInt(NumerationFormat));
 
+	static const QString NumerationAlignment("NumerationAlignment");
+	if (attrs.hasAttribute(NumerationAlignment))
+		newStyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt(NumerationAlignment)));
+
 	static const QString NumerationLevel("NumerationLevel");
 	if (attrs.hasAttribute(NumerationLevel))
 		newStyle.setNumLevel(attrs.valueAsInt(NumerationLevel));
@@ -5585,6 +5589,8 @@
 		pstyle.setNumName(attrs.valueAsString("NumerationName"));
 	if (attrs.hasAttribute("NumerationFormat"))
 		pstyle.setNumFormat(attrs.valueAsInt("NumerationFormat"));
+	if (attrs.hasAttribute("NumerationAlignment"))
+		pstyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt("NumerationAlignment")));
 	if (attrs.hasAttribute("NumerationLevel"))
 		pstyle.setNumLevel(attrs.valueAsInt("NumerationLevel"));
 	if (attrs.hasAttribute("NumerationStart"))

Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -858,6 +858,8 @@
 		docu.writeAttribute("Numeration", static_cast<int>(style.hasNum()));
 	if (!style.isInhNumFormat())
 		docu.writeAttribute("NumerationFormat", style.numFormat());
+	if (!style.isInhNumAlignment())
+		docu.writeAttribute("NumerationAlignment", static_cast<int>(style.numAlignment()));
 	if (!style.isInhNumName())
 		docu.writeAttribute("NumerationName", style.numName());
 	if (!style.isInhNumLevel())

Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3679,6 +3679,10 @@
 	if (attrs.hasAttribute(NumerationFormat))
 		newStyle.setNumFormat(attrs.valueAsInt(NumerationFormat));
 
+	static const QString NumerationAlignment("NumerationAlignment");
+	if (attrs.hasAttribute(NumerationAlignment))
+		newStyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt(NumerationAlignment)));
+
 	static const QString NumerationLevel("NumerationLevel");
 	if (attrs.hasAttribute(NumerationLevel))
 		newStyle.setNumLevel(attrs.valueAsInt(NumerationLevel));
@@ -6709,6 +6713,8 @@
 		pstyle.setNumName(attrs.valueAsString("NumerationName"));
 	if (attrs.hasAttribute("NumerationFormat"))
 		pstyle.setNumFormat(attrs.valueAsInt("NumerationFormat"));
+	if (attrs.hasAttribute("NumerationAlignment"))
+		pstyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(attrs.valueAsInt("NumerationAlignment")));
 	if (attrs.hasAttribute("NumerationLevel"))
 		pstyle.setNumLevel(attrs.valueAsInt("NumerationLevel"));
 	if (attrs.hasAttribute("NumerationStart"))

Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27703)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -860,6 +860,8 @@
 		docu.writeAttribute("Numeration", static_cast<int>(style.hasNum()));
 	if (!style.isInhNumFormat())
 		docu.writeAttribute("NumerationFormat", style.numFormat());
+	if (!style.isInhNumAlignment())
+		docu.writeAttribute("NumerationAlignment", static_cast<int>(style.numAlignment()));
 	if (!style.isInhNumName())
 		docu.writeAttribute("NumerationName", style.numName());
 	if (!style.isInhNumLevel())

Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27703)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9771,6 +9771,7 @@
 						auto *is = new ScOldNewState<ParagraphStyle>(Um::SetStyle);
 						is->set("SET_PARASTYLE");
 						is->set("POS", pos);
+						is->set("PARA", static_cast<int>(currItem->itemText.nrOfParagraph(pos)));
 						is->setStates(currItem->itemText.paragraphStyle(pos), newStyle);
 						m_undoManager->action(currItem, is);
 					}
@@ -9784,6 +9785,7 @@
 				auto *is = new ScOldNewState<ParagraphStyle>(Um::SetStyle);
 				is->set("SET_PARASTYLE");
 				is->set("POS", stop);
+				is->set("PARA", static_cast<int>(currItem->itemText.nrOfParagraph(stop)));
 				is->setStates(currItem->itemText.paragraphStyle(stop), newStyle2);
 				m_undoManager->action(currItem, is);
 			}
@@ -9956,6 +9958,7 @@
 						auto *is = new ScOldNewState<ParagraphStyle>(Um::SetStyle);
 						is->set("APPLY_PARASTYLE");
 						is->set("POS", pos);
+						is->set("PARA", static_cast<int>(currItem->itemText.nrOfParagraph(pos)));
 						is->setStates(currItem->itemText.paragraphStyle(pos), newStyle);
 						m_undoManager->action(currItem, is);
 					}
@@ -9967,6 +9970,7 @@
 				auto *is = new ScOldNewState<ParagraphStyle>(Um::SetStyle);
 				is->set("APPLY_PARASTYLE");
 				is->set("POS", stop);
+				is->set("PARA", static_cast<int>(currItem->itemText.nrOfParagraph(stop)));
 				is->setStates(currItem->itemText.paragraphStyle(stop), newStyle);
 				m_undoManager->action(currItem, is);
 			}

Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27703)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -47,6 +47,7 @@
 ATTRDEF(bool, hasNum, HasNum, false)
 ATTRDEF(QString, numName, NumName, "")
 ATTRDEF(int, numFormat, NumFormat, 0)
+ATTRDEF(ParagraphStyle::NumAlignment, numAlignment, NumAlignment, ParagraphStyle::NumLeftAligned)
 ATTRDEF(QString, numPrefix, NumPrefix, "")
 ATTRDEF(QString, numSuffix, NumSuffix, ".")
 ATTRDEF(int, numLevel, NumLevel, 0)

Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27703)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -266,6 +266,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::NumAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(const QList<ParagraphStyle::TabRecord> & )
 {
 	return "dummy";
@@ -356,6 +361,12 @@
 	return parseEnum<ParagraphStyle::LineSpacingMode>(str);
 }
 
+template<>
+ParagraphStyle::NumAlignment parse<ParagraphStyle::NumAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::NumAlignment>(str);
+}
+
 
 using Tablist = QList<ParagraphStyle::TabRecord>;
 

Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27703)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -50,6 +50,14 @@
 		RTL  = 1
 	};
 
+	enum NumAlignment
+	{
+		NumLeftAligned    = 0,
+		NumCentered       = 1,
+		NumRightAligned   = 2,
+		NumSuffixAligned  = 3
+	};
+
 	enum OpticalMarginType
 	{
 		OM_None  = 0,

Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27703)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -19,6 +19,7 @@
 #include "scribusapp.h"
 #include "scribusdoc.h"
 #include "selection.h"
+#include "styles/paragraphstyle.h"
 #include "util.h"
 
 PropertyWidget_ParEffect::PropertyWidget_ParEffect(QWidget *parent) : QFrame(parent)
@@ -31,6 +32,7 @@
 	if (m_doc)
 		peCharStyleCombo->updateStyleList();
 	fillBulletStrEditCombo();
+	fillNumAlignmentCombo();
 
 	numStart->setMinimum(1);
 	numStart->setMaximum(9999);
@@ -185,6 +187,10 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+
+	const bool isNumberedList = (id == 2);
+	numAlignmentLabel->setVisible(isNumberedList);
+	numAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -198,6 +204,21 @@
 		bulletStrEdit->setEditText(QChar(0x2022));
 }
 
+void PropertyWidget_ParEffect::fillNumAlignmentCombo()
+{
+	QSignalBlocker sb(numAlignmentCombo);
+	const int currentData = numAlignmentCombo->currentData().toInt();
+	numAlignmentCombo->clear();
+	numAlignmentCombo->addItem(tr("Left"), static_cast<int>(ParagraphStyle::NumLeftAligned));
+	numAlignmentCombo->addItem(tr("Center"), static_cast<int>(ParagraphStyle::NumCentered));
+	numAlignmentCombo->addItem(tr("Right"), static_cast<int>(ParagraphStyle::NumRightAligned));
+	numAlignmentCombo->addItem(tr("Suffix"), static_cast<int>(ParagraphStyle::NumSuffixAligned));
+	int index = numAlignmentCombo->findData(currentData);
+	if (index < 0)
+		index = 0;
+	numAlignmentCombo->setCurrentIndex(index);
+}
+
 void PropertyWidget_ParEffect::fillPECombo()
 {
 	QSignalBlocker sb(peCombo);
@@ -229,8 +250,8 @@
 	QSignalBlocker blocker7(numPrefix);
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
+	QSignalBlocker blockerD(numAlignmentCombo);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
 
 	if (newPStyle.hasDropCap())
@@ -266,10 +287,13 @@
 	numPrefix->setText(newPStyle.numPrefix());
 	numSuffix->setText(newPStyle.numSuffix());
 	numStart->setValue(newPStyle.numStart());
+	int numAlignmentIndex = numAlignmentCombo->findData(static_cast<int>(newPStyle.numAlignment()));
+	if (numAlignmentIndex < 0)
+		numAlignmentIndex = 0;
+	numAlignmentCombo->setCurrentIndex(numAlignmentIndex);
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
 	if (oldPeComboIndex != peCombo->currentIndex())
@@ -284,11 +308,11 @@
 	connect(numComboBox, SIGNAL(textActivated(QString)), this, SLOT(handleNumName(QString)), Qt::UniqueConnection);
 	connect(numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(handleNumLevel(int)), Qt::UniqueConnection);
 	connect(numFormatCombo, SIGNAL(activated(int)), this, SLOT(handleNumFormat(int)), Qt::UniqueConnection);
+	connect(numAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleNumAlignment(int)), Qt::UniqueConnection);
 	connect(numPrefix, SIGNAL(textChanged(QString)), this, SLOT(handleNumPrefix(QString)), Qt::UniqueConnection);
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -300,11 +324,11 @@
 	disconnect(numComboBox, SIGNAL(textActivated(QString)), this, SLOT(handleNumName(QString)));
 	disconnect(numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(handleNumLevel(int)));
 	disconnect(numFormatCombo, SIGNAL(activated(int)), this, SLOT(handleNumFormat(int)));
+	disconnect(numAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleNumAlignment(int)));
 	disconnect(numPrefix, SIGNAL(textChanged(QString)), this, SLOT(handleNumPrefix(QString)));
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -387,6 +411,7 @@
 		newStyle.setHasNum(true);
 		newStyle.setNumName(numComboBox->currentText());
 		newStyle.setNumFormat(numFormatCombo->currentFormat());
+		newStyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(numAlignmentCombo->currentData().toInt()));
 		newStyle.setNumLevel(numLevelSpin->value() - 1);
 		newStyle.setNumStart(numStart->value());
 		newStyle.setNumPrefix(numPrefix->text());
@@ -404,7 +429,6 @@
 		newStyle.setHasNum(false);
 	}
 	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -519,30 +543,30 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handleNumStart(int start)
+void PropertyWidget_ParEffect::handleNumAlignment(int)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setNumStart(start);
+	newStyle.setNumAlignment(static_cast<ParagraphStyle::NumAlignment>(numAlignmentCombo->currentData().toInt()));
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEOffset(double offset)
+void PropertyWidget_ParEffect::handleNumStart(int start)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectOffset(offset / m_unitRatio);
+	newStyle.setNumStart(start);
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handlePEOffset(double offset)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	newStyle.setParEffectOffset(offset / m_unitRatio);
 	handleChanges(m_item, newStyle);
 }
 
@@ -587,6 +611,7 @@
 void PropertyWidget_ParEffect::languageChange()
 {
 	fillPECombo();
+	fillNumAlignmentCombo();
 	retranslateUi(this);
 }
 

Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27703)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -64,9 +64,9 @@
 	void handleNumLevel(int);
 	void handleNumPrefix(const QString&);
 	void handleNumSuffix(const QString&);
+	void handleNumAlignment(int);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
 	void handlePECharStyle(const QString&);
 
 private slots:
@@ -78,6 +78,7 @@
 	void closeEnhanced(bool show = false);
 	void setType(int id);
 	void fillBulletStrEditCombo();
+	void fillNumAlignmentCombo();
 	void fillPECombo();
 
 signals:

Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27703)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -371,6 +371,7 @@
          </property>
         </widget>
        </item>
+
       </layout>
      </widget>
     </widget>
@@ -439,9 +440,25 @@
          </widget>
         </item>
         <item>
-         <widget class="QCheckBox" name="peIndent">
+         <widget class="QLabel" name="numAlignmentLabel">
+          <property name="toolTip">
+           <string>Align the generated list marker inside its marker area</string>
+          </property>
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Marker alignment:</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+          </property>
+          <property name="buddy">
+           <cstring>numAlignmentCombo</cstring>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="numAlignmentCombo">
+          <property name="toolTip">
+           <string>Use Suffix to align suffixes such as dots or parentheses in 1., 10., 100.</string>
           </property>
          </widget>
         </item>
@@ -557,7 +574,7 @@
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
   <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+  <tabstop>numAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>

Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27703)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -6,6 +6,7 @@
 */
 
 #include <QEvent>
+#include <QSignalBlocker>
 
 
 #include "iconmanager.h"
@@ -13,6 +14,7 @@
 #include "smpstylewidget.h"
 #include "scribus.h"
 #include "scribusapp.h"
+#include "styles/paragraphstyle.h"
 #include "units.h"
 #include "util.h"
 #include "ui/charselectenhanced.h"
@@ -70,6 +72,7 @@
 	numSuffix->setMaxLength(5);
 	numSuffix->setMaximumWidth(QFontMetrics(numSuffix->font()).averageCharWidth() * 7);
 	fillNumRestartCombo();
+	fillNumAlignmentCombo();
 	dropCapLines->setMinimum(2);
 	dropCapLines->setMaximum(99);
 
@@ -152,6 +155,7 @@
 	int  oldNumRestartIndex = numRestartCombo->currentIndex();
 	bool numRestartComboBlocked = numRestartCombo->blockSignals(true);
 	fillNumRestartCombo();
+	fillNumAlignmentCombo();
 	numRestartCombo->setCurrentIndex(oldNumRestartIndex);
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
@@ -303,8 +307,6 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -320,6 +322,8 @@
 			numComboBox->setParentItem(0);
 		numFormatCombo->setCurrentFormat((NumFormat) pstyle->numFormat());
 		numFormatCombo->setParentFormat((NumFormat) parent->numFormat());
+		numAlignmentCombo->setCurrentItemByData(static_cast<int>(pstyle->numAlignment()), pstyle->isInhNumAlignment());
+		numAlignmentCombo->setParentItem(numAlignmentCombo->getItemIndexForData(static_cast<int>(parent->numAlignment())));
 		numLevelSpin->setValue(pstyle->numLevel() +1, pstyle->isInhNumLevel());
 		NumStruct * numS = m_Doc->numerations.value(pstyle->numName());
 		if (numS)
@@ -356,7 +360,6 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
 		parentParEffectsButton->hide();
 		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -369,6 +372,7 @@
 		numComboBox->setCurrentItem(numComboBox->findText(numName));
 		numNewLineEdit->clear();
 		numFormatCombo->setCurrentFormat((NumFormat) pstyle->numFormat());
+		numAlignmentCombo->setCurrentItemByData(static_cast<int>(pstyle->numAlignment()));
 		numLevelSpin->setValue(pstyle->numLevel()+1);
 		NumStruct * numS = m_Doc->numerations.value(pstyle->numName());
 		if (numS)
@@ -981,6 +985,21 @@
 	numComboBox->setCurrentItem(0);
 }
 
+void SMPStyleWidget::fillNumAlignmentCombo()
+{
+	QSignalBlocker sb(numAlignmentCombo);
+	int currentData = numAlignmentCombo->currentData().toInt();
+	numAlignmentCombo->clear();
+	numAlignmentCombo->addItem(tr("Left"), static_cast<int>(ParagraphStyle::NumLeftAligned));
+	numAlignmentCombo->addItem(tr("Center"), static_cast<int>(ParagraphStyle::NumCentered));
+	numAlignmentCombo->addItem(tr("Right"), static_cast<int>(ParagraphStyle::NumRightAligned));
+	numAlignmentCombo->addItem(tr("Suffix"), static_cast<int>(ParagraphStyle::NumSuffixAligned));
+	int index = numAlignmentCombo->findData(currentData);
+	if (index < 0)
+		index = 0;
+	numAlignmentCombo->setCurrentIndex(index);
+}
+
 void SMPStyleWidget::fillNumRestartCombo()
 {
 	numRestartCombo->clear();
@@ -1013,6 +1032,10 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 
+		const bool isNumberedList = (id == 3);
+		numAlignmentLabel->setVisible(isNumberedList);
+		numAlignmentCombo->setVisible(isNumberedList);
+
 		if (id == 2) // Bullet List
 		{
 			if (bulletStrEdit->currentText().isEmpty())
@@ -1029,6 +1052,8 @@
 		stackedWidget->setVisible(false);
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
+		numAlignmentLabel->setVisible(false);
+		numAlignmentCombo->setVisible(false);
 	}
 }
 
@@ -1095,6 +1120,16 @@
 
 void SMPStyleWidget::showNumeration(const QList<ParagraphStyle *> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
 {
+	ParagraphStyle::NumAlignment alignment = pstyles[0]->numAlignment();
+	for (int i = 0; i < pstyles.count(); ++i)
+	{
+		if (alignment != pstyles[i]->numAlignment())
+		{
+			alignment = ParagraphStyle::NumLeftAligned;
+			break;
+		}
+	}
+	numAlignmentCombo->setCurrentItemByData(static_cast<int>(alignment));
 
 	QString prefix = pstyles[0]->numPrefix();
 	for (int i = 0; i < pstyles.count(); ++i)

Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27703)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -50,6 +50,7 @@
 
 	void fillBulletStrEditCombo();
 	void fillNumerationsCombo();
+	void fillNumAlignmentCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
 	void setParagraphEffect(int);

Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27703)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -1476,6 +1476,7 @@
                </layout>
               </widget>
              </item>
+
             </layout>
            </item>
           </layout>
@@ -1546,44 +1547,41 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
+              <widget class="FormWidget" name="numAlignmentLabel">
+               <property name="sizePolicy">
+                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+                 <horstretch>0</horstretch>
+                 <verstretch>0</verstretch>
+                </sizepolicy>
+               </property>
+               <property name="toolTip">
+                <string>Align the generated list marker inside its marker area</string>
+               </property>
+               <property name="label" stdset="0">
+                <string>Marker alignment:</string>
+               </property>
+               <layout class="QHBoxLayout" name="horizontalLayout_24">
+                <property name="leftMargin">
+                 <number>0</number>
+                </property>
+                <property name="topMargin">
+                 <number>0</number>
+                </property>
+                <property name="rightMargin">
+                 <number>0</number>
+                </property>
+                <property name="bottomMargin">
+                 <number>0</number>
+                </property>
+                <item>
+                 <widget class="SMScComboBox" name="numAlignmentCombo">
+                  <property name="toolTip">
+                   <string>Use Suffix to align suffixes such as dots or parentheses in 1., 10., 100.</string>
+                  </property>
+                 </widget>
+                </item>
+               </layout>
+              </widget>
             </item>
             <item>
              <spacer name="horizontalSpacer_14">
@@ -1792,7 +1790,7 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
+  <tabstop>numAlignmentCombo</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>

Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27703)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -511,12 +511,12 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	connect(m_pwidget->numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(slotNumLevel(int)));
 	connect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
+	connect(m_pwidget->numAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotNumAlignment(int)));
 	connect(m_pwidget->numStartSpin, SIGNAL(valueChanged(int)), this, SLOT(slotNumStart(int)));
 	connect(m_pwidget->numRestartCombo, SIGNAL(activated(int)), this, SLOT(slotNumRestart(int)));
 	connect(m_pwidget->numRestartOtherBox, SIGNAL(toggled(bool)), this, SLOT(slotNumOther(bool)));
@@ -606,11 +606,11 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
 	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
+	disconnect(m_pwidget->numAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotNumAlignment(int)));
 	disconnect(m_pwidget->numLevelSpin, SIGNAL(valueChanged(int)), this, SLOT(slotNumLevel(int)));
 	disconnect(m_pwidget->numStartSpin, SIGNAL(valueChanged(int)), this, SLOT(slotNumStart(int)));
 	disconnect(m_pwidget->numRestartCombo, SIGNAL(activated(int)), this, SLOT(slotNumRestart(int)));
@@ -953,20 +953,6 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
-{
-	if (m_pwidget->parEffectIndentBox->useParentValue())
-		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
-	{
-		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
-	}
-	
-	slotSelectionDirty();
-}
-
 void SMParagraphStyle::slotParEffectCharStyle(int index)
 {
 	QString name;
@@ -1084,6 +1070,23 @@
 	}
 
 	slotSelectionDirty();
+}
+
+void SMParagraphStyle::slotNumAlignment(int)
+{
+	if (m_pwidget->numAlignmentCombo->useParentValue())
+	{
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->resetNumAlignment();
+	}
+	else
+	{
+		ParagraphStyle::NumAlignment alignment = static_cast<ParagraphStyle::NumAlignment>(m_pwidget->numAlignmentCombo->currentData().toInt());
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->setNumAlignment(alignment);
+	}
+
+	slotSelectionDirty();
 }
 
 void SMParagraphStyle::slotNumLevel(int level)

Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27703)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,13 +77,13 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
 	void slotNumNew();
 	void slotSelectionDirty();
 	void slotNumFormat(int);
+	void slotNumAlignment(int);
 	void slotNumLevel(int level);
 	void slotNumPrefix(const QString &str);
 	void slotNumSuffix(const QString &str);

Index: scribus/ui/stylemanager.cpp
===================================================================
--- scribus/ui/stylemanager.cpp	(revision 27703)
+++ scribus/ui/stylemanager.cpp	(working copy)
@@ -181,6 +181,35 @@
 	}
 }
 
+void StyleManager::startup()
+{
+	setFontSize();
+	if (!m_doc && !isModal())
+	{
+		storeVisibility(false);
+		hide();
+		emit paletteShown(false);
+		return;
+	}
+
+	ScrPaletteBase::startup();
+}
+
+void StyleManager::setPaletteShown(bool visible)
+{
+	if (visible && !m_doc && !isModal())
+	{
+		storeVisibility(false);
+		hide();
+		emit paletteShown(false);
+		return;
+	}
+
+	ScrPaletteBase::setPaletteShown(visible);
+	if (!visible)
+		emit paletteShown(false);
+}
+
 template<class ItemType> 
 ItemType* StyleManager::item()
 {
@@ -254,6 +283,8 @@
 	deleteUnusedButton->setEnabled(hasDoc);
 	m_rightClickPopup->setEnabled(hasDoc);
 	m_newPopup->setEnabled(hasDoc);
+	if (!hasDoc && !isModal())
+		setPaletteShown(false);
 	if (m_doc && (m_doc != oldDoc) && this->isVisible())
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(slotDocSelectionChanged()));
 

Index: scribus/ui/stylemanager.h
===================================================================
--- scribus/ui/stylemanager.h	(revision 27703)
+++ scribus/ui/stylemanager.h	(working copy)
@@ -49,10 +49,12 @@
 	 * document, and recreates the tree and associated shortcut actions.
 	 */
 	void reloadStyles();
+	void startup();
 
 	QMap<QString,Keys> keyMap();
 
 public slots:
+	void setPaletteShown(bool visible) override;
 	void setDoc(ScribusDoc *doc);
 	void languageChange();
 	void unitChange();

qirat

2026-07-10 01:02

reporter   ~0054006

with* a grain of salt... Lol

Fahad

2026-07-13 15:01

developer   ~0054029

First of all, thank you so much for putting together the second-list-marker-alignment-indent-ui.patch and offering your help! I really appreciate the time and effort you took to look into the performance issues and propose a clever workaround. Your approach to reusing the main layout loop's text feed was a great piece of engineering to avoid redundant shaping.

After diving deep into the performance profile and looking at how other major software handles this, I wanted to share why I couldn't use that exact patch as-is, what the underlying problem turned out to be, and how the final design solves it permanently.

The Problem with the First Two Approaches
The core feature—automatically fitting the list alignment precisely to the widest live marker—is inherently expensive.
- My original patch (v1) was perfectly accurate across multiple linked text frames, but it was incredibly slow. It forced a full re-shape of the entire story from scratch on every single keystroke.
- Your patch genuinely fixed the duplicate shaping bug by reusing the layout's active ShapedTextFeed. However, because it started measuring from the current frame (firstInFrame()) instead of the very beginning of the story (character 0), it introduced a correctness bug: if a numbered list spans across multiple linked frames, each frame only sees its own local text. This causes the alignment to break and become inconsistent across frame boundaries.

I was stuck in a trade-off: my way was correct but slow; your way was fast but broke cross-frame alignment.

How Industry Giants Solve This:

To figure out a way out of this trap, I researched how LibreOffice, InDesign, and LaTeX handle list alignment. It turns out none of them do live document scanning like I was trying to do:

- LibreOffice & InDesign: They don't measure the numbers at all. They use a fixed-width box (a user-defined indent/tab setting) and align the text inside that box. If your numbers get too big, they clip or overflow until the user manually changes the setting.

- LaTeX (enumitem): LaTeX has an "auto-fit" feature, but it doesn't scan the document either. It takes a hardcoded placeholder guess (like "viii" for Roman numerals) and measures just that one string to determine the width.

My New, Final Approach:

I realized I could get the best of both worlds: the instant O(1) performance of LaTeX/LibreOffice, combined with the exact accuracy of my original live-updating feature.

Instead of scanning real text frames or glyphs in the document, my final patch derives the maximum width using synthetic string generation:
1. Track the Max Counter: Scribus already tracks list counts during layout. I added a small field (m_maxCounters) to remember the highest number a list actually reaches.
2. Generate the Worst-Case String: For normal numbers (1, 2, 3), the highest number is always the widest string. For non-monotonic lists (like Roman numerals, where 38 is XXXVIII—much wider than 39), I run a lightning-fast, pure-arithmetic string loop from 1 to the max count to find the longest text candidate.
3. Measure a "Ghost" String: I pass only that one winning candidate string to a lightweight, isolated text-shaper probe. It never touches the real document text or frames.
4. Global Cache: The resulting width is cached in StoryText's shared memory, making it instantly available across all linked frames without any recalculations.

This completely eliminates the heavy story-scanning loops, maintains absolute cross-frame correctness, and actually outperforms what LaTeX does because it uses the real list count rather than a hardcoded guess!

The result:

    Same correctness as your original patch (handles multi-frame chains perfectly).

    No full-document reshaping—performance is O(1) in most cases.

    No frame-boundary bugs (since we don't rely on frame text at all).

    Better UX than industry standards—we keep the live auto-fit.

Finally, please the new patch with huge document.
one last note, I couldn't reproduce your problem in my machine. it may be caching problem.
suffixalignment v2.patch (51,056 bytes)   
diff --git a/scribus/numeration.h b/scribus/numeration.h
index 88857e042..a3bad3aed 100644
--- a/scribus/numeration.h
+++ b/scribus/numeration.h
@@ -77,6 +77,7 @@ struct NumStruct
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 1c5b95c58..ed0eb1df3 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -1075,6 +1075,152 @@ double calculateLineSpacing (const ParagraphStyle &style, PageItem *item)
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
+
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	double result;
+	if (!numS || level < 0 || level >= numS->m_maxCounters.count())
+	{
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	}
+	else
+	{
+		int maxCount = qMax(1, numS->m_maxCounters.at(level));
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
 
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
@@ -1391,8 +1537,8 @@ void PageItem_TextFrame::layout()
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1425,16 +1571,6 @@ void PageItem_TextFrame::layout()
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1807,7 +1943,7 @@ void PageItem_TextFrame::layout()
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1816,33 +1952,34 @@ void PageItem_TextFrame::layout()
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
 
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
index 424ddcfbc..763a573ba 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
@@ -3123,10 +3123,6 @@ void Scribus150Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@ PageItem* Scribus150Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
index a6e7b4baa..ffebaef12 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
@@ -842,8 +842,6 @@ void Scribus150Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
index c18650807..a08b06396 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
@@ -3131,10 +3131,6 @@ void Scribus170Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@ PageItem* Scribus170Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
index 3b7a5ec1c..1e8c88068 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
@@ -844,8 +844,6 @@ void Scribus170Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
index 6c96651fb..a1372df7f 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
@@ -3635,10 +3635,6 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6679,8 +6679,6 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6717,6 +6715,8 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
index ae959bec6..2608bc355 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
@@ -846,8 +846,6 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
diff --git a/scribus/scribusdoc.cpp b/scribus/scribusdoc.cpp
index c55f7634f..3faf9ae00 100644
--- a/scribus/scribusdoc.cpp
+++ b/scribus/scribusdoc.cpp
@@ -17731,6 +17731,11 @@ void ScribusDoc::setNumerationCounter(const QString& numName, int level, int num
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+			numS->m_maxCounters.append(0);
+		if (number > numS->m_maxCounters.at(level))
+			numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
diff --git a/scribus/styles/paragraphstyle.attrdefs.cxx b/scribus/styles/paragraphstyle.attrdefs.cxx
index 238e71a5c..bbd0c0525 100644
--- a/scribus/styles/paragraphstyle.attrdefs.cxx
+++ b/scribus/styles/paragraphstyle.attrdefs.cxx
@@ -40,7 +40,6 @@ ATTRDEF(bool, keepTogether, KeepTogether, false)
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@ ATTRDEF(int, numStart, NumStart, 1)
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
diff --git a/scribus/styles/paragraphstyle.cpp b/scribus/styles/paragraphstyle.cpp
index f182104f3..9384ef24e 100644
--- a/scribus/styles/paragraphstyle.cpp
+++ b/scribus/styles/paragraphstyle.cpp
@@ -208,6 +208,14 @@ void ParagraphStyle::setStyle(const ParagraphStyle & other)
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@ static QString toXMLString(ParagraphStyle::AlignmentType val)
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -344,6 +357,12 @@ ParagraphStyle::AlignmentType parse<ParagraphStyle::AlignmentType>(const Xml_str
 	return parseEnum<ParagraphStyle::AlignmentType>(str);
 }
 
+template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
 template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
diff --git a/scribus/styles/paragraphstyle.h b/scribus/styles/paragraphstyle.h
index 4e5bac2a1..2f8303791 100644
--- a/scribus/styles/paragraphstyle.h
+++ b/scribus/styles/paragraphstyle.h
@@ -76,6 +76,13 @@ public:
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@ public:
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
diff --git a/scribus/text/sctext_shared.cpp b/scribus/text/sctext_shared.cpp
index d984c7901..d7d7a882f 100644
--- a/scribus/text/sctext_shared.cpp
+++ b/scribus/text/sctext_shared.cpp
@@ -68,6 +68,7 @@ void ScText_Shared::clear()
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
diff --git a/scribus/text/sctext_shared.h b/scribus/text/sctext_shared.h
index d16bd8267..2de71bbbb 100644
--- a/scribus/text/sctext_shared.h
+++ b/scribus/text/sctext_shared.h
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,10 @@ public:
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
 	void clear();
 	
 	/**
diff --git a/scribus/text/storytext.cpp b/scribus/text/storytext.cpp
index 4214bb583..5c9f2b468 100644
--- a/scribus/text/storytext.cpp
+++ b/scribus/text/storytext.cpp
@@ -1391,6 +1391,11 @@ void StoryText::clearFlag(int pos, LayoutFlags flags)
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+
 
 const CharStyle & StoryText::charStyle() const
 {
@@ -2261,6 +2266,10 @@ void StoryText::invalidate(int firstItem, int endItem)
 		if (par)
 			par->charStyleContext()->invalidate();
 	}
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+
 	if (!signalsBlocked())
 		emit changed(firstItem, endItem);
 }
diff --git a/scribus/text/storytext.h b/scribus/text/storytext.h
index 7920d56ab..9aaf6effe 100644
--- a/scribus/text/storytext.h
+++ b/scribus/text/storytext.h
@@ -29,6 +29,7 @@ pageitem.cpp  -  description
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,10 @@ public:
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
diff --git a/scribus/ui/propertywidget_pareffect.cpp b/scribus/ui/propertywidget_pareffect.cpp
index 0e6ef1e52..1a3701ea5 100644
--- a/scribus/ui/propertywidget_pareffect.cpp
+++ b/scribus/ui/propertywidget_pareffect.cpp
@@ -45,6 +45,10 @@ PropertyWidget_ParEffect::PropertyWidget_ParEffect(QWidget *parent) : QFrame(par
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@ void PropertyWidget_ParEffect::setType(int id)
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@ void PropertyWidget_ParEffect::connectSignals()
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@ void PropertyWidget_ParEffect::disconnectSignals()
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@ void PropertyWidget_ParEffect::handleParEffectUse()
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@ void PropertyWidget_ParEffect::handlePEOffset(double offset)
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
diff --git a/scribus/ui/propertywidget_pareffect.h b/scribus/ui/propertywidget_pareffect.h
index 62729f7c2..54d2aeab4 100644
--- a/scribus/ui/propertywidget_pareffect.h
+++ b/scribus/ui/propertywidget_pareffect.h
@@ -66,7 +66,7 @@ public slots:
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
diff --git a/scribus/ui/propertywidget_pareffectbase.ui b/scribus/ui/propertywidget_pareffectbase.ui
index 1a2670a01..fa21acf2e 100644
--- a/scribus/ui/propertywidget_pareffectbase.ui
+++ b/scribus/ui/propertywidget_pareffectbase.ui
@@ -438,10 +438,17 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
           </property>
          </widget>
         </item>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
diff --git a/scribus/ui/smpstylewidget.cpp b/scribus/ui/smpstylewidget.cpp
index 12fde4428..d5e0fc626 100644
--- a/scribus/ui/smpstylewidget.cpp
+++ b/scribus/ui/smpstylewidget.cpp
@@ -25,7 +25,6 @@ static bool isEqual(double a, double b)
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@ SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@ void SMPStyleWidget::languageChange()
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@ void SMPStyleWidget::fillPECombo()
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@ void SMPStyleWidget::setParagraphEffect(int index)
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
diff --git a/scribus/ui/smpstylewidget.h b/scribus/ui/smpstylewidget.h
index c985d7033..4cb5c88bd 100644
--- a/scribus/ui/smpstylewidget.h
+++ b/scribus/ui/smpstylewidget.h
@@ -52,6 +52,7 @@ private:
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
diff --git a/scribus/ui/smpstylewidget.ui b/scribus/ui/smpstylewidget.ui
index 8a6d90e31..f62551956 100644
--- a/scribus/ui/smpstylewidget.ui
+++ b/scribus/ui/smpstylewidget.ui
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@ A value of 0 means unlimited hyphenations.</string>
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@ A value of 0 means unlimited hyphenations.</string>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@ A value of 0 means unlimited hyphenations.</string>
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@ A value of 0 means unlimited hyphenations.</string>
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@ A value of 0 means unlimited hyphenations.</string>
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1545,46 +1556,6 @@ A value of 0 means unlimited hyphenations.</string>
               </layout>
              </widget>
             </item>
-            <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
             <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
@@ -1606,7 +1577,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@ A value of 0 means unlimited hyphenations.</string>
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
diff --git a/scribus/ui/smtextstyles.cpp b/scribus/ui/smtextstyles.cpp
index b021322dd..390365f53 100644
--- a/scribus/ui/smtextstyles.cpp
+++ b/scribus/ui/smtextstyles.cpp
@@ -37,7 +37,6 @@ for which a new license (GPL+exception) is in place.
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@ void SMParagraphStyle::setupConnections()
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@ void SMParagraphStyle::removeConnections()
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@ void SMParagraphStyle::slotParEffectOffset()
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
diff --git a/scribus/ui/smtextstyles.h b/scribus/ui/smtextstyles.h
index 3dd02356a..087870f22 100644
--- a/scribus/ui/smtextstyles.h
+++ b/scribus/ui/smtextstyles.h
@@ -77,7 +77,7 @@ private slots:
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignment v2.patch (51,056 bytes)   

qirat

2026-07-14 03:29

reporter   ~0054030

Thank yo very much @Fahad. This time, I tested the patch on a clean r27709. I use 3-directory structure src, build and install, keeping things separate. I deleted all of them, fetched the source, built and compiled. Here is what I observed and more:

1. I think it is the fonts. Different fonts behave differently with dot as a suffix.
2. When I use other suffixes like ) or even ! (just as a test), they all seem to align fine. Only dot gives a problem.
3. Try Adwaita Sans. Dots do not align perfectly but ) does.
4. No issue of any kind with monospace fonts (dots and ) align fine).

The UI with your patch needs tweaking I think. The "Restart after..." options belong below the "Restart:[dropdown]" property, at the very least for now.

Also, the "Auto-Indent" UI you removed. I find it very useful when I tested it earlier. It is amazing what one can achieve with just one tick in the checkbox. It only just needs to be renamed to "Auto Hanging Indent" imho.

Fahad

2026-07-14 04:04

developer   ~0054031

I couldn't reproduce it. I am using Fedora 44.
 Auto-Indent feature is confusing a little bit, to work you have to set left margin first which is not obvious in the first place, the new feature is more simple and easy. GUI staff I don't know . I think the whole page needs to redesign IMO.
1.png (53,167 bytes)   
1.png (53,167 bytes)   
2.png (67,323 bytes)   
2.png (67,323 bytes)   

qirat

2026-07-14 05:17

reporter   ~0054032

That is weird. I think I will test on other system and OS, and report back.

I might also try the UI stuff but guide me a little here how should the UI patch go?
- I post it here and one is supposed to apply it after your patch? (Bcs it will have components you added that did not exist earlier.)
- or, I post it as a new bug report (which I think will confuse things more, as one does not know when to apply that)

Fahad

2026-07-14 06:11

developer   ~0054033

try to export to pdf and check if the problem still there.
for UI, I think it is better to open a new bug report because it is not related to this feature.

Fahad

2026-07-14 11:31

developer   ~0054034

To stress-test my patch against large documents, I created a 200-page document and filled it with 11,250 single-line paragraphs. I then changed the paragraph effect to apply a Numbered List.

I noticed a very slow performance during this test. Since the default alignment suffix is left—which matches what the current trunk has—I confirmed that my new approach did not cause this lag. The bottleneck already existed in the trunk.

I managed to fix the slowness in the layout engine. Now, I have made Scribus acceptably fast when applying paragraph effects, even across massive, document-wide style changes.

finally please test
suffixalignmentv3.patch (54,288 bytes)   
diff --git a/scribus/numeration.h b/scribus/numeration.h
index 88857e0429..a3bad3aed5 100644
--- a/scribus/numeration.h
+++ b/scribus/numeration.h
@@ -77,6 +77,7 @@ struct NumStruct
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 1c5b95c586..c95f693b0a 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -1075,6 +1075,152 @@ double calculateLineSpacing (const ParagraphStyle &style, PageItem *item)
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
+
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	double result;
+	if (!numS || level < 0 || level >= numS->m_maxCounters.count())
+	{
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	}
+	else
+	{
+		int maxCount = qMax(1, numS->m_maxCounters.at(level));
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
 
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
@@ -1391,8 +1537,8 @@ void PageItem_TextFrame::layout()
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1425,16 +1571,6 @@ void PageItem_TextFrame::layout()
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1807,7 +1943,7 @@ void PageItem_TextFrame::layout()
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1816,33 +1952,34 @@ void PageItem_TextFrame::layout()
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
 
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -4937,6 +5074,7 @@ bool PageItem_TextFrame::checkKeyIsShortcut(QKeyEvent *k)
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5032,7 +5170,7 @@ void PageItem_TextFrame::updateBulletsNum()
 		}
 
 	}
-
+	itemText.blockSignals(false);
 	m_Doc->updateNumbers();
 }
 
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
index 424ddcfbcc..763a573bad 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
@@ -3123,10 +3123,6 @@ void Scribus150Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@ PageItem* Scribus150Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
index a6e7b4baaa..ffebaef127 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
@@ -842,8 +842,6 @@ void Scribus150Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
index c18650807f..a08b063961 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
@@ -3131,10 +3131,6 @@ void Scribus170Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@ PageItem* Scribus170Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
index 3b7a5ec1ca..1e8c88068e 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
@@ -844,8 +844,6 @@ void Scribus170Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
index 6c96651fbf..a1372df7fb 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
@@ -3635,10 +3635,6 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6679,8 +6679,6 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6717,6 +6715,8 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
index ae959bec6f..2608bc355f 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
@@ -846,8 +846,6 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
diff --git a/scribus/scribusdoc.cpp b/scribus/scribusdoc.cpp
index c55f7634fe..6442091bd9 100644
--- a/scribus/scribusdoc.cpp
+++ b/scribus/scribusdoc.cpp
@@ -17731,6 +17731,11 @@ void ScribusDoc::setNumerationCounter(const QString& numName, int level, int num
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+			numS->m_maxCounters.append(0);
+		if (number > numS->m_maxCounters.at(level))
+			numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17738,7 +17743,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17818,6 +17823,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17901,7 +17907,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17957,7 +17962,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
diff --git a/scribus/styles/paragraphstyle.attrdefs.cxx b/scribus/styles/paragraphstyle.attrdefs.cxx
index 238e71a5cf..bbd0c05251 100644
--- a/scribus/styles/paragraphstyle.attrdefs.cxx
+++ b/scribus/styles/paragraphstyle.attrdefs.cxx
@@ -40,7 +40,6 @@ ATTRDEF(bool, keepTogether, KeepTogether, false)
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@ ATTRDEF(int, numStart, NumStart, 1)
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
diff --git a/scribus/styles/paragraphstyle.cpp b/scribus/styles/paragraphstyle.cpp
index f182104f34..9384ef24e5 100644
--- a/scribus/styles/paragraphstyle.cpp
+++ b/scribus/styles/paragraphstyle.cpp
@@ -208,6 +208,14 @@ void ParagraphStyle::setStyle(const ParagraphStyle & other)
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@ static QString toXMLString(ParagraphStyle::AlignmentType val)
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -344,6 +357,12 @@ ParagraphStyle::AlignmentType parse<ParagraphStyle::AlignmentType>(const Xml_str
 	return parseEnum<ParagraphStyle::AlignmentType>(str);
 }
 
+template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
 template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
diff --git a/scribus/styles/paragraphstyle.h b/scribus/styles/paragraphstyle.h
index 4e5bac2a12..2f83037918 100644
--- a/scribus/styles/paragraphstyle.h
+++ b/scribus/styles/paragraphstyle.h
@@ -76,6 +76,13 @@ public:
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@ public:
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
diff --git a/scribus/text/sctext_shared.cpp b/scribus/text/sctext_shared.cpp
index d984c79019..03ecc35b12 100644
--- a/scribus/text/sctext_shared.cpp
+++ b/scribus/text/sctext_shared.cpp
@@ -68,6 +68,9 @@ void ScText_Shared::clear()
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	maxParEffectWidthCache.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
diff --git a/scribus/text/sctext_shared.h b/scribus/text/sctext_shared.h
index d16bd8267a..d22c0f7316 100644
--- a/scribus/text/sctext_shared.h
+++ b/scribus/text/sctext_shared.h
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,13 @@ public:
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
diff --git a/scribus/text/storytext.cpp b/scribus/text/storytext.cpp
index 4214bb583e..58793910a0 100644
--- a/scribus/text/storytext.cpp
+++ b/scribus/text/storytext.cpp
@@ -27,7 +27,6 @@ pageitem.cpp  -  description
 //FIXME: this include must go to sctextstruct.h !
 #include <QList>
 #include <cassert>  //added to make Fedora-5 happy
-
 #include "notesstyles.h"
 #include "scribusdoc.h"
 #include "sctext_shared.h"
@@ -699,6 +698,7 @@ void StoryText::removeChars(int pos, uint len)
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
+	d->plainTextValid = false;
 	invalidate(pos, length());
 }
 
@@ -773,6 +773,7 @@ void StoryText::insertChars(int pos, const QString& txt, bool applyNeighbourStyl
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +834,7 @@ void StoryText::insertCharsWithSoftHyphens(int pos, const QString& txt, bool app
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +862,7 @@ void StoryText::replaceChar(int pos, QChar ch)
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1025,8 @@ QString StoryText::plainText() const
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1041,8 @@ QString StoryText::plainText() const
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,6 +1396,11 @@ void StoryText::clearFlag(int pos, LayoutFlags flags)
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+
 
 const CharStyle & StoryText::charStyle() const
 {
@@ -2261,6 +2271,10 @@ void StoryText::invalidate(int firstItem, int endItem)
 		if (par)
 			par->charStyleContext()->invalidate();
 	}
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+
 	if (!signalsBlocked())
 		emit changed(firstItem, endItem);
 }
diff --git a/scribus/text/storytext.h b/scribus/text/storytext.h
index 7920d56aba..9aaf6effed 100644
--- a/scribus/text/storytext.h
+++ b/scribus/text/storytext.h
@@ -29,6 +29,7 @@ pageitem.cpp  -  description
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,10 @@ public:
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
diff --git a/scribus/ui/propertywidget_pareffect.cpp b/scribus/ui/propertywidget_pareffect.cpp
index 0e6ef1e520..1a3701ea52 100644
--- a/scribus/ui/propertywidget_pareffect.cpp
+++ b/scribus/ui/propertywidget_pareffect.cpp
@@ -45,6 +45,10 @@ PropertyWidget_ParEffect::PropertyWidget_ParEffect(QWidget *parent) : QFrame(par
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@ void PropertyWidget_ParEffect::setType(int id)
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@ void PropertyWidget_ParEffect::connectSignals()
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@ void PropertyWidget_ParEffect::disconnectSignals()
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@ void PropertyWidget_ParEffect::handleParEffectUse()
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@ void PropertyWidget_ParEffect::handlePEOffset(double offset)
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
diff --git a/scribus/ui/propertywidget_pareffect.h b/scribus/ui/propertywidget_pareffect.h
index 62729f7c28..54d2aeab46 100644
--- a/scribus/ui/propertywidget_pareffect.h
+++ b/scribus/ui/propertywidget_pareffect.h
@@ -66,7 +66,7 @@ public slots:
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
diff --git a/scribus/ui/propertywidget_pareffectbase.ui b/scribus/ui/propertywidget_pareffectbase.ui
index 1a2670a01a..fa21acf2e1 100644
--- a/scribus/ui/propertywidget_pareffectbase.ui
+++ b/scribus/ui/propertywidget_pareffectbase.ui
@@ -438,10 +438,17 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
           </property>
          </widget>
         </item>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
diff --git a/scribus/ui/smpstylewidget.cpp b/scribus/ui/smpstylewidget.cpp
index 12fde44283..d5e0fc6261 100644
--- a/scribus/ui/smpstylewidget.cpp
+++ b/scribus/ui/smpstylewidget.cpp
@@ -25,7 +25,6 @@ static bool isEqual(double a, double b)
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@ SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@ void SMPStyleWidget::languageChange()
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@ void SMPStyleWidget::fillPECombo()
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@ void SMPStyleWidget::setParagraphEffect(int index)
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
diff --git a/scribus/ui/smpstylewidget.h b/scribus/ui/smpstylewidget.h
index c985d7033b..4cb5c88bd9 100644
--- a/scribus/ui/smpstylewidget.h
+++ b/scribus/ui/smpstylewidget.h
@@ -52,6 +52,7 @@ private:
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
diff --git a/scribus/ui/smpstylewidget.ui b/scribus/ui/smpstylewidget.ui
index 8a6d90e316..f625519568 100644
--- a/scribus/ui/smpstylewidget.ui
+++ b/scribus/ui/smpstylewidget.ui
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@ A value of 0 means unlimited hyphenations.</string>
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@ A value of 0 means unlimited hyphenations.</string>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@ A value of 0 means unlimited hyphenations.</string>
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@ A value of 0 means unlimited hyphenations.</string>
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@ A value of 0 means unlimited hyphenations.</string>
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1545,46 +1556,6 @@ A value of 0 means unlimited hyphenations.</string>
               </layout>
              </widget>
             </item>
-            <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
             <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
@@ -1606,7 +1577,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@ A value of 0 means unlimited hyphenations.</string>
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
diff --git a/scribus/ui/smtextstyles.cpp b/scribus/ui/smtextstyles.cpp
index b021322dd6..390365f537 100644
--- a/scribus/ui/smtextstyles.cpp
+++ b/scribus/ui/smtextstyles.cpp
@@ -37,7 +37,6 @@ for which a new license (GPL+exception) is in place.
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@ void SMParagraphStyle::setupConnections()
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@ void SMParagraphStyle::removeConnections()
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@ void SMParagraphStyle::slotParEffectOffset()
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
diff --git a/scribus/ui/smtextstyles.h b/scribus/ui/smtextstyles.h
index 3dd02356a0..087870f222 100644
--- a/scribus/ui/smtextstyles.h
+++ b/scribus/ui/smtextstyles.h
@@ -77,7 +77,7 @@ private slots:
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignmentv3.patch (54,288 bytes)   

qirat

2026-07-15 08:00

reporter   ~0054038

Perfect. Works across linked frames (tested with 100 items). Thanks a lot, @Fahad.

qirat

2026-07-15 13:38

reporter   ~0054041

How about making "Right" the default option? I think most users would what the lists suffix-aligned. They can always choose otheriwse anyway.

ale

2026-07-16 06:46

manager   ~0054043

Last edited: 2026-07-16 06:48

In my world, lists are mostly left aligned...
(but right aligned and -- probably very rare -- centered to exist)

qirat

2026-07-16 10:58

reporter   ~0054044

I would say that most books (especially fiction) do not have any lists. It is the TOC that gives you a headache (either Arabic numbers or Roman). They do not align at all (as of now) and it looks unprofessional.

Having an option to align suffix right is a great improvement imho (especially the one that is simpler in Scribus than other DTPs). Just one click and done.

I would also argue to bring back the "Auto Hanging Indent" (I would like to call it that instead of "auto indent" which is a bit vague). For paragraphs as lists (as opposed to just one word per entry), it achives with one click what would make it a headache to set (for a new user atleast).

@Fahad removed it saying:
> Auto-Indent feature is confusing a little bit, to work you have to set left margin first which is not obvious in the first place, the new feature is more simple and easy.

I understand his point as well. However, how about making it simple instead? Rewriting that bit of code and making it so that it takes care of it all? Setting left indent if not set and ignore if set?

Fahad

2026-07-16 11:21

developer   ~0054045

@qirat it seems either me or you didn't understad auto-indent work :-)
what is my understanding is auto-Indent is suffix right align. If it is not that please educate me.

ale

2026-07-16 15:57

manager   ~0054046

As far I can tell, most "normal" numbered lists are left aligned.
I attach the screenshot of what does LibreOffice by default.

When numbering is used in titles, the numbers are almost always left aligned.

The only case, where I think people are using centered or right aligned numbers is in table of contents, mostly when using roman numbering.

in many cases, this is not really relevant or visible, since those lists normally do not have double digits numbers.

for titles, there is also the case where the number is on its own line.
numbered-list.png (16,678 bytes)   
numbered-list.png (16,678 bytes)   

ale

2026-07-16 16:00

manager   ~0054047

Last edited: 2026-07-16 16:03

Here is an example I just stumbled upon in a local newspaper.
(Well, it's left aligned, but the indent is probably defined as Scribus now does: with a fixed space after the dot... which works well if the digits all have the same width, but is otherwise ugly to look at)
list-watson.png (130,033 bytes)   
list-watson.png (130,033 bytes)   

qirat

2026-07-16 19:23

reporter   ~0054048

@ale, I agree the lists left align by default and I take back my point to make "right" the default. I thought that would not not bother people much. But kindly see that the left aligned lists look at least the way they look in LO (your screenshot): left edge of items aligned, even though numbers are not.

@Fahad, I did not get you. Kindly elaborate.

Fahad

2026-07-28 08:47

developer   ~0054077

what am saying is auto-indent is right alignment except you have to make left space by increasing left margin to make it works. why do you want to keep it? you said: how about making it simple instead?. with my patch it is simple already.

qirat

2026-07-28 11:13

reporter   ~0054090

1. I meant with the UI there, one could just tick the box and it creates a perfect hanging indent for the list (numbered or bulleted).

2. Otherwise it is a bit work to set up a para style or do it directly: setting up left and hanging margin in the para style or PP.

Number 1 is earier, don't you think? Am I missing something here?

Fahad

2026-07-28 14:11

developer   ~0054091

Ok now I understand you. You mean keeping the old ui auto-indent checkbox but change it works from right alignment to auto hanging so when it is checked, it will make a hanging indent automatically. While this can be done, I think it is not related to this bug and it deserves its own bug report becuse we should agree on the standard depth. Is it 0.5 inches? like what ms word does, or it is open like google docs does?

qirat

2026-08-04 11:57

reporter   ~0054120

@Fahad, please see issue 0017897. Lets move this discussion there.

qirat

2026-08-04 13:36

reporter   ~0054126

@Fahad, I noticed that in the screenshots we were comparing (after your comment 0017243:0054030), where I said dots didn't aligning and you said they align, you and I were testing slightly differently.

You were working with all entries starting with lowercase while I had all start with an uppercase. I tested with lowercase and there is no issue, while uppercase first letter gives minor issue sometimes. Clip attached.

Try with uppercase first letter. (Menu > Item > Text Transformation).

Fahad

2026-08-05 04:35

developer   ~0054130

Still I couldn't reproduce it

qirat

2026-08-05 06:55

reporter   ~0054138

Ther must be something else on my end then that is causing it. I will test more.

qirat

2026-08-05 06:58

reporter   ~0054139

At this point please see if the ticket can be resolved.

I will keep testing, but the bigger part of the ticket is fixed: having the option to align suffix. That should be enough for this ticket I think.

Fahad

2026-08-11 06:41

developer   ~0054196

in this updated patch v4 I added left alignment to use same logic. in this screenshot how is look like .
new.png (58,175 bytes)   
new.png (58,175 bytes)   
suffixalignmentv4.patch (56,008 bytes)   

diff --git a/scribus/numeration.h b/scribus/numeration.h
index 88857e0429..a3bad3aed5 100644
--- a/scribus/numeration.h
+++ b/scribus/numeration.h
@@ -77,6 +77,7 @@ struct NumStruct
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 1c5b95c586..204e1e69de 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -381,6 +381,7 @@ struct LineControl {
 	bool     wasFirstInRow { false };
 	double   leftIndent { 0.0 };
 	double   rightIndent { 0.0 };
+	double   suffixPad { 0.0 };
 	double   rightMargin { 0.0 };
 	double   mustLineEnd { false };
 	int      restartIndex { 0 };  //index of glyph run where line computing should be restarted
@@ -459,6 +460,7 @@ struct LineControl {
 		maxStretch = 0.0;
 		leftIndent = 0.0;
 		rightIndent = 0.0;
+		suffixPad = 0.0;
 		rightMargin = 0.0;
 		rowDesc = 0.0;
 	}
@@ -1075,6 +1077,152 @@ double calculateLineSpacing (const ParagraphStyle &style, PageItem *item)
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
+
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	double result;
+	if (!numS || level < 0 || level >= numS->m_maxCounters.count())
+	{
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	}
+	else
+	{
+		int maxCount = qMax(1, numS->m_maxCounters.at(level));
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
 
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
@@ -1391,8 +1539,8 @@ void PageItem_TextFrame::layout()
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1425,16 +1573,6 @@ void PageItem_TextFrame::layout()
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1807,7 +1945,7 @@ void PageItem_TextFrame::layout()
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1815,34 +1953,45 @@ void PageItem_TextFrame::layout()
 					}
 
 					if (BulNumMode || DropCmode)
-					{
-						if (style.parEffectIndent())
 					{
 						double effectWidth = 0.0;
 						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-								const auto & glyph = glyphClusters[j];
+							const auto& glyph = glyphClusters[j];
 							if (glyph.firstChar() != a)
 								break;
 							effectWidth += glyph.width();
 						}
-
-							if (style.direction() == ParagraphStyle::RTL)
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
 						{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
+
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
 						}
+						else if (style.hasNum() && style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						{
+							// marker stays flush left (leftIndent untouched); instead we pad
+							// AFTER the marker so text always starts at a fixed column
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
+
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							current.suffixPad = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -2454,14 +2603,14 @@ void PageItem_TextFrame::layout()
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + current.suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + current.suffixPad;
 
 				if (DropCmode)
 				{
@@ -4937,6 +5086,7 @@ bool PageItem_TextFrame::checkKeyIsShortcut(QKeyEvent *k)
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5032,7 +5182,7 @@ void PageItem_TextFrame::updateBulletsNum()
 		}
 
 	}
-
+	itemText.blockSignals(false);
 	m_Doc->updateNumbers();
 }
 
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
index 424ddcfbcc..763a573bad 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
@@ -3123,10 +3123,6 @@ void Scribus150Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@ PageItem* Scribus150Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
index a6e7b4baaa..ffebaef127 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
@@ -842,8 +842,6 @@ void Scribus150Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
index c18650807f..a08b063961 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
@@ -3131,10 +3131,6 @@ void Scribus170Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@ PageItem* Scribus170Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
index 3b7a5ec1ca..1e8c88068e 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
@@ -844,8 +844,6 @@ void Scribus170Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
index 6c96651fbf..a1372df7fb 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
@@ -3635,10 +3635,6 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6679,8 +6679,6 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6717,6 +6715,8 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
index ae959bec6f..2608bc355f 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
@@ -846,8 +846,6 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
diff --git a/scribus/scribusdoc.cpp b/scribus/scribusdoc.cpp
index 7bd763f34e..ccb686b416 100644
--- a/scribus/scribusdoc.cpp
+++ b/scribus/scribusdoc.cpp
@@ -17733,6 +17733,11 @@ void ScribusDoc::setNumerationCounter(const QString& numName, int level, int num
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+			numS->m_maxCounters.append(0);
+		if (number > numS->m_maxCounters.at(level))
+			numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17740,7 +17745,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17820,6 +17825,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17903,7 +17909,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17959,7 +17964,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
diff --git a/scribus/styles/paragraphstyle.attrdefs.cxx b/scribus/styles/paragraphstyle.attrdefs.cxx
index 238e71a5cf..bbd0c05251 100644
--- a/scribus/styles/paragraphstyle.attrdefs.cxx
+++ b/scribus/styles/paragraphstyle.attrdefs.cxx
@@ -40,7 +40,6 @@ ATTRDEF(bool, keepTogether, KeepTogether, false)
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@ ATTRDEF(int, numStart, NumStart, 1)
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
diff --git a/scribus/styles/paragraphstyle.cpp b/scribus/styles/paragraphstyle.cpp
index f182104f34..9384ef24e5 100644
--- a/scribus/styles/paragraphstyle.cpp
+++ b/scribus/styles/paragraphstyle.cpp
@@ -208,6 +208,14 @@ void ParagraphStyle::setStyle(const ParagraphStyle & other)
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@ static QString toXMLString(ParagraphStyle::AlignmentType val)
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -344,6 +357,12 @@ ParagraphStyle::AlignmentType parse<ParagraphStyle::AlignmentType>(const Xml_str
 	return parseEnum<ParagraphStyle::AlignmentType>(str);
 }
 
+template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
 template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
diff --git a/scribus/styles/paragraphstyle.h b/scribus/styles/paragraphstyle.h
index 4e5bac2a12..2f83037918 100644
--- a/scribus/styles/paragraphstyle.h
+++ b/scribus/styles/paragraphstyle.h
@@ -76,6 +76,13 @@ public:
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@ public:
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
diff --git a/scribus/text/sctext_shared.cpp b/scribus/text/sctext_shared.cpp
index d984c79019..03ecc35b12 100644
--- a/scribus/text/sctext_shared.cpp
+++ b/scribus/text/sctext_shared.cpp
@@ -68,6 +68,9 @@ void ScText_Shared::clear()
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	maxParEffectWidthCache.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
diff --git a/scribus/text/sctext_shared.h b/scribus/text/sctext_shared.h
index d16bd8267a..d22c0f7316 100644
--- a/scribus/text/sctext_shared.h
+++ b/scribus/text/sctext_shared.h
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,13 @@ public:
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
diff --git a/scribus/text/storytext.cpp b/scribus/text/storytext.cpp
index 4214bb583e..58793910a0 100644
--- a/scribus/text/storytext.cpp
+++ b/scribus/text/storytext.cpp
@@ -27,7 +27,6 @@ pageitem.cpp  -  description
 //FIXME: this include must go to sctextstruct.h !
 #include <QList>
 #include <cassert>  //added to make Fedora-5 happy
-
 #include "notesstyles.h"
 #include "scribusdoc.h"
 #include "sctext_shared.h"
@@ -699,6 +698,7 @@ void StoryText::removeChars(int pos, uint len)
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
+	d->plainTextValid = false;
 	invalidate(pos, length());
 }
 
@@ -773,6 +773,7 @@ void StoryText::insertChars(int pos, const QString& txt, bool applyNeighbourStyl
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +834,7 @@ void StoryText::insertCharsWithSoftHyphens(int pos, const QString& txt, bool app
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +862,7 @@ void StoryText::replaceChar(int pos, QChar ch)
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1025,8 @@ QString StoryText::plainText() const
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1041,8 @@ QString StoryText::plainText() const
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,6 +1396,11 @@ void StoryText::clearFlag(int pos, LayoutFlags flags)
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+
 
 const CharStyle & StoryText::charStyle() const
 {
@@ -2261,6 +2271,10 @@ void StoryText::invalidate(int firstItem, int endItem)
 		if (par)
 			par->charStyleContext()->invalidate();
 	}
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+
 	if (!signalsBlocked())
 		emit changed(firstItem, endItem);
 }
diff --git a/scribus/text/storytext.h b/scribus/text/storytext.h
index 7920d56aba..9aaf6effed 100644
--- a/scribus/text/storytext.h
+++ b/scribus/text/storytext.h
@@ -29,6 +29,7 @@ pageitem.cpp  -  description
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,10 @@ public:
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
diff --git a/scribus/ui/propertywidget_pareffect.cpp b/scribus/ui/propertywidget_pareffect.cpp
index 0e6ef1e520..1a3701ea52 100644
--- a/scribus/ui/propertywidget_pareffect.cpp
+++ b/scribus/ui/propertywidget_pareffect.cpp
@@ -45,6 +45,10 @@ PropertyWidget_ParEffect::PropertyWidget_ParEffect(QWidget *parent) : QFrame(par
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@ void PropertyWidget_ParEffect::setType(int id)
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@ void PropertyWidget_ParEffect::connectSignals()
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@ void PropertyWidget_ParEffect::disconnectSignals()
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@ void PropertyWidget_ParEffect::handleParEffectUse()
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@ void PropertyWidget_ParEffect::handlePEOffset(double offset)
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
diff --git a/scribus/ui/propertywidget_pareffect.h b/scribus/ui/propertywidget_pareffect.h
index 62729f7c28..54d2aeab46 100644
--- a/scribus/ui/propertywidget_pareffect.h
+++ b/scribus/ui/propertywidget_pareffect.h
@@ -66,7 +66,7 @@ public slots:
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
diff --git a/scribus/ui/propertywidget_pareffectbase.ui b/scribus/ui/propertywidget_pareffectbase.ui
index 1a2670a01a..fa21acf2e1 100644
--- a/scribus/ui/propertywidget_pareffectbase.ui
+++ b/scribus/ui/propertywidget_pareffectbase.ui
@@ -438,10 +438,17 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
           </property>
          </widget>
         </item>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
diff --git a/scribus/ui/smpstylewidget.cpp b/scribus/ui/smpstylewidget.cpp
index 12fde44283..d5e0fc6261 100644
--- a/scribus/ui/smpstylewidget.cpp
+++ b/scribus/ui/smpstylewidget.cpp
@@ -25,7 +25,6 @@ static bool isEqual(double a, double b)
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@ SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@ void SMPStyleWidget::languageChange()
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@ void SMPStyleWidget::fillPECombo()
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@ void SMPStyleWidget::setParagraphEffect(int index)
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
diff --git a/scribus/ui/smpstylewidget.h b/scribus/ui/smpstylewidget.h
index c985d7033b..4cb5c88bd9 100644
--- a/scribus/ui/smpstylewidget.h
+++ b/scribus/ui/smpstylewidget.h
@@ -52,6 +52,7 @@ private:
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
diff --git a/scribus/ui/smpstylewidget.ui b/scribus/ui/smpstylewidget.ui
index 8a6d90e316..f625519568 100644
--- a/scribus/ui/smpstylewidget.ui
+++ b/scribus/ui/smpstylewidget.ui
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@ A value of 0 means unlimited hyphenations.</string>
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@ A value of 0 means unlimited hyphenations.</string>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@ A value of 0 means unlimited hyphenations.</string>
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@ A value of 0 means unlimited hyphenations.</string>
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@ A value of 0 means unlimited hyphenations.</string>
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1545,46 +1556,6 @@ A value of 0 means unlimited hyphenations.</string>
               </layout>
              </widget>
             </item>
-            <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
             <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
@@ -1606,7 +1577,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@ A value of 0 means unlimited hyphenations.</string>
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
diff --git a/scribus/ui/smtextstyles.cpp b/scribus/ui/smtextstyles.cpp
index b021322dd6..390365f537 100644
--- a/scribus/ui/smtextstyles.cpp
+++ b/scribus/ui/smtextstyles.cpp
@@ -37,7 +37,6 @@ for which a new license (GPL+exception) is in place.
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@ void SMParagraphStyle::setupConnections()
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@ void SMParagraphStyle::removeConnections()
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,15 +951,23 @@ void SMParagraphStyle::slotParEffectOffset()
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
+			m_selection[i]->resetSuffixAlignment();
+	}
 	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
 
 	slotSelectionDirty();
diff --git a/scribus/ui/smtextstyles.h b/scribus/ui/smtextstyles.h
index 3dd02356a0..087870f222 100644
--- a/scribus/ui/smtextstyles.h
+++ b/scribus/ui/smtextstyles.h
@@ -77,7 +77,7 @@ private slots:
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignmentv4.patch (56,008 bytes)   

qirat

2026-08-12 02:18

reporter   ~0054197

Thank you @Fahad for your quick work.

Kindly, could you please update it against the latest r27770, so I could test? When I dry-run here on the latest, it gives conflicts and some more:
----
U scribus/numeration.h
C scribus/pageitem_textframe.cpp
> applied hunk @@ -381,6 +381,7 @@ with offset 9
> applied hunk @@ -459,6 +460,7 @@ with offset 9
> applied hunk @@ -1075,6 +1077,152 @@ with offset 9
> applied hunk @@ -1391,8 +1539,8 @@ with offset 9
> applied hunk @@ -1425,16 +1573,6 @@ with offset 9
> applied hunk @@ -1807,7 +1945,7 @@ with offset 9
> applied hunk @@ -2454,14 +2603,14 @@ with offset 9
> applied hunk @@ -4937,6 +5086,7 @@ with offset 9
> applied hunk @@ -5032,7 +5182,7 @@ with offset 9
> rejected hunk @@ -1815,34 +1953,45 @@
U scribus/plugins/fileloader/scribus150format/scribus150format.cpp
U scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
U scribus/plugins/fileloader/scribus170format/scribus170format.cpp
U scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
U scribus/plugins/fileloader/scribus171format/scribus171format.cpp
> applied hunk @@ -6679,8 +6679,6 @@ with offset 4
> applied hunk @@ -6717,6 +6715,8 @@ with offset 4
U scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
U scribus/scribusdoc.cpp
> applied hunk @@ -17733,6 +17733,11 @@ with offset 23
> applied hunk @@ -17740,7 +17745,7 @@ with offset 23
> applied hunk @@ -17820,6 +17825,7 @@ with offset 23
> applied hunk @@ -17903,7 +17909,6 @@ with offset 23
> applied hunk @@ -17959,7 +17964,6 @@ with offset 23
U scribus/styles/paragraphstyle.attrdefs.cxx
U scribus/styles/paragraphstyle.cpp
U scribus/styles/paragraphstyle.h
U scribus/text/sctext_shared.cpp
U scribus/text/sctext_shared.h
U scribus/text/storytext.cpp
U scribus/text/storytext.h
U scribus/ui/propertywidget_pareffect.cpp
U scribus/ui/propertywidget_pareffect.h
U scribus/ui/propertywidget_pareffectbase.ui
U scribus/ui/smpstylewidget.cpp
U scribus/ui/smpstylewidget.h
U scribus/ui/smpstylewidget.ui
C scribus/ui/smtextstyles.cpp
> rejected hunk @@ -953,15 +951,23 @@
U scribus/ui/smtextstyles.h
Summary of conflicts:
  Text conflicts: 2
-----

Fahad

2026-08-12 04:05

developer   ~0054198

Here we go. happy testing
suffixalignmentv5.patch.patch (56,384 bytes)   
diff --git a/scribus/numeration.h b/scribus/numeration.h
index 88857e0429..a3bad3aed5 100644
--- a/scribus/numeration.h
+++ b/scribus/numeration.h
@@ -77,6 +77,7 @@ struct NumStruct
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 10e5ba1dc5..01cad00b41 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -390,6 +390,7 @@ struct LineControl {
 	bool     wasFirstInRow { false };
 	double   leftIndent { 0.0 };
 	double   rightIndent { 0.0 };
+	double   suffixPad { 0.0 };
 	double   rightMargin { 0.0 };
 	double   mustLineEnd { false };
 	int      restartIndex { 0 };  //index of glyph run where line computing should be restarted
@@ -468,6 +469,7 @@ struct LineControl {
 		maxStretch = 0.0;
 		leftIndent = 0.0;
 		rightIndent = 0.0;
+		suffixPad = 0.0;
 		rightMargin = 0.0;
 		rowDesc = 0.0;
 	}
@@ -1084,6 +1086,152 @@ double calculateLineSpacing (const ParagraphStyle &style, PageItem *item)
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
+
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	double result;
+	if (!numS || level < 0 || level >= numS->m_maxCounters.count())
+	{
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	}
+	else
+	{
+		int maxCount = qMax(1, numS->m_maxCounters.at(level));
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
 
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
@@ -1400,8 +1548,8 @@ void PageItem_TextFrame::layout()
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,16 +1582,6 @@ void PageItem_TextFrame::layout()
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1816,7 +1954,7 @@ void PageItem_TextFrame::layout()
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1825,33 +1963,44 @@ void PageItem_TextFrame::layout()
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
 
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
+						}
+						else if (style.hasNum() && style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						{
+							// marker stays flush left (leftIndent untouched); instead we pad
+							// AFTER the marker so text always starts at a fixed column
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
+
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							current.suffixPad = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -2463,14 +2612,14 @@ void PageItem_TextFrame::layout()
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + current.suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + current.suffixPad;
 
 				if (DropCmode)
 				{
@@ -4946,6 +5095,7 @@ bool PageItem_TextFrame::checkKeyIsShortcut(QKeyEvent *k)
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5041,7 +5191,7 @@ void PageItem_TextFrame::updateBulletsNum()
 		}
 
 	}
-
+	itemText.blockSignals(false);
 	m_Doc->updateNumbers();
 }
 
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
index 424ddcfbcc..763a573bad 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format.cpp
@@ -3123,10 +3123,6 @@ void Scribus150Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@ PageItem* Scribus150Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
index a6e7b4baaa..ffebaef127 100644
--- a/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
+++ b/scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
@@ -842,8 +842,6 @@ void Scribus150Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
index c18650807f..a08b063961 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format.cpp
@@ -3131,10 +3131,6 @@ void Scribus170Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@ PageItem* Scribus170Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
diff --git a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
index 3b7a5ec1ca..1e8c88068e 100644
--- a/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
+++ b/scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
@@ -844,8 +844,6 @@ void Scribus170Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
index 350fd762ca..9327d796fb 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format.cpp
@@ -3635,10 +3635,6 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@ void Scribus171Format::readParagraphStyle(ScribusDoc *doc, ScXmlStreamReader& re
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@ PageItem* Scribus171Format::pasteItem(ScribusDoc *doc, const ScXmlStreamAttribut
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
diff --git a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
index 96e8b492aa..dc74f58d1c 100644
--- a/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
+++ b/scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
@@ -846,8 +846,6 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@ void Scribus171Format::putPStyle(ScXmlStreamWriter & docu, const ParagraphStyle
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
diff --git a/scribus/scribusdoc.cpp b/scribus/scribusdoc.cpp
index 58eeb12a87..f6f954bf5d 100644
--- a/scribus/scribusdoc.cpp
+++ b/scribus/scribusdoc.cpp
@@ -17740,6 +17740,11 @@ void ScribusDoc::setNumerationCounter(const QString& numName, int level, int num
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+			numS->m_maxCounters.append(0);
+		if (number > numS->m_maxCounters.at(level))
+			numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17747,7 +17752,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17827,6 +17832,7 @@ int ScribusDoc::updateLocalNums(StoryText& itemText)
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17910,7 +17916,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17966,7 +17971,6 @@ void ScribusDoc::updateNumbers(bool updateNumerations)
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
diff --git a/scribus/styles/paragraphstyle.attrdefs.cxx b/scribus/styles/paragraphstyle.attrdefs.cxx
index 238e71a5cf..bbd0c05251 100644
--- a/scribus/styles/paragraphstyle.attrdefs.cxx
+++ b/scribus/styles/paragraphstyle.attrdefs.cxx
@@ -40,7 +40,6 @@ ATTRDEF(bool, keepTogether, KeepTogether, false)
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@ ATTRDEF(int, numStart, NumStart, 1)
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
diff --git a/scribus/styles/paragraphstyle.cpp b/scribus/styles/paragraphstyle.cpp
index f182104f34..9384ef24e5 100644
--- a/scribus/styles/paragraphstyle.cpp
+++ b/scribus/styles/paragraphstyle.cpp
@@ -208,6 +208,14 @@ void ParagraphStyle::setStyle(const ParagraphStyle & other)
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@ static QString toXMLString(ParagraphStyle::AlignmentType val)
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -344,6 +357,12 @@ ParagraphStyle::AlignmentType parse<ParagraphStyle::AlignmentType>(const Xml_str
 	return parseEnum<ParagraphStyle::AlignmentType>(str);
 }
 
+template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
 template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
diff --git a/scribus/styles/paragraphstyle.h b/scribus/styles/paragraphstyle.h
index 4e5bac2a12..2f83037918 100644
--- a/scribus/styles/paragraphstyle.h
+++ b/scribus/styles/paragraphstyle.h
@@ -76,6 +76,13 @@ public:
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@ public:
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
diff --git a/scribus/text/sctext_shared.cpp b/scribus/text/sctext_shared.cpp
index d984c79019..03ecc35b12 100644
--- a/scribus/text/sctext_shared.cpp
+++ b/scribus/text/sctext_shared.cpp
@@ -68,6 +68,9 @@ void ScText_Shared::clear()
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	maxParEffectWidthCache.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
diff --git a/scribus/text/sctext_shared.h b/scribus/text/sctext_shared.h
index d16bd8267a..d22c0f7316 100644
--- a/scribus/text/sctext_shared.h
+++ b/scribus/text/sctext_shared.h
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,13 @@ public:
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
diff --git a/scribus/text/storytext.cpp b/scribus/text/storytext.cpp
index 4214bb583e..58793910a0 100644
--- a/scribus/text/storytext.cpp
+++ b/scribus/text/storytext.cpp
@@ -27,7 +27,6 @@ pageitem.cpp  -  description
 //FIXME: this include must go to sctextstruct.h !
 #include <QList>
 #include <cassert>  //added to make Fedora-5 happy
-
 #include "notesstyles.h"
 #include "scribusdoc.h"
 #include "sctext_shared.h"
@@ -699,6 +698,7 @@ void StoryText::removeChars(int pos, uint len)
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
+	d->plainTextValid = false;
 	invalidate(pos, length());
 }
 
@@ -773,6 +773,7 @@ void StoryText::insertChars(int pos, const QString& txt, bool applyNeighbourStyl
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +834,7 @@ void StoryText::insertCharsWithSoftHyphens(int pos, const QString& txt, bool app
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +862,7 @@ void StoryText::replaceChar(int pos, QChar ch)
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1025,8 @@ QString StoryText::plainText() const
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1041,8 @@ QString StoryText::plainText() const
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,6 +1396,11 @@ void StoryText::clearFlag(int pos, LayoutFlags flags)
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+
 
 const CharStyle & StoryText::charStyle() const
 {
@@ -2261,6 +2271,10 @@ void StoryText::invalidate(int firstItem, int endItem)
 		if (par)
 			par->charStyleContext()->invalidate();
 	}
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+
 	if (!signalsBlocked())
 		emit changed(firstItem, endItem);
 }
diff --git a/scribus/text/storytext.h b/scribus/text/storytext.h
index 7920d56aba..9aaf6effed 100644
--- a/scribus/text/storytext.h
+++ b/scribus/text/storytext.h
@@ -29,6 +29,7 @@ pageitem.cpp  -  description
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,10 @@ public:
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
diff --git a/scribus/ui/propertywidget_pareffect.cpp b/scribus/ui/propertywidget_pareffect.cpp
index 0e6ef1e520..1a3701ea52 100644
--- a/scribus/ui/propertywidget_pareffect.cpp
+++ b/scribus/ui/propertywidget_pareffect.cpp
@@ -45,6 +45,10 @@ PropertyWidget_ParEffect::PropertyWidget_ParEffect(QWidget *parent) : QFrame(par
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@ void PropertyWidget_ParEffect::setType(int id)
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@ void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@ void PropertyWidget_ParEffect::connectSignals()
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@ void PropertyWidget_ParEffect::disconnectSignals()
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@ void PropertyWidget_ParEffect::handleParEffectUse()
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@ void PropertyWidget_ParEffect::handlePEOffset(double offset)
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
diff --git a/scribus/ui/propertywidget_pareffect.h b/scribus/ui/propertywidget_pareffect.h
index 62729f7c28..54d2aeab46 100644
--- a/scribus/ui/propertywidget_pareffect.h
+++ b/scribus/ui/propertywidget_pareffect.h
@@ -66,7 +66,7 @@ public slots:
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
diff --git a/scribus/ui/propertywidget_pareffectbase.ui b/scribus/ui/propertywidget_pareffectbase.ui
index 1a2670a01a..fa21acf2e1 100644
--- a/scribus/ui/propertywidget_pareffectbase.ui
+++ b/scribus/ui/propertywidget_pareffectbase.ui
@@ -438,10 +438,17 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
           </property>
          </widget>
         </item>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
diff --git a/scribus/ui/smpstylewidget.cpp b/scribus/ui/smpstylewidget.cpp
index 12fde44283..d5e0fc6261 100644
--- a/scribus/ui/smpstylewidget.cpp
+++ b/scribus/ui/smpstylewidget.cpp
@@ -25,7 +25,6 @@ static bool isEqual(double a, double b)
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@ SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@ void SMPStyleWidget::languageChange()
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@ void SMPStyleWidget::show(ParagraphStyle *pstyle, QList<ParagraphStyle> &pstyles
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@ void SMPStyleWidget::fillPECombo()
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@ void SMPStyleWidget::setParagraphEffect(int index)
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
diff --git a/scribus/ui/smpstylewidget.h b/scribus/ui/smpstylewidget.h
index c985d7033b..4cb5c88bd9 100644
--- a/scribus/ui/smpstylewidget.h
+++ b/scribus/ui/smpstylewidget.h
@@ -52,6 +52,7 @@ private:
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
diff --git a/scribus/ui/smpstylewidget.ui b/scribus/ui/smpstylewidget.ui
index 8a6d90e316..f625519568 100644
--- a/scribus/ui/smpstylewidget.ui
+++ b/scribus/ui/smpstylewidget.ui
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@ A value of 0 means unlimited hyphenations.</string>
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@ A value of 0 means unlimited hyphenations.</string>
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@ A value of 0 means unlimited hyphenations.</string>
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@ A value of 0 means unlimited hyphenations.</string>
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@ A value of 0 means unlimited hyphenations.</string>
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@ A value of 0 means unlimited hyphenations.</string>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@ A value of 0 means unlimited hyphenations.</string>
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@ A value of 0 means unlimited hyphenations.</string>
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@ A value of 0 means unlimited hyphenations.</string>
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1545,46 +1556,6 @@ A value of 0 means unlimited hyphenations.</string>
               </layout>
              </widget>
             </item>
-            <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
             <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
@@ -1606,7 +1577,7 @@ A value of 0 means unlimited hyphenations.</string>
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@ A value of 0 means unlimited hyphenations.</string>
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
diff --git a/scribus/ui/smtextstyles.cpp b/scribus/ui/smtextstyles.cpp
index b021322dd6..390365f537 100644
--- a/scribus/ui/smtextstyles.cpp
+++ b/scribus/ui/smtextstyles.cpp
@@ -37,7 +37,6 @@ for which a new license (GPL+exception) is in place.
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@ void SMParagraphStyle::setupConnections()
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@ void SMParagraphStyle::removeConnections()
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@ void SMParagraphStyle::slotParEffectOffset()
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
diff --git a/scribus/ui/smtextstyles.h b/scribus/ui/smtextstyles.h
index 3dd02356a0..087870f222 100644
--- a/scribus/ui/smtextstyles.h
+++ b/scribus/ui/smtextstyles.h
@@ -77,7 +77,7 @@ private slots:
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignmentv5.patch.patch (56,384 bytes)   

qirat

2026-08-12 04:49

reporter   ~0054199

Thank you so much for this quick fix. Here is more though:
----
U scribus/numeration.h
U scribus/pageitem_textframe.cpp
U scribus/plugins/fileloader/scribus150format/scribus150format.cpp
U scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
U scribus/plugins/fileloader/scribus170format/scribus170format.cpp
U scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
U scribus/plugins/fileloader/scribus171format/scribus171format.cpp
U scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
U scribus/scribusdoc.cpp
> applied hunk @@ -17740,6 +17740,11 @@ with offset 16
> applied hunk @@ -17747,7 +17752,7 @@ with offset 16
> applied hunk @@ -17827,6 +17832,7 @@ with offset 16
> applied hunk @@ -17910,7 +17916,6 @@ with offset 16
> applied hunk @@ -17966,7 +17971,6 @@ with offset 16
U scribus/styles/paragraphstyle.attrdefs.cxx
U scribus/styles/paragraphstyle.cpp
U scribus/styles/paragraphstyle.h
U scribus/text/sctext_shared.cpp
U scribus/text/sctext_shared.h
U scribus/text/storytext.cpp
U scribus/text/storytext.h
U scribus/ui/propertywidget_pareffect.cpp
U scribus/ui/propertywidget_pareffect.h
U scribus/ui/propertywidget_pareffectbase.ui
U scribus/ui/smpstylewidget.cpp
U scribus/ui/smpstylewidget.h
U scribus/ui/smpstylewidget.ui
U scribus/ui/smtextstyles.cpp
U scribus/ui/smtextstyles.h
----
There still is some offset and it is a large number.

qirat

2026-08-12 07:53

reporter   ~0054200

I used AI to update the patch against the latest code. Things are mearly perfect as per my testing. 'Left' and 'Right' works fine. Only the 'Centre' looks a little off to me. (Unless AI messed something up, which I think it did not.)

My list is 100+ items that is why there is more space between number and text. Screenshots below.
suffix-align-v5.jpg (344,585 bytes)   
suffix-align-v5.jpg (344,585 bytes)   

qirat

2026-08-12 07:54

reporter   ~0054201

Sorry, I meant "...nearly* perfect..."

qirat

2026-08-12 09:33

reporter   ~0054202

Oh, and here is the updated patch (against r27770) if you would like to see the difference between the two to see what AI did to your code. I use an app called Meld on Linux.
suffix-alignment-v5.1.patch (52,052 bytes)   
Index: scribus/numeration.h
===================================================================
--- scribus/numeration.h	(revision 27770)
+++ scribus/numeration.h	(working copy)
@@ -77,6 +77,7 @@
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 

Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27770)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -390,6 +390,7 @@
 	bool     wasFirstInRow { false };
 	double   leftIndent { 0.0 };
 	double   rightIndent { 0.0 };
+	double   suffixPad { 0.0 };
 	double   rightMargin { 0.0 };
 	double   mustLineEnd { false };
 	int      restartIndex { 0 };  //index of glyph run where line computing should be restarted
@@ -468,6 +469,7 @@
 		maxStretch = 0.0;
 		leftIndent = 0.0;
 		rightIndent = 0.0;
+		suffixPad = 0.0;
 		rightMargin = 0.0;
 		rowDesc = 0.0;
 	}
@@ -1084,6 +1086,152 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
+
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	double result;
+	if (!numS || level < 0 || level >= numS->m_maxCounters.count())
+	{
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	}
+	else
+	{
+		int maxCount = qMax(1, numS->m_maxCounters.at(level));
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
 
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
@@ -1400,8 +1548,8 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,16 +1582,6 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1816,7 +1954,7 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1825,33 +1963,44 @@
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
-
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
+
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
+						}
+						else if (style.hasNum() && style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						{
+							// marker stays flush left (leftIndent untouched); instead we pad
+							// AFTER the marker so text always starts at a fixed column
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey,style);
+
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							current.suffixPad = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
 						}
+
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -2463,14 +2612,14 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + current.suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + current.suffixPad;
 
 				if (DropCmode)
 				{
@@ -4946,6 +5095,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5041,7 +5191,7 @@
 		}
 
 	}
-
+	itemText.blockSignals(false);
 	m_Doc->updateNumbers();
 }
 

Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))

Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())

Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))

Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())

Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");

Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27770)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())

Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27770)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -17756,6 +17756,11 @@
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+			numS->m_maxCounters.append(0);
+		if (number > numS->m_maxCounters.at(level))
+			numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17763,7 +17768,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17843,6 +17848,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17932,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17987,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 

Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27770)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)

Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27770)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);

Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27770)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }

Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27770)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,9 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	maxParEffectWidthCache.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 

Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27770)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,13 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**

Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27770)
+++ scribus/text/storytext.cpp	(working copy)
@@ -27,7 +27,6 @@
 //FIXME: this include must go to sctextstruct.h !
 #include <QList>
 #include <cassert>  //added to make Fedora-5 happy
-
 #include "notesstyles.h"
 #include "scribusdoc.h"
 #include "sctext_shared.h"
@@ -699,6 +698,7 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
+	d->plainTextValid = false;
 	invalidate(pos, length());
 }
 
@@ -773,6 +773,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +834,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +862,7 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1025,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1041,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,6 +1396,11 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+
 
 const CharStyle & StoryText::charStyle() const
 {
@@ -2261,6 +2271,10 @@
 		if (par)
 			par->charStyleContext()->invalidate();
 	}
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+
 	if (!signalsBlocked())
 		emit changed(firstItem, endItem);
 }

Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27770)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,10 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)

Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27770)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -45,6 +45,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +189,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +237,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +276,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +302,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +318,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +417,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +549,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 

Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27770)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:

Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27770)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -438,10 +438,17 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
           </property>
          </widget>
         </item>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>

Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27770)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)

Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27770)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);

Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27770)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>

Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27770)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 

Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27770)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffix-alignment-v5.1.patch (52,052 bytes)   

Fahad

2026-08-16 18:16

developer   ~0054215

Fixing the center alignment took longer than expected because I ran a stress test using a very large numbered list. During testing, I uncovered several performance bugs related to numbered lists, all of which are now resolved. With those fixed, I completed the center alignment fix. Attached is v6 of the patch along with the large test file. please test
big-list.sla.gz (33,458 bytes)
suffixalignv6.patch (61,331 bytes)   
Index: scribus/numeration.h
===================================================================
--- scribus/numeration.h	(revision 27773)
+++ scribus/numeration.h	(working copy)
@@ -77,6 +77,7 @@
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27773)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,7 +32,6 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
-
 #include "actionmanager.h"
 #include "appmodes.h"
 #include "canvas.h"
@@ -1084,7 +1083,162 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
 
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		probe.applyStyle(pos, style);
+		probe.applyCharStyle(pos, candidate.length(), style.charStyle());
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	int maxCountFromNumS = -1;
+	if (numS && level >= 0 && level < numS->m_maxCounters.count())
+		maxCountFromNumS = numS->m_maxCounters.at(level);
+
+	int maxCountFromLocal = -1;
+	if (maxCountFromNumS < 0)
+	{
+		QHash<int, int>& localMax = item->itemText.localNumMaxCounters();
+		auto localIt = localMax.constFind(level);
+		if (localIt != localMax.constEnd())
+			maxCountFromLocal = localIt.value();
+	}
+	double result;
+	if (maxCountFromNumS < 0 && maxCountFromLocal < 0)
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	else
+	{
+		int maxCount = qMax(1, (maxCountFromNumS >= 0) ? maxCountFromNumS : maxCountFromLocal);
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			candidates << candidateMarkerString(style, maxCount);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
+
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1328,10 +1482,12 @@
 		}
 		
 		// update Bullet & number list if any.
-		if (itemText.hasTextMarks() || itemText.hasBulletOrNum() ||  itemText.marksCountChanged())
+		if (!itemText.bulletsNumSynced() &&
+				(itemText.hasTextMarks() || itemText.hasBulletOrNum() || itemText.marksCountChanged()))
 		{
 			updateBulletsNum();
 			itemText.resetMarksCountChanged();
+			itemText.setBulletsNumSynced(true);
 		}
 
 		ITextContext* context = this;
@@ -1400,8 +1556,10 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
+		int suffixPadComputedForA = -1;
+		double suffixPadValue = 0.0;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,16 +1592,6 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1816,7 +1964,7 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1825,33 +1973,33 @@
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
 							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
+									cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
 							}
-
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
-							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
+							else // Center
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
 						}
+						if (style.direction() == ParagraphStyle::RTL)
+							current.rightIndent -= indentAdjust;
+						else
+							current.leftIndent -= indentAdjust;
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -2449,7 +2597,45 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				// suffixPadValue/suffixPadComputedForA are local to this loop,
+				// not stored on LineControl. An earlier version cached the pad
+				// on a LineControl member reset only in startLine(), but that
+				// went stale on a mid-line restart (correct on the first
+				// restart, silently 0 on later ones for the same marker).
+				// Recomputing here per glyph-run avoids that entirely.
+				bool wantsSuffixPad = BulNumMode && style.hasNum()
+						&& (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left
+							|| style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center);
+				if (wantsSuffixPad && suffixPadComputedForA != a)
+				{
+					// The marker (e.g. "1.") can shape into more than one glyph
+					// cluster, all reporting the same firstChar()==a. Only compute
+					// the width on the FIRST glyph we see for this `a` — the sum
+					// must start from the run's beginning, not from whichever
+					// glyph happens to be current, or it undercounts on later
+					// glyphs of the same marker.
+					double effectWidth = 0.0;
+					for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+					{
+						const auto& glyph = glyphClusters[j];
+						if (glyph.firstChar() != a)
+							break;
+						effectWidth += glyph.width();
+					}
+					const QString listKey = numListKey(style);
+					if (!cachedMaxParEffectWidthByList.contains(listKey))
+						cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
+					double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+					if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						suffixPadValue = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
+					else // Center: marker already moved by half the diff (indentAdjust
+						// above), so only the remaining half needs to go after it
+						// to bring the following text to the fixed column.
+						suffixPadValue = qMax(0.0, (maxParEffectWidth - style.parEffectOffset() - effectWidth) / 2.0);
+					suffixPadComputedForA = a;
+				}
+				double suffixPad = wantsSuffixPad ? suffixPadValue : 0.0;
+				current.xPos += style.parEffectOffset() + suffixPad;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2463,7 +2649,7 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
@@ -2470,7 +2656,7 @@
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + suffixPad;
 
 				if (DropCmode)
 				{
@@ -3087,7 +3273,6 @@
 
 NoRoom:
 	invalid = false;
-	
 	adjustParagraphEndings ();
 
 	if (verticalAlign > 0)
@@ -4946,6 +5131,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5039,10 +5225,9 @@
 			index--;
 			continue;
 		}
-
 	}
-
-	m_Doc->updateNumbers();
+		itemText.blockSignals(false);
+		m_Doc->updateNumbers();
 }
 
 
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27773)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9965,6 +9965,7 @@
 				if (start >= stop)
 					start = stop = currItem->itemText.normalizedCursorPosition();
 			}
+			currItem->itemText.blockSignals(true);
 			for (int pos = start; pos < stop; ++pos)
 			{
 				if (currItem->itemText.text(pos) == SpecialChars::PARSEP)
@@ -9989,6 +9990,10 @@
 				m_undoManager->action(currItem, is);
 			}
 			currItem->itemText.applyStyle(stop, newStyle, rmDirectFormatting);
+			currItem->itemText.blockSignals(false);
+			PageItem_TextFrame* currTextFrame = currItem->asTextFrame();
+			if (currTextFrame)
+				TextFrameSpellChecker::instance()->frameTextChanged(currTextFrame);
 			currItem->invalid = true;
 		}
 		if (currItem->isPathText())
@@ -17756,6 +17761,11 @@
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+		numS->m_maxCounters.append(0);
+	if (number > numS->m_maxCounters.at(level))
+		numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17763,7 +17773,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17828,6 +17838,13 @@
 		else
 			count++;
 		counters.replace(level, count);
+		// "<local block>" numbering has no NumStruct (nothing is registered
+		// under that name in doc->numerations), so track the running max
+		// per level on the story itself instead. maxParagraphEffectWidth()
+		// reads this for the left-suffix pad calculation.
+		QHash<int, int>& localMax = itemText.localNumMaxCounters();
+		if (count > localMax.value(level, 0))
+			localMax[level] = count;
 		//nums.insert(level, num);
 		QString result;
 		for (int i = 0; i <= level; ++i)
@@ -17843,6 +17860,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17944,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17999,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27773)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27773)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27773)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27773)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,8 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27773)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,23 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	/// highest counter value reached per level, for "<local block>" numbering
+	/// (local lists have no NumStruct to store this in, unlike named lists'
+	/// m_maxCounters, so we keep it here instead)
+	QHash<int, int> localNumMaxCounters;
+
+	/// true once updateBulletsNum() has synced marks for the current story
+	/// state; cleared on any edit so it reruns exactly once per real change,
+	/// not once per frame in the chain
+	bool bulletsNumSynced = false;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27773)
+++ scribus/text/storytext.cpp	(working copy)
@@ -561,6 +561,8 @@
 	// doesn't choke:
 	it->ch = QChar();
 	d->replaceCharStyleContextInParagraph(pos, paragraphStyle(pos+1).charStyleContext());
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 /**
@@ -581,6 +583,10 @@
 		}
 	}
 	d->replaceCharStyleContextInParagraph(pos, it->parstyle->charStyleContext());
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 // Returns the matched story length (including skipped diacritics), or -1.
@@ -699,7 +705,19 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
-	invalidate(pos, length());
+	// #context-invalidation only needs to reach the end of the CURRENT
+	// paragraph: each paragraph's ParagraphStyle owns its own, independent
+	// m_cstyleContext (see paragraphstyle.h), so removing chars here cannot
+	// change how a downstream, untouched paragraph resolves its style parent.
+	// The emit below still uses the full range -- layout genuinely needs to
+	// know everything from pos onward shifted.
+	int contextEnd = qMin(nextParagraph(pos) + 1, length());
+	invalidateContextOnly(pos, contextEnd);
+	d->plainTextValid = false;
+
+	if (!signalsBlocked())
+		emit changed(pos, length());
+
 }
 
 void StoryText::trim()
@@ -773,6 +791,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +852,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +880,7 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1043,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1059,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,7 +1414,26 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
 
+QHash<int, int>& StoryText::localNumMaxCounters()
+{
+	return d->localNumMaxCounters;
+}
+
+bool StoryText::bulletsNumSynced() const
+{
+	return d->bulletsNumSynced;
+}
+
+void StoryText::setBulletsNumSynced(bool synced)
+{
+	d->bulletsNumSynced = synced;
+}
+
 const CharStyle & StoryText::charStyle() const
 {
 	return charStyle(d->cursorPosition);
@@ -1599,6 +1641,8 @@
 			--i;
 		}
 	}
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -1630,6 +1674,8 @@
 		//		qDebug() << QString("applying parstyle %1 as defaultstyle for %2").arg(paragraphStyle(pos).name()).arg(pos);
 		d->trailingStyle.eraseStyle(style);
 	}
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -2249,6 +2295,10 @@
 
 void StoryText::invalidateAll()
 {
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	d->pstyleContext.invalidate();
 	invalidate(0, length());
 }
@@ -2265,6 +2315,16 @@
 		emit changed(firstItem, endItem);
 }
 
+void StoryText::invalidateContextOnly(int firstItem, int endItem)
+{
+	for (int i = firstItem; i < endItem; ++i)
+	{
+		ParagraphStyle* par = item(i)->parstyle;
+		if (par)
+			par->charStyleContext()->invalidate();
+	}
+}
+
 StoryTextSnapshot StoryText::createSnapshot() const
 {
 	// Delegate to the static factory method
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27773)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,19 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
+	/// highest counter reached per level for "<local block>" numbering;
+	/// shared across every frame referencing this story
+	QHash<int, int>& localNumMaxCounters();
+
+	/// true once bullet/number marks have been synced for the current
+	/// story state; cleared on any edit
+	bool bulletsNumSynced() const;
+	void setBulletsNumSynced(bool synced);
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
@@ -335,6 +349,8 @@
 	
 	/// mark these runs as invalid, ie. need itemize and shaping
 	void invalidate(int firstRun, int lastRun);
+	/// like invalidate(), but only resets paragraph-style contexts -- no signal
+	void invalidateContextOnly(int firstItem, int endItem);
 	void removeParSep(int pos);
 	void insertParSep(int pos);
 
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27773)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -220,7 +220,8 @@
 				else if (charStyle.name() != style.peCharStyleName())
 					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
 				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				if (!charStyle.equiv(m_story.charStyle(i)))
+					m_story.setCharStyle(i, 1, charStyle);
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
@@ -229,10 +230,12 @@
 				if (charStyle.parent() == style.peCharStyleName())
 				{
 					const QString& curParent(style.hasParent() ? style.parent() : style.name());
+					CharStyle newStyle(charStyle);
 					if (doc->charStyles().contains(style.peCharStyleName()))
-						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-					m_story.setCharStyle(i, 1, charStyle);
+						newStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
+					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					if (!newStyle.equiv(m_story.charStyle(i)))
+						m_story.setCharStyle(i, 1, newStyle);
 				}
 			}
 		}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27773)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -6,7 +6,6 @@
 */
 
 #include <QSignalBlocker>
-
 #include "propertywidget_pareffect.h"
 
 #include "appmodehelper.h"
@@ -45,6 +44,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +188,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +236,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +275,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +301,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +317,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +416,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +548,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
@@ -578,7 +592,6 @@
 		tempSelection.addItem(item);
 		m_doc->itemSelection_ApplyParagraphStyle(newStyle, &tempSelection);
 		m_doc->updateNumbers();
-
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 		connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
 	}
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27773)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27773)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -438,14 +438,21 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
           </property>
          </widget>
         </item>
         <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
+          </property>
+         </widget>
+        </item>
+        <item>
          <spacer name="horizontalSpacer_5">
           <property name="orientation">
            <enum>Qt::Orientation::Horizontal</enum>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27773)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27773)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27773)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27773)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27773)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignv6.patch (61,331 bytes)   

qirat

2026-08-17 10:19

reporter   ~0054218

It works fine for me, @Fahad. That 11250 list items in your file is a real stress test. It make Scribus a bit unresponsive for a few secs when you apply/make a change. But for 100 items, it is pretty fast. Ready, imho.

Here is a little issue: the Centre option is a bit off when going from 1 digit to 2, from 2 to 3 and even 3 to 4 (your list). Screenshot added.
suffix-align-v6.jpg (371,022 bytes)   
suffix-align-v6.jpg (371,022 bytes)   

qirat

2026-08-17 10:21

reporter   ~0054219

And thanks for updating it against the latest r27773. :)

Fahad

2026-08-18 14:02

developer   ~0054226

thanks @qirat for testing. You spot a really nasty bug it took me a long time to fix. However, here is a new version for testing.
suffixalignv7.patch (62,364 bytes)   
Index: scribus/numeration.h
===================================================================
--- scribus/numeration.h	(revision 27773)
+++ scribus/numeration.h	(working copy)
@@ -77,6 +77,7 @@
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27773)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,7 +32,6 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
-
 #include "actionmanager.h"
 #include "appmodes.h"
 #include "canvas.h"
@@ -1084,7 +1083,176 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Build the text a given count would produce for this list level.
+// Pure string formatting — no document access, no shaping.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
 
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	StoryText probe(item->doc());
+
+	for (const QString& candidate : candidates)
+	{
+		if (probe.length() > 0)
+			probe.insertChars(SpecialChars::PARSEP);
+
+		int pos = probe.length();
+		probe.insertChars(candidate);
+		CharStyle markerStyle = style.charStyle();
+
+		markerStyle.setFont(style.charStyle().font());
+		if (!style.peCharStyleName().isEmpty() && item->doc()->charStyles().contains(style.peCharStyleName()))
+					markerStyle.setParent(item->doc()->charStyle(style.peCharStyleName()).name());
+		probe.applyCharStyle(pos, candidate.length(), markerStyle);
+	}
+
+	QList<GlyphCluster> glyphClusters;
+	ShapedTextFeed feed(&probe, 0, item);
+	for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+		;
+	double maxWidth = 0.0, current = 0.0;
+	int prevA = -1;
+	for (const auto& glyph : as_const(glyphClusters))
+	{
+		int a = glyph.firstChar();
+		if (probe.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			maxWidth = qMax(maxWidth, current);
+			current = 0.0;
+		}
+		current += glyph.width();
+	}
+	return qMax(maxWidth, current);
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback only: used when counter data isn't available yet
+	// (e.g. numbering hasn't run for this list this pass).
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	int maxCountFromNumS = -1;
+	if (numS && level >= 0 && level < numS->m_maxCounters.count())
+		maxCountFromNumS = numS->m_maxCounters.at(level);
+	int maxCountFromLocal = -1;
+	if (maxCountFromNumS < 0)
+	{
+		QHash<int, int>& localMax = item->itemText.localNumMaxCounters();
+		auto localIt = localMax.constFind(level);
+		if (localIt != localMax.constEnd())
+			maxCountFromLocal = localIt.value();
+
+	}
+	double result;
+	if (maxCountFromNumS < 0 && maxCountFromLocal < 0)
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	else
+	{
+		int maxCount = qMax(1, (maxCountFromNumS >= 0) ? maxCountFromNumS : maxCountFromLocal);
+		auto fmt = static_cast<NumFormat>(style.numFormat());
+		QStringList candidates;
+		if (fmt == Type_1_2_3 || fmt == Type_1_2_3_ar)
+		{
+			// Digit widths aren't uniform in proportional fonts (e.g. Georgia),
+			// so the numerically highest count doesn't always shape the widest
+			// marker -- "108." measured wider than "112." in testing. Check
+			// every candidate with the same digit count as maxCount (that's
+			// always where the widest one lives, since digit count only grows
+			// with count) instead of assuming the top value wins.
+			int digitCount = QString::number(maxCount).length();
+			int rangeStart = 1;
+			for (int d = 1; d < digitCount; ++d)
+				rangeStart *= 10;
+			for (int n = rangeStart; n <= maxCount; ++n)
+				candidates << candidateMarkerString(style, n);
+		}
+		else
+		{
+			int longestLen = -1;
+			for (int n = 1; n <= maxCount; ++n)
+			{
+				QString candidate = candidateMarkerString(style, n);
+				if (candidate.length() > longestLen)
+				{
+					longestLen = candidate.length();
+					candidates.clear();
+					candidates << candidate;
+				}
+				else if (candidate.length() == longestLen)
+				{
+					candidates << candidate;
+				}
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
+
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1328,10 +1496,12 @@
 		}
 		
 		// update Bullet & number list if any.
-		if (itemText.hasTextMarks() || itemText.hasBulletOrNum() ||  itemText.marksCountChanged())
+		if (!itemText.bulletsNumSynced() &&
+				(itemText.hasTextMarks() || itemText.hasBulletOrNum() || itemText.marksCountChanged()))
 		{
 			updateBulletsNum();
 			itemText.resetMarksCountChanged();
+			itemText.setBulletsNumSynced(true);
 		}
 
 		ITextContext* context = this;
@@ -1400,8 +1570,10 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
+		int suffixPadComputedForA = -1;
+		double suffixPadValue = 0.0;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,16 +1606,6 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1816,7 +1978,7 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
@@ -1825,33 +1987,41 @@
 
 					if (BulNumMode || DropCmode)
 					{
-						if (style.parEffectIndent())
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
 							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
+									cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
 							}
-
-							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
-							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
+							else // Center
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
 						}
+						if (style.direction() == ParagraphStyle::RTL)
+						{
+							current.rightIndent -= indentAdjust;
+							if (current.rightIndent < 0.0)
+								current.rightIndent = 0.0;
+						}
+						else
+						{
+							current.leftIndent -= indentAdjust;
+							if (current.leftIndent < 0.0)
+								current.leftIndent = 0.0;
+						}
 					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
@@ -2449,7 +2619,45 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				// suffixPadValue/suffixPadComputedForA are local to this loop,
+				// not stored on LineControl. An earlier version cached the pad
+				// on a LineControl member reset only in startLine(), but that
+				// went stale on a mid-line restart (correct on the first
+				// restart, silently 0 on later ones for the same marker).
+				// Recomputing here per glyph-run avoids that entirely.
+				bool wantsSuffixPad = BulNumMode && style.hasNum()
+						&& (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left
+							|| style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center);
+				if (wantsSuffixPad && suffixPadComputedForA != a)
+				{
+					// The marker (e.g. "1.") can shape into more than one glyph
+					// cluster, all reporting the same firstChar()==a. Only compute
+					// the width on the FIRST glyph we see for this `a` — the sum
+					// must start from the run's beginning, not from whichever
+					// glyph happens to be current, or it undercounts on later
+					// glyphs of the same marker.
+					double effectWidth = 0.0;
+					for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+					{
+						const auto& glyph = glyphClusters[j];
+						if (glyph.firstChar() != a)
+							break;
+						effectWidth += glyph.width();
+					}
+					const QString listKey = numListKey(style);
+					if (!cachedMaxParEffectWidthByList.contains(listKey))
+						cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
+					double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+					if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						suffixPadValue = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
+					else // Center: marker already moved by half the diff (indentAdjust
+						// above), so only the remaining half needs to go after it
+						// to bring the following text to the fixed column.
+						suffixPadValue = qMax(0.0, (maxParEffectWidth - style.parEffectOffset() - effectWidth) / 2.0);
+					suffixPadComputedForA = a;
+				}
+				double suffixPad = wantsSuffixPad ? suffixPadValue : 0.0;
+				current.xPos += style.parEffectOffset() + suffixPad;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2463,7 +2671,7 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
@@ -2470,7 +2678,7 @@
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + suffixPad;
 
 				if (DropCmode)
 				{
@@ -3087,7 +3295,6 @@
 
 NoRoom:
 	invalid = false;
-	
 	adjustParagraphEndings ();
 
 	if (verticalAlign > 0)
@@ -4946,6 +5153,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5039,10 +5247,9 @@
 			index--;
 			continue;
 		}
-
 	}
-
-	m_Doc->updateNumbers();
+		itemText.blockSignals(false);
+		m_Doc->updateNumbers();
 }
 
 
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27773)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27773)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9965,6 +9965,7 @@
 				if (start >= stop)
 					start = stop = currItem->itemText.normalizedCursorPosition();
 			}
+			currItem->itemText.blockSignals(true);
 			for (int pos = start; pos < stop; ++pos)
 			{
 				if (currItem->itemText.text(pos) == SpecialChars::PARSEP)
@@ -9989,6 +9990,10 @@
 				m_undoManager->action(currItem, is);
 			}
 			currItem->itemText.applyStyle(stop, newStyle, rmDirectFormatting);
+			currItem->itemText.blockSignals(false);
+			PageItem_TextFrame* currTextFrame = currItem->asTextFrame();
+			if (currTextFrame)
+				TextFrameSpellChecker::instance()->frameTextChanged(currTextFrame);
 			currItem->invalid = true;
 		}
 		if (currItem->isPathText())
@@ -17756,6 +17761,11 @@
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+		numS->m_maxCounters.append(0);
+	if (number > numS->m_maxCounters.at(level))
+		numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17763,7 +17773,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17828,6 +17838,13 @@
 		else
 			count++;
 		counters.replace(level, count);
+		// "<local block>" numbering has no NumStruct (nothing is registered
+		// under that name in doc->numerations), so track the running max
+		// per level on the story itself instead. maxParagraphEffectWidth()
+		// reads this for the left-suffix pad calculation.
+		QHash<int, int>& localMax = itemText.localNumMaxCounters();
+		if (count > localMax.value(level, 0))
+			localMax[level] = count;
 		//nums.insert(level, num);
 		QString result;
 		for (int i = 0; i <= level; ++i)
@@ -17843,6 +17860,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17944,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17999,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27773)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27773)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27773)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27773)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,8 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27773)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,23 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	/// highest counter value reached per level, for "<local block>" numbering
+	/// (local lists have no NumStruct to store this in, unlike named lists'
+	/// m_maxCounters, so we keep it here instead)
+	QHash<int, int> localNumMaxCounters;
+
+	/// true once updateBulletsNum() has synced marks for the current story
+	/// state; cleared on any edit so it reruns exactly once per real change,
+	/// not once per frame in the chain
+	bool bulletsNumSynced = false;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27773)
+++ scribus/text/storytext.cpp	(working copy)
@@ -561,6 +561,8 @@
 	// doesn't choke:
 	it->ch = QChar();
 	d->replaceCharStyleContextInParagraph(pos, paragraphStyle(pos+1).charStyleContext());
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 /**
@@ -581,6 +583,10 @@
 		}
 	}
 	d->replaceCharStyleContextInParagraph(pos, it->parstyle->charStyleContext());
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 // Returns the matched story length (including skipped diacritics), or -1.
@@ -699,7 +705,19 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
-	invalidate(pos, length());
+	// #context-invalidation only needs to reach the end of the CURRENT
+	// paragraph: each paragraph's ParagraphStyle owns its own, independent
+	// m_cstyleContext (see paragraphstyle.h), so removing chars here cannot
+	// change how a downstream, untouched paragraph resolves its style parent.
+	// The emit below still uses the full range -- layout genuinely needs to
+	// know everything from pos onward shifted.
+	int contextEnd = qMin(nextParagraph(pos) + 1, length());
+	invalidateContextOnly(pos, contextEnd);
+	d->plainTextValid = false;
+
+	if (!signalsBlocked())
+		emit changed(pos, length());
+
 }
 
 void StoryText::trim()
@@ -773,6 +791,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +852,7 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +880,7 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1043,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1059,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,7 +1414,26 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
 
+QHash<int, int>& StoryText::localNumMaxCounters()
+{
+	return d->localNumMaxCounters;
+}
+
+bool StoryText::bulletsNumSynced() const
+{
+	return d->bulletsNumSynced;
+}
+
+void StoryText::setBulletsNumSynced(bool synced)
+{
+	d->bulletsNumSynced = synced;
+}
+
 const CharStyle & StoryText::charStyle() const
 {
 	return charStyle(d->cursorPosition);
@@ -1599,6 +1641,8 @@
 			--i;
 		}
 	}
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -1630,6 +1674,8 @@
 		//		qDebug() << QString("applying parstyle %1 as defaultstyle for %2").arg(paragraphStyle(pos).name()).arg(pos);
 		d->trailingStyle.eraseStyle(style);
 	}
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -2249,6 +2295,10 @@
 
 void StoryText::invalidateAll()
 {
+	// a list marker's max width can depend on any character in the story,
+	// so any edit can change it; drop the whole cache to stay safe
+	d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 	d->pstyleContext.invalidate();
 	invalidate(0, length());
 }
@@ -2265,6 +2315,16 @@
 		emit changed(firstItem, endItem);
 }
 
+void StoryText::invalidateContextOnly(int firstItem, int endItem)
+{
+	for (int i = firstItem; i < endItem; ++i)
+	{
+		ParagraphStyle* par = item(i)->parstyle;
+		if (par)
+			par->charStyleContext()->invalidate();
+	}
+}
+
 StoryTextSnapshot StoryText::createSnapshot() const
 {
 	// Delegate to the static factory method
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27773)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,19 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
+	/// highest counter reached per level for "<local block>" numbering;
+	/// shared across every frame referencing this story
+	QHash<int, int>& localNumMaxCounters();
+
+	/// true once bullet/number marks have been synced for the current
+	/// story state; cleared on any edit
+	bool bulletsNumSynced() const;
+	void setBulletsNumSynced(bool synced);
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
@@ -335,6 +349,8 @@
 	
 	/// mark these runs as invalid, ie. need itemize and shaping
 	void invalidate(int firstRun, int lastRun);
+	/// like invalidate(), but only resets paragraph-style contexts -- no signal
+	void invalidateContextOnly(int firstItem, int endItem);
 	void removeParSep(int pos);
 	void insertParSep(int pos);
 
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27773)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -220,7 +220,8 @@
 				else if (charStyle.name() != style.peCharStyleName())
 					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
 				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				if (!charStyle.equiv(m_story.charStyle(i)))
+					m_story.setCharStyle(i, 1, charStyle);
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
@@ -229,10 +230,12 @@
 				if (charStyle.parent() == style.peCharStyleName())
 				{
 					const QString& curParent(style.hasParent() ? style.parent() : style.name());
+					CharStyle newStyle(charStyle);
 					if (doc->charStyles().contains(style.peCharStyleName()))
-						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-					m_story.setCharStyle(i, 1, charStyle);
+						newStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
+					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					if (!newStyle.equiv(m_story.charStyle(i)))
+						m_story.setCharStyle(i, 1, newStyle);
 				}
 			}
 		}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27773)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -6,7 +6,6 @@
 */
 
 #include <QSignalBlocker>
-
 #include "propertywidget_pareffect.h"
 
 #include "appmodehelper.h"
@@ -45,6 +44,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +188,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +236,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +275,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +301,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +317,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +416,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +548,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
@@ -578,7 +592,6 @@
 		tempSelection.addItem(item);
 		m_doc->itemSelection_ApplyParagraphStyle(newStyle, &tempSelection);
 		m_doc->updateNumbers();
-
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 		connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
 	}
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27773)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27773)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -438,14 +438,21 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
           </property>
          </widget>
         </item>
         <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
+          </property>
+         </widget>
+        </item>
+        <item>
          <spacer name="horizontalSpacer_5">
           <property name="orientation">
            <enum>Qt::Orientation::Horizontal</enum>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27773)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27773)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27773)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27773)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27773)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignv7.patch (62,364 bytes)   

qirat

2026-08-18 16:03

reporter   ~0054227

Unfortunately, it is the same for me here as before. However I have a few other but related things to say, @Fahad.

qirat

2026-08-18 16:04

reporter   ~0054228

@Fahad, here is something I have been pondering. The name of the option/feature seem off to me. As far as I know I was the first who requested such a feature in 0017768 where you could align the suffix. But it rather went to become the name/label of the feature/option: Suffix Align.

#MISLEADING NAME
I think this is a little misleading. Think for a moment, if it is the 'suffix' to be aligned the list numbers would be aligned/look the same no matter which option is selected. It would look like as the currently 'Right' alignment is, because dot is the 'suffix' here and dot is just the same size. Type 5 dots in 5 lines and align the however you want they look the same in all alignments, left/right/centre.

#RENAME
So I propose a rename of the feature to "Marker Alignment" and it would solve so much (not only the user confusion).
The label can be renamed to "Marker Align" and the combo has four options: Left, Right, Centre, Suffix.

Here, Left/Right/Centre belong to the 'NUMBER' itself (especially when there is no suffix used) and align them according to the option.
Left (aligns left side of the numbers):
5
6
10

Right (aligns right side of the numbers):
  5
  6
10

Centre (aligns centres of the numbers):
(it is hard to do in text, I might add a screenshot, you maybe already got the point)

The Suffix option belongs to the character used as a suffix, and then aligns that char/suffix. It does not matter left/right/centre, it is the same for a uniform character, whichever side is safe/easy to code/program. It brings the numbers to look like they look in the 'Right' option (just described above, or the 'Right' option in v7 of the patch) because a uniform character aligns the same no matter left, right or centre.

Or, we could remove 'suffix' altogether since 'Right' does the same to numbers and if a user uses a suffix, it could just sit there with the number (however the number is aligned and suffix just follow the number).

I hope this makes sense. What do you think @ale ?

ale

2026-08-20 13:47

manager   ~0054251

Last edited: 2026-08-20 13:51

Testing v7.

It mostly works, but I could find a few issues:


  • the first character being typed stays left until a new line is created
  • applying RTL need to be done twice to get a correct result
  • the * listing is pretty silly but does not seem to be working anymore (no * shown. the screenshot below shows how 1.6 renders)
  • the second line does not align
  • the indenting for the roman numbering is too wide
list-asterisk.png (5,810 bytes)   
list-asterisk.png (5,810 bytes)   
list-first-character.gif (28,057 bytes)   
list-first-character.gif (28,057 bytes)   
list-no-hanging-indent.png (26,561 bytes)   
list-no-hanging-indent.png (26,561 bytes)   
list-roman-too-wide.png (25,886 bytes)   
list-roman-too-wide.png (25,886 bytes)   
list-rtl-twice-apply.gif (123,728 bytes)   
list-rtl-twice-apply.gif (123,728 bytes)   

Fahad

2026-09-04 06:34

developer   ~0054411

hi @ale I have fixed all issues you caught.

Please test
suffixalignv8.patch (63,093 bytes)   
Index: scribus/numeration.h
===================================================================
--- scribus/numeration.h	(revision 27810)
+++ scribus/numeration.h	(working copy)
@@ -77,6 +77,7 @@
 	QString m_name;
 	QList<Numeration> m_nums;
 	QList<int> m_counters;
+	QList<int> m_maxCounters;   // highest value m_counters ever reached, per level
 	int m_lastlevel { -1 };
 };
 
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27810)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,7 +32,6 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
-
 #include "actionmanager.h"
 #include "appmodes.h"
 #include "canvas.h"
@@ -1084,7 +1083,156 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style)
+{
+	return style.numName() + QLatin1Char('\x1f')
+		+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+		+ style.numPrefix() + QLatin1Char('\x1f')
+		+ style.numSuffix() + QLatin1Char('\x1f')
+		+ QString::number(style.numLevel());
+}
+// Builds the marker string for a given list count.
+static QString candidateMarkerString(const ParagraphStyle& style, int count)
+{
+	Numeration num;
+	num.numFormat = static_cast<NumFormat>(style.numFormat());
+	QString result = style.numPrefix();
+	result += num.numString(count);
+	result += style.numSuffix();
+	return result;
+}
 
+// Shape one or more short candidate strings through the real font/kerning
+// path and return the widest rendered result. The candidates are synthetic
+// (not real document text), so this never depends on itemText or frame chains.
+static double shapeCandidateWidths(PageItem* item, const ParagraphStyle& style, const QStringList& candidates)
+{
+	CharStyle markerStyle = style.charStyle();
+	markerStyle.setFont(style.charStyle().font());
+	if (!style.peCharStyleName().isEmpty() && item->doc()->charStyles().contains(style.peCharStyleName()))
+		markerStyle.setParent(item->doc()->charStyle(style.peCharStyleName()).name());
+
+	// Measure each candidate in a small temporary story. The probe is
+	// cleared before each candidate so insertion cost stays constant.
+	// Shaping uses the normal text layout path, so kerning, prefix,
+	// suffix and all number formats are handled correctly.
+	StoryText probe(item->doc());
+	probe.blockSignals(true);
+	double maxWidth = 0.0;
+	for (const QString& candidate : candidates)
+	{
+		probe.clear();
+		probe.insertChars(candidate);
+		probe.applyCharStyle(0, candidate.length(), markerStyle);
+
+		QList<GlyphCluster> glyphClusters;
+		ShapedTextFeed feed(&probe, 0, item);
+		for (int i = 0; feed.haveMoreText(i, glyphClusters); ++i)
+			;
+		double width = 0.0;
+		for (const auto& glyph : as_const(glyphClusters))
+			width += glyph.width();
+		maxWidth = qMax(maxWidth, width);
+	}
+	return maxWidth;
+}
+static double maxParagraphEffectWidthLegacyScan(PageItem* item, const QString& targetListKey)
+{
+	// Fallback used when counter data is not available yet.
+	ShapedTextFeed shapedText(&item->itemText, 0, item);
+
+	QList<GlyphCluster> glyphClusters;
+	for (int j = 0; shapedText.haveMoreText(j, glyphClusters); ++j)
+		;
+
+	double maxParEffectWidth = 0.0;
+	int prevA = -1;
+
+	for (int j = 0; j < glyphClusters.count(); ++j)
+	{
+		int a = glyphClusters[j].firstChar();
+		if (item->itemText.isBlockStart(a) && a != prevA)
+		{
+			prevA = a;
+			const ParagraphStyle& pStyle = item->itemText.paragraphStyle(a);
+			if (pStyle.hasNum() && numListKey(pStyle) == targetListKey)
+			{
+				double effectWidth = 0.0;
+				for (int k = j; k < glyphClusters.count(); ++k)
+				{
+					const auto& glyph = glyphClusters[k];
+					if (glyph.firstChar() != a)
+						break;
+					effectWidth += glyph.width();
+				}
+
+				double totalWidth = pStyle.parEffectOffset() + effectWidth;
+				if (totalWidth > maxParEffectWidth)
+					maxParEffectWidth = totalWidth;
+			}
+		}
+	}
+	return maxParEffectWidth;
+}
+
+static double maxParagraphEffectWidth(PageItem* item, const QString& targetListKey, const ParagraphStyle& style)
+{
+	QHash<QString, double>& sharedCache = item->itemText.maxParEffectWidthCache();
+	auto cacheIt = sharedCache.constFind(targetListKey);
+	if (cacheIt != sharedCache.constEnd())
+		return cacheIt.value();
+	ScribusDoc* doc = item->doc();
+	NumStruct* numS = doc ? doc->numerations.value(style.numName(), nullptr) : nullptr;
+	int level = style.numLevel();
+
+	int maxCountFromNumS = -1;
+	if (numS && level >= 0 && level < numS->m_maxCounters.count())
+		maxCountFromNumS = numS->m_maxCounters.at(level);
+	int maxCountFromLocal = -1;
+	if (maxCountFromNumS < 0)
+	{
+		QHash<int, int>& localMax = item->itemText.localNumMaxCounters();
+		auto localIt = localMax.constFind(level);
+		if (localIt != localMax.constEnd())
+			maxCountFromLocal = localIt.value();
+
+	}
+	double result;
+	if (maxCountFromNumS < 0 && maxCountFromLocal < 0)
+		result = maxParagraphEffectWidthLegacyScan(item, targetListKey);
+	else
+	{
+		int maxCount = qMax(1, (maxCountFromNumS >= 0) ? maxCountFromNumS : maxCountFromLocal);
+		// Marker string length is not a reliable proxy for value (Roman
+		// numerals) and rendered width is not uniform across same-length
+		// strings in proportional fonts ("108." vs "112." in Georgia).
+		// So: pre-filter candidates down to whichever are tied for the
+		// longest string, then let shapeCandidateWidths() pick the widest
+		// of those by actual rendered width. This is correct for every
+		// numFormat, decimal included, so one loop covers all of them.
+		int minCount = qMax(1, style.numStart());
+		int longestLen = -1;
+		QStringList candidates;
+		for (int n = minCount; n <= maxCount; ++n)
+		{
+			QString candidate = candidateMarkerString(style, n);
+			if (candidate.length() > longestLen)
+			{
+				longestLen = candidate.length();
+				candidates.clear();
+				candidates << candidate;
+			}
+			else if (candidate.length() == longestLen)
+			{
+				candidates << candidate;
+			}
+		}
+		result = style.parEffectOffset() + shapeCandidateWidths(item, style, candidates);
+	}
+	sharedCache.insert(targetListKey, result);
+	return result;
+}
+
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1328,11 +1476,13 @@
 		}
 		
 		// update Bullet & number list if any.
-		if (itemText.hasTextMarks() || itemText.hasBulletOrNum() ||  itemText.marksCountChanged())
+		if (!itemText.bulletsNumSynced() &&
+				(itemText.hasTextMarks() || itemText.hasBulletOrNum() || itemText.marksCountChanged()))
 		{
 			updateBulletsNum();
 			itemText.resetMarksCountChanged();
 		}
+		itemText.setBulletsNumSynced(true);
 
 		ITextContext* context = this;
 		//TextShaper textShaper(this, itemText, firstInFrame());
@@ -1400,8 +1550,10 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		QHash<QString, double> cachedMaxParEffectWidthByList;
+		int suffixPadComputedForA = -1;
+		double suffixPadValue = 0.0;
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,16 +1586,6 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
-				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
-				}
-				else
-				{
-					autoLeftIndent = 0.0;
-				}
 				style = itemText.paragraphStyle(a);
 				if (style.hasBullet() || style.hasNum())
 				{
@@ -1816,43 +1958,68 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
 						}
 					}
+					// ---- Numbered lists: align all lines with the widest marker ----
+					if (style.hasNum())
+					{
+						const QString listKey = numListKey(style);
+						if (!cachedMaxParEffectWidthByList.contains(listKey))
+							cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
+						double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
 
-					if (BulNumMode || DropCmode)
-					{
-						if (style.parEffectIndent())
+						// Lines WITHOUT the marker need the full width as indent
+						if (!BulNumMode)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
-
 							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+								current.rightIndent += maxParEffectWidth;
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								current.leftIndent  += maxParEffectWidth;
 						}
+						// Lines WITH the marker: no extra indent here – suffixPad (Left) or
+						// indentAdjust (Right/Center) will be applied later.
 					}
+
+					if (BulNumMode || DropCmode)
+					{
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+						{
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right ||
+							style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center)
+						{
+							const QString listKey = numListKey(style);
+							if (!cachedMaxParEffectWidthByList.contains(listKey))
+								cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
+							double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+							if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Right)
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth);
+							else // Center
+								indentAdjust = style.parEffectOffset() + (effectWidth - maxParEffectWidth) / 2.0;
+						}
+						if (style.direction() == ParagraphStyle::RTL)
+						{
+							current.rightIndent -= indentAdjust;
+							if (current.rightIndent < 0.0)
+								current.rightIndent = 0.0;
+						}
+						else
+						{
+							current.leftIndent -= indentAdjust;
+							if (current.leftIndent < 0.0)
+								current.leftIndent = 0.0;
+						}
+					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
 					if (rtlDropFollow)
@@ -2449,7 +2616,45 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				// suffixPadValue/suffixPadComputedForA are local to this loop
+				// rather than stored on LineControl, because a LineControl
+				// member would only be safe to reset in startLine() -- and a
+				// mid-line restart doesn't call startLine() again for the
+				// same marker, so a stored value would go stale on restart.
+				// Recomputing per glyph-run avoids that.
+				bool wantsSuffixPad = BulNumMode && style.hasNum()
+						&& (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left
+							|| style.suffixAlignment() == ParagraphStyle::SuffixAlign_Center);
+				if (wantsSuffixPad && suffixPadComputedForA != a)
+				{
+					// The marker (e.g. "1.") can shape into more than one glyph
+					// cluster, all reporting the same firstChar()==a. Only compute
+					// the width on the FIRST glyph we see for this `a` — the sum
+					// must start from the run's beginning, not from whichever
+					// glyph happens to be current, or it undercounts on later
+					// glyphs of the same marker.
+					double effectWidth = 0.0;
+					for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+					{
+						const auto& glyph = glyphClusters[j];
+						if (glyph.firstChar() != a)
+							break;
+						effectWidth += glyph.width();
+					}
+					const QString listKey = numListKey(style);
+					if (!cachedMaxParEffectWidthByList.contains(listKey))
+						cachedMaxParEffectWidthByList[listKey] = maxParagraphEffectWidth(this, listKey, style);
+					double maxParEffectWidth = cachedMaxParEffectWidthByList.value(listKey);
+					if (style.suffixAlignment() == ParagraphStyle::SuffixAlign_Left)
+						suffixPadValue = qMax(0.0, maxParEffectWidth - style.parEffectOffset() - effectWidth);
+					else // Center: marker already moved by half the diff (indentAdjust
+						// above), so only the remaining half needs to go after it
+						// to bring the following text to the fixed column.
+						suffixPadValue = qMax(0.0, (maxParEffectWidth - style.parEffectOffset() - effectWidth) / 2.0);
+					suffixPadComputedForA = a;
+				}
+				double suffixPad = wantsSuffixPad ? suffixPadValue : 0.0;
+				current.xPos += style.parEffectOffset() + suffixPad;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2463,7 +2668,7 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + suffixPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
@@ -2470,7 +2675,7 @@
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + suffixPad;
 
 				if (DropCmode)
 				{
@@ -3087,7 +3292,6 @@
 
 NoRoom:
 	invalid = false;
-	
 	adjustParagraphEndings ();
 
 	if (verticalAlign > 0)
@@ -4946,6 +5150,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5039,10 +5244,9 @@
 			index--;
 			continue;
 		}
-
 	}
-
-	m_Doc->updateNumbers();
+		itemText.blockSignals(false);
+		m_Doc->updateNumbers();
 }
 
 
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString SuffixAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(SuffixAlignment))
+		newStyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt(SuffixAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setSuffixAlignment(static_cast<ParagraphStyle::SuffixAlignment>(attrs.valueAsInt("SuffixAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27810)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhSuffixAlignment())
+		docu.writeAttribute("SuffixAlignment", style.suffixAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27810)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9965,6 +9965,7 @@
 				if (start >= stop)
 					start = stop = currItem->itemText.normalizedCursorPosition();
 			}
+			currItem->itemText.blockSignals(true);
 			for (int pos = start; pos < stop; ++pos)
 			{
 				if (currItem->itemText.text(pos) == SpecialChars::PARSEP)
@@ -9989,6 +9990,10 @@
 				m_undoManager->action(currItem, is);
 			}
 			currItem->itemText.applyStyle(stop, newStyle, rmDirectFormatting);
+			currItem->itemText.blockSignals(false);
+			PageItem_TextFrame* currTextFrame = currItem->asTextFrame();
+			if (currTextFrame)
+				TextFrameSpellChecker::instance()->frameTextChanged(currTextFrame);
 			currItem->invalid = true;
 		}
 		if (currItem->isPathText())
@@ -17756,6 +17761,11 @@
 		numS->m_counters.insert(level, number);
 	else
 		numS->m_counters.replace(level, number);
+
+	while (numS->m_maxCounters.count() <= level)
+		numS->m_maxCounters.append(0);
+	if (number > numS->m_maxCounters.at(level))
+		numS->m_maxCounters.replace(level, number);
 }
 
 int ScribusDoc::updateLocalNums(StoryText& itemText)
@@ -17763,7 +17773,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17828,6 +17838,13 @@
 		else
 			count++;
 		counters.replace(level, count);
+		// "<local block>" numbering has no NumStruct (nothing is registered
+		// under that name in doc->numerations), so track the running max
+		// per level on the story itself instead. maxParagraphEffectWidth()
+		// reads this for the left-suffix pad calculation.
+		QHash<int, int>& localMax = itemText.localNumMaxCounters();
+		if (count > localMax.value(level, 0))
+			localMax[level] = count;
 		//nums.insert(level, num);
 		QString result;
 		for (int i = 0; i <= level; ++i)
@@ -17843,6 +17860,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17944,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17999,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27810)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::SuffixAlignment, suffixAlignment, SuffixAlignment, ParagraphStyle::SuffixAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27810)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::SuffixAlignment ParagraphStyle::flipSuffixAlignmentForRTL(SuffixAlignment align) const
+{
+	if (align == ParagraphStyle::SuffixAlign_Left)
+		return ParagraphStyle::SuffixAlign_Right;
+	if (align == ParagraphStyle::SuffixAlign_Right)
+		return ParagraphStyle::SuffixAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::SuffixAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::SuffixAlignment parse<ParagraphStyle::SuffixAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::SuffixAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27810)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum SuffixAlignment
+	{
+		SuffixAlign_Left   = 0,
+		SuffixAlign_Center = 1,
+		SuffixAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::SuffixAlignment flipSuffixAlignmentForRTL(ParagraphStyle::SuffixAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27810)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,11 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	localNumMaxCounters.clear();
+	cachedPlainText.clear();
+	plainTextValid = false;
+	bulletsNumSynced = false;
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27810)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,23 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	/// highest counter value reached per level, for "<local block>" numbering
+	/// (local lists have no NumStruct to store this in, unlike named lists'
+	/// m_maxCounters, so we keep it here instead)
+	QHash<int, int> localNumMaxCounters;
+
+	/// true once updateBulletsNum() has synced marks for the current story
+	/// state; cleared on any edit so it reruns exactly once per real change,
+	/// not once per frame in the chain
+	bool bulletsNumSynced = false;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+
 	void clear();
 	
 	/**
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27810)
+++ scribus/text/storytext.cpp	(working copy)
@@ -561,6 +561,10 @@
 	// doesn't choke:
 	it->ch = QChar();
 	d->replaceCharStyleContextInParagraph(pos, paragraphStyle(pos+1).charStyleContext());
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 }
 
 /**
@@ -581,6 +585,9 @@
 		}
 	}
 	d->replaceCharStyleContextInParagraph(pos, it->parstyle->charStyleContext());
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 // Returns the matched story length (including skipped diacritics), or -1.
@@ -699,7 +706,20 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
-	invalidate(pos, length());
+	// #context-invalidation only needs to reach the end of the CURRENT
+	// paragraph: each paragraph's ParagraphStyle owns its own, independent
+	// m_cstyleContext (see paragraphstyle.h), so removing chars here cannot
+	// change how a downstream, untouched paragraph resolves its style parent.
+	// The emit below still uses the full range -- layout genuinely needs to
+	// know everything from pos onward shifted.
+	int contextEnd = qMin(nextParagraph(pos) + 1, length());
+	invalidateContextOnly(pos, contextEnd);
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
+
+	if (!signalsBlocked())
+		emit changed(pos, length());
+
 }
 
 void StoryText::trim()
@@ -773,6 +793,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +855,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +884,8 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1048,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1064,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,7 +1419,26 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
 
+QHash<int, int>& StoryText::localNumMaxCounters()
+{
+	return d->localNumMaxCounters;
+}
+
+bool StoryText::bulletsNumSynced() const
+{
+	return d->bulletsNumSynced;
+}
+
+void StoryText::setBulletsNumSynced(bool synced)
+{
+	d->bulletsNumSynced = synced;
+}
+
 const CharStyle & StoryText::charStyle() const
 {
 	return charStyle(d->cursorPosition);
@@ -1599,6 +1646,10 @@
 			--i;
 		}
 	}
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -1630,6 +1681,11 @@
 		//		qDebug() << QString("applying parstyle %1 as defaultstyle for %2").arg(paragraphStyle(pos).name()).arg(pos);
 		d->trailingStyle.eraseStyle(style);
 	}
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -2249,6 +2305,13 @@
 
 void StoryText::invalidateAll()
 {
+	// Clear the marker-width cache when the story is invalidated.
+	// Skip this while signals are blocked because temporary probe stories
+	// may be invalidated repeatedly.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	d->pstyleContext.invalidate();
 	invalidate(0, length());
 }
@@ -2265,6 +2328,16 @@
 		emit changed(firstItem, endItem);
 }
 
+void StoryText::invalidateContextOnly(int firstItem, int endItem)
+{
+	for (int i = firstItem; i < endItem; ++i)
+	{
+		ParagraphStyle* par = item(i)->parstyle;
+		if (par)
+			par->charStyleContext()->invalidate();
+	}
+}
+
 StoryTextSnapshot StoryText::createSnapshot() const
 {
 	// Delegate to the static factory method
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27810)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,19 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
+	/// highest counter reached per level for "<local block>" numbering;
+	/// shared across every frame referencing this story
+	QHash<int, int>& localNumMaxCounters();
+
+	/// true once bullet/number marks have been synced for the current
+	/// story state; cleared on any edit
+	bool bulletsNumSynced() const;
+	void setBulletsNumSynced(bool synced);
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
@@ -335,6 +349,8 @@
 	
 	/// mark these runs as invalid, ie. need itemize and shaping
 	void invalidate(int firstRun, int lastRun);
+	/// like invalidate(), but only resets paragraph-style contexts -- no signal
+	void invalidateContextOnly(int firstItem, int endItem);
 	void removeParSep(int pos);
 	void insertParSep(int pos);
 
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27810)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -220,7 +220,8 @@
 				else if (charStyle.name() != style.peCharStyleName())
 					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
 				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				if (!charStyle.equiv(m_story.charStyle(i)))
+					m_story.setCharStyle(i, 1, charStyle);
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
@@ -229,10 +230,12 @@
 				if (charStyle.parent() == style.peCharStyleName())
 				{
 					const QString& curParent(style.hasParent() ? style.parent() : style.name());
+					CharStyle newStyle(charStyle);
 					if (doc->charStyles().contains(style.peCharStyleName()))
-						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-					m_story.setCharStyle(i, 1, charStyle);
+						newStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
+					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					if (!newStyle.equiv(m_story.charStyle(i)))
+						m_story.setCharStyle(i, 1, newStyle);
 				}
 			}
 		}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27810)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -6,7 +6,6 @@
 */
 
 #include <QSignalBlocker>
-
 #include "propertywidget_pareffect.h"
 
 #include "appmodehelper.h"
@@ -45,6 +44,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +188,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +236,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(suffixAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +275,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::SuffixAlignment displayAlign = newPStyle.suffixAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipSuffixAlignmentForRTL(displayAlign);
+
+	int idx = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		suffixAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +301,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +317,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleSuffixAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +416,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +548,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleSuffixAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::SuffixAlignment>(suffixAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipSuffixAlignmentForRTL(align);
+	newStyle.setSuffixAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
@@ -578,7 +592,6 @@
 		tempSelection.addItem(item);
 		m_doc->itemSelection_ApplyParagraphStyle(newStyle, &tempSelection);
 		m_doc->updateNumbers();
-
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 		connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
 	}
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27810)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleSuffixAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27810)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -438,14 +438,21 @@
           </layout>
          </widget>
         </item>
-        <item>
-         <widget class="QCheckBox" name="peIndent">
+<item>
+         <widget class="QLabel" name="suffixAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Suffix Align:</string>
           </property>
          </widget>
         </item>
         <item>
+         <widget class="QComboBox" name="suffixAlignmentCombo">
+          <property name="toolTip">
+           <string>Alignment of suffix characters in numbered lists</string>
+          </property>
+         </widget>
+        </item>
+        <item>
          <spacer name="horizontalSpacer_5">
           <property name="orientation">
            <enum>Qt::Orientation::Horizontal</enum>
@@ -556,8 +563,8 @@
   <tabstop>numStart</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
-  <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+<tabstop>peOffset</tabstop>
+  <tabstop>suffixAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27810)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillSuffixAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillSuffixAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::SuffixAlignment parentDisplayAlign = parent->suffixAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipSuffixAlignmentForRTL(parentDisplayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem(suffixAlignIndex, pstyle->isInhSuffixAlignment());
+		suffixAlignmentCombo->setParentItem(suffixAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::SuffixAlignment displayAlign = pstyle->suffixAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipSuffixAlignmentForRTL(displayAlign);
+
+		int suffixAlignIndex = suffixAlignmentCombo->findData(static_cast<int>(displayAlign));
+		suffixAlignmentCombo->setCurrentItem((suffixAlignIndex >= 0) ? suffixAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillSuffixAlignmentCombo()
+{
+	QSignalBlocker sb(suffixAlignmentCombo);
+	int currIndex = suffixAlignmentCombo->currentIndex();
+	suffixAlignmentCombo->clear();
+	suffixAlignmentCombo->addItem(tr("Left"), ParagraphStyle::SuffixAlign_Left);
+	suffixAlignmentCombo->addItem(tr("Center"), ParagraphStyle::SuffixAlign_Center);
+	suffixAlignmentCombo->addItem(tr("Right"), ParagraphStyle::SuffixAlign_Right);
+	suffixAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	suffixAlignmentLabel->setVisible(isNumberedList);
+	suffixAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27810)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillSuffixAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27810)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="suffixAlignmentCombo">
+               <property name="toolTip">
+                <string>Alignment of suffix characters in numbered lists</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Suffix Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27810)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->suffixAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotSuffixAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotSuffixAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->suffixAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetSuffixAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::SuffixAlignment>(m_pwidget->suffixAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::SuffixAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipSuffixAlignmentForRTL(styleAlign);
+			m_selection[i]->setSuffixAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27810)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotSuffixAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignv8.patch (63,093 bytes)   

qirat

2026-09-04 13:28

reporter   ~0054413

I tested it, @Fahad. 'Centre' is problematic still (same as my comment 0017243:0054218).

I gave it a try with ai, and it seems to work fine. The patch is surprisingly small. Pls give it a test.

#Marker Alignment for Numbered/Bulleted Paragraph Effects (it preferred Marker Alignment like did)

- Adds a `ParagraphStyle` Marker Alignment property with `Legacy`, `Left`, `Center`, and `Right` modes.
- `Legacy` retains the existing Scribus paragraph-effect layout path, preserving layout of existing documents/styles unless Marker Alignment is explicitly changed.
- Computes a common marker field from the maximum shaped width of the actual generated markers in each contiguous list block and numbering level.
- Uses existing `MARKBullNumType` marker strings rather than deriving widths from counter maxima or synthesizing candidate marker strings.
- Shapes markers through a dedicated side-effect-free `TextContext`, avoiding mutation of normal mark ownership/page state during width measurement.
- Applies Left/Center/Right by repositioning the marker within the common field while reserving a constant marker advance, keeping the following text column aligned.
- Keeps `parEffectOffset()` as the marker-to-text gap rather than including it in marker-width calculation.
- Preserves existing `ParagraphEffectIndent` / Auto-Indent behavior; Marker Alignment remains stored but inactive while Auto-Indent controls marker placement.
- Adds Properties Palette and Style Manager controls, including normal paragraph-style inheritance handling.
- Adds current 1.7.1 `.sla` load/save support for `MarkerAlignment`.
- Invalidates the linked text-frame chain when numbering changes may alter the common marker-field width.
- Does not add numbering counter maxima, synthetic marker scans, persistent StoryText width caches, or changes to older file-format loaders.
marker-alignment-v1.03.patch (29,159 bytes)   
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27812)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -50,6 +50,14 @@
 		RTL  = 1
 	};
 
+	enum MarkerAlignmentType
+	{
+		MarkerAlignLegacy = 0,
+		MarkerAlignLeft    = 1,
+		MarkerAlignCenter  = 2,
+		MarkerAlignRight   = 3
+	};
+
 	enum OpticalMarginType
 	{
 		OM_None  = 0,
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27812)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -41,6 +41,7 @@
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
 ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
+ATTRDEF(ParagraphStyle::MarkerAlignmentType, markerAlignment, MarkerAlignment, ParagraphStyle::MarkerAlignLegacy)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27812)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -351,6 +351,12 @@
 }
 
 template<>
+ParagraphStyle::MarkerAlignmentType parse<ParagraphStyle::MarkerAlignmentType>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::MarkerAlignmentType>(str);
+}
+
+template<>
 ParagraphStyle::LineSpacingMode parse<ParagraphStyle::LineSpacingMode>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::LineSpacingMode>(str);
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27812)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,6 +32,7 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
+#include <utility>
 
 #include "actionmanager.h"
 #include "appmodes.h"
@@ -76,6 +77,129 @@
 
 using namespace std;
 
+namespace
+{
+class MarkerMeasureContext final : public TextContext
+{
+public:
+	explicit MarkerMeasureContext(PageItem* frame) : TextContext(frame) {}
+
+	double height() const override { return getFrame() ? getFrame()->height() : 0.0; }
+	double width() const override { return getFrame() ? getFrame()->width() : 0.0; }
+
+	QString expand(const ExpansionPoint& expansion) override
+	{
+		if (expansion.getType() != ExpansionPoint::MarkCE)
+			return QString();
+
+		Mark* mark = expansion.getMark();
+		if (mark && mark->isType(MARKBullNumType))
+			return mark->getString();
+		return QString();
+	}
+};
+
+struct MarkerBlockEntry
+{
+	int position { 0 };
+	int level { 0 };
+	bool alignmentActive { false };
+};
+
+QString markerListFamily(const ParagraphStyle& style)
+{
+	if (style.hasNum())
+		return QStringLiteral("N:") + style.numName();
+	if (style.hasBullet())
+		return QStringLiteral("B:") + style.bulletStr();
+	return QString();
+}
+
+double shapedMarkerWidth(StoryText& text, int position, ITextContext* context)
+{
+	TextShaper shaper(context, text, position, true);
+	ShapedText markerText = shaper.shape(position, position + 1);
+	double width = 0.0;
+	for (const GlyphCluster& cluster : markerText.glyphs())
+	{
+		if (cluster.firstChar() == position)
+			width += cluster.width();
+	}
+	return width;
+}
+
+QHash<int, double> markerFieldWidths(StoryText& text, PageItem* frame)
+{
+	QHash<int, double> fields;
+	MarkerMeasureContext context(frame);
+	QList<MarkerBlockEntry> block;
+	QString blockFamily;
+
+	auto flushBlock = [&]()
+	{
+		if (block.isEmpty())
+			return;
+
+		bool needsAlignment = false;
+		for (const MarkerBlockEntry& entry : std::as_const(block))
+		{
+			if (entry.alignmentActive)
+			{
+				needsAlignment = true;
+				break;
+			}
+		}
+
+		if (needsAlignment)
+		{
+			QHash<int, double> maxWidthByLevel;
+			for (const MarkerBlockEntry& entry : std::as_const(block))
+			{
+				double width = shapedMarkerWidth(text, entry.position, &context);
+				maxWidthByLevel[entry.level] = qMax(maxWidthByLevel.value(entry.level), width);
+			}
+			for (const MarkerBlockEntry& entry : std::as_const(block))
+				fields.insert(entry.position, maxWidthByLevel.value(entry.level));
+		}
+
+		block.clear();
+		blockFamily.clear();
+	};
+
+	for (int position = 0; position < text.length(); ++position)
+	{
+		if (position > 0 && text.text(position - 1) != SpecialChars::PARSEP)
+			continue;
+
+		const ParagraphStyle& style = text.paragraphStyle(position);
+		QString family = markerListFamily(style);
+		if (family.isEmpty())
+		{
+			flushBlock();
+		}
+		else
+		{
+			if (!block.isEmpty() && family != blockFamily)
+				flushBlock();
+			if (block.isEmpty())
+				blockFamily = family;
+
+			MarkerBlockEntry entry;
+			entry.position = position;
+			entry.level = style.hasNum() ? style.numLevel() : 0;
+			entry.alignmentActive = !style.parEffectIndent()
+				&& style.parEffectOffset() >= 0.0
+				&& style.markerAlignment() != ParagraphStyle::MarkerAlignLegacy;
+			block.append(entry);
+		}
+
+	}
+	flushBlock();
+
+	return fields;
+}
+}
+
 PageItem_TextFrame::PageItem_TextFrame(ScribusDoc *pa, double x, double y, double w, double h, double w2, const QString& fill, const QString& outline)
 	: PageItem(pa, PageItem::TextFrame, x, y, w, h, w2, fill, outline)
 {
@@ -1397,11 +1518,15 @@
 		//why emit invalidating signals each time text is changed by applying styles?
 		//this speed up layouting in case of using notes marks and drop caps
 		itemText.blockSignals(true);
+		const QHash<int, double> markerFields = markerFieldWidths(itemText, this);
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
 
 		double autoLeftIndent = 0.0;
+		int markerAlignmentChar = -1;
+		double markerAlignmentExtra = 0.0;
+		double markerAlignmentShift = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1448,6 +1573,38 @@
 				if (style.hasBullet() || style.hasNum())
 				{
 					BulNumMode = true;
+
+					bool firstMarkerCluster = (currentIndex == 0) || (current.glyphs[currentIndex - 1].firstChar() != a);
+					bool alignMarker = firstMarkerCluster
+						&& !style.parEffectIndent()
+						&& style.parEffectOffset() >= 0.0
+						&& style.markerAlignment() != ParagraphStyle::MarkerAlignLegacy;
+					if (alignMarker)
+					{
+						double markerWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+						{
+							const auto& markerGlyph = glyphClusters[j];
+							if (markerGlyph.firstChar() != a)
+								break;
+							markerWidth += markerGlyph.width();
+						}
+
+						double markerGap = style.parEffectOffset();
+						double markerField = qMax(markerWidth, markerFields.value(a, markerWidth));
+						double freeSpace = markerField - markerWidth;
+						double lead = 0.0;
+						if (style.markerAlignment() == ParagraphStyle::MarkerAlignCenter)
+							lead = freeSpace / 2.0;
+						else if (style.markerAlignment() == ParagraphStyle::MarkerAlignRight)
+							lead = freeSpace;
+
+						markerAlignmentChar = a;
+						markerAlignmentExtra = freeSpace + markerGap;
+						markerAlignmentShift = lead;
+						if (style.direction() == ParagraphStyle::RTL)
+							markerAlignmentShift += markerGap;
+					}
 				}
 			}
 
@@ -2449,7 +2606,11 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				double effectExtra = style.parEffectOffset();
+				bool markerAligned = BulNumMode && (markerAlignmentChar == a);
+				if (markerAligned)
+					effectExtra = markerAlignmentExtra;
+				current.xPos += effectExtra;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2462,14 +2623,19 @@
 						currentGlyph.extraWidth = 0.0;
 					}
 				}
-				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				// Set the trailing advance for Drop Cap, Bullet & Number List.
+				current.glyphs[currentIndex].extraWidth += effectExtra;
+				if (markerAligned)
+				{
+					// Marker Alignment uses the existing hanging-indent area as a fixed field.
+					// Only the marker moves inside that field; its total advance stays fixed.
+					current.glyphs[currentIndex].xoffset += markerAlignmentShift;
+				}
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
-				// the marker glyph to the right of its advance box so the reserved offset
-				// falls on the inner (left) side, next to the text. Drop caps and bullet /
-				// numbered-list markers share this offset mechanism.
-				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
+				// the glyph to the right of its advance box so the reserved offset falls on
+				// the inner (left) side, next to the text.
+				else if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
 					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
 
 				if (DropCmode)
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27812)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -17926,12 +17926,17 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
+		bool hasMarkerAlignment = false;
 
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
 			{
 				const ParagraphStyle& style = item->itemText.paragraphStyle(pos);
+				if ((style.hasBullet() || style.hasNum())
+					&& !style.parEffectIndent()
+					&& style.markerAlignment() != ParagraphStyle::MarkerAlignLegacy)
+					hasMarkerAlignment = true;
 				if (style.hasNum() && style.numName() != "<local block>")
 				{
 					if (!numerations.contains(style.numName()))
@@ -17995,7 +18000,12 @@
 		if (firstInvalidChar >= 0)
 		{
 			if (item->isTextFrame())
-				item->asTextFrame()->invalidateLayout(firstInvalidChar);
+			{
+				if (hasMarkerAlignment)
+					item->asTextFrame()->invalidateLayout(true);
+				else
+					item->asTextFrame()->invalidateLayout(firstInvalidChar);
+			}
 			else
 				item->invalidateLayout();
 		}
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27812)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3639,6 +3639,10 @@
 	if (attrs.hasAttribute(ParagraphEffectIndent))
 		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
 
+	static const QString MarkerAlignment("MarkerAlignment");
+	if (attrs.hasAttribute(MarkerAlignment))
+		newStyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignmentType>(attrs.valueAsInt(MarkerAlignment)));
+
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6685,6 +6689,8 @@
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
 	if (attrs.hasAttribute("ParagraphEffectIndent"))
 		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
+	if (attrs.hasAttribute("MarkerAlignment"))
+		pstyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignmentType>(attrs.valueAsInt("MarkerAlignment")));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27812)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -848,6 +848,8 @@
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
 	if (!style.isInhParEffectIndent())
 		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
+	if (!style.isInhMarkerAlignment())
+		docu.writeAttribute("MarkerAlignment", static_cast<int>(style.markerAlignment()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27812)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -461,6 +461,43 @@
        </layout>
       </item>
       <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_markerAlignment">
+        <property name="spacing">
+         <number>4</number>
+        </property>
+        <item>
+         <widget class="QLabel" name="markerAlignmentLabel">
+          <property name="text">
+           <string>Marker Alignment:</string>
+          </property>
+          <property name="buddy">
+           <cstring>markerAlignmentCombo</cstring>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="markerAlignmentCombo">
+          <property name="toolTip">
+           <string>Align the list marker within the paragraph hanging-indent field; Auto-Indent uses legacy marker placement</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_markerAlignment">
+          <property name="orientation">
+           <enum>Qt::Orientation::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>0</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
        <layout class="QHBoxLayout" name="horizontalLayout">
         <property name="spacing">
          <number>4</number>
@@ -558,6 +595,7 @@
   <tabstop>numSuffix</tabstop>
   <tabstop>peOffset</tabstop>
   <tabstop>peIndent</tabstop>
+  <tabstop>markerAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27812)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -67,6 +67,7 @@
 	void handleNumStart(int);
 	void handlePEOffset(double);
 	void handlePEIndent(bool);
+	void handleMarkerAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
@@ -79,6 +80,7 @@
 	void setType(int id);
 	void fillBulletStrEditCombo();
 	void fillPECombo();
+	void fillMarkerAlignmentCombo();
 
 signals:
 	void needsRelayout();
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27812)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -185,6 +185,11 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+
+	bool isList = (id == 1 || id == 2);
+	markerAlignmentLabel->setVisible(isList);
+	markerAlignmentCombo->setVisible(isList);
+	markerAlignmentCombo->setEnabled(isList && !peIndent->isChecked());
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -210,6 +215,19 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void PropertyWidget_ParEffect::fillMarkerAlignmentCombo()
+{
+	QSignalBlocker sb(markerAlignmentCombo);
+	int currentValue = markerAlignmentCombo->currentData().toInt();
+	markerAlignmentCombo->clear();
+	markerAlignmentCombo->addItem(tr("Legacy"), ParagraphStyle::MarkerAlignLegacy);
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlignLeft);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlignCenter);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlignRight);
+	int index = markerAlignmentCombo->findData(currentValue);
+	markerAlignmentCombo->setCurrentIndex(index >= 0 ? index : 0);
+}
+
 void PropertyWidget_ParEffect::updateStyle(const ParagraphStyle& newPStyle)
 {
 	int oldPeComboIndex = peCombo->currentIndex();
@@ -232,6 +250,7 @@
 	QSignalBlocker blockerA(peOffset);
 	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(markerAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -270,6 +289,9 @@
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
 	peIndent->setChecked(newPStyle.parEffectIndent());
+	int markerAlignmentIndex = markerAlignmentCombo->findData(static_cast<int>(newPStyle.markerAlignment()));
+	markerAlignmentCombo->setCurrentIndex(markerAlignmentIndex >= 0 ? markerAlignmentIndex : 0);
+	markerAlignmentCombo->setEnabled((newPStyle.hasBullet() || newPStyle.hasNum()) && !newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
 	if (oldPeComboIndex != peCombo->currentIndex())
@@ -289,6 +311,7 @@
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
 	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -305,6 +328,7 @@
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
 	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -405,6 +429,8 @@
 	}
 	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
 	newStyle.setParEffectIndent(peIndent->isChecked());
+	if (peCombo->currentIndex() == 2 || peCombo->currentIndex() == 3)
+		newStyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignmentType>(markerAlignmentCombo->currentData().toInt()));
 
 	setType(peCombo->currentData().toInt());
 
@@ -541,11 +567,21 @@
 {
 	if (!m_doc || !m_item)
 		return;
+	markerAlignmentCombo->setEnabled(!indent);
 	ParagraphStyle newStyle;
 	newStyle.setParEffectIndent(indent);
 	handleChanges(m_item, newStyle);
 }
 
+void PropertyWidget_ParEffect::handleMarkerAlignment(int)
+{
+	if (!m_doc || !m_item)
+		return;
+	ParagraphStyle newStyle;
+	newStyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignmentType>(markerAlignmentCombo->currentData().toInt()));
+	handleChanges(m_item, newStyle);
+}
+
 void PropertyWidget_ParEffect::handlePECharStyle(const QString& name)
 {
 	if (!m_doc || !m_item)
@@ -588,6 +624,7 @@
 {
 	fillPECombo();
 	retranslateUi(this);
+	fillMarkerAlignmentCombo();
 }
 
 void PropertyWidget_ParEffect::openEnhanced()
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27812)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -1601,6 +1601,43 @@
            </layout>
           </item>
           <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_markerAlignment">
+            <property name="spacing">
+             <number>4</number>
+            </property>
+            <item>
+             <widget class="QLabel" name="markerAlignmentLabel">
+              <property name="text">
+               <string>Marker Alignment:</string>
+              </property>
+              <property name="buddy">
+               <cstring>markerAlignmentCombo</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="SMScComboBox" name="markerAlignmentCombo">
+              <property name="toolTip">
+               <string>Align the list marker within the paragraph hanging-indent field; Auto-Indent uses legacy marker placement</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <spacer name="horizontalSpacer_markerAlignment">
+              <property name="orientation">
+               <enum>Qt::Orientation::Horizontal</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>0</width>
+                <height>20</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+           </layout>
+          </item>
+          <item>
            <layout class="QHBoxLayout" name="horizontalLayout_15">
             <property name="spacing">
              <number>16</number>
@@ -1793,6 +1830,7 @@
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
   <tabstop>parEffectIndentBox</tabstop>
+  <tabstop>markerAlignmentCombo</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27812)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillMarkerAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27812)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -74,6 +74,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillMarkerAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,6 +157,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
+	fillMarkerAlignmentCombo();
 
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
@@ -305,6 +307,8 @@
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
 		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
 		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+		markerAlignmentCombo->setCurrentItemByData(static_cast<int>(pstyle->markerAlignment()), pstyle->isInhMarkerAlignment());
+		markerAlignmentCombo->setParentItem(markerAlignmentCombo->getItemIndexForData(static_cast<int>(parent->markerAlignment())));
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -357,6 +361,7 @@
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
 		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
+		markerAlignmentCombo->setCurrentItemByData(static_cast<int>(pstyle->markerAlignment()));
 		parentParEffectsButton->hide();
 		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -1000,6 +1005,28 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillMarkerAlignmentCombo()
+{
+	QSignalBlocker sb(markerAlignmentCombo);
+	int currentValue = m_currPStyle ? static_cast<int>(m_currPStyle->markerAlignment()) : markerAlignmentCombo->currentData().toInt();
+	const ParagraphStyle* parent = m_currPStyle ? dynamic_cast<const ParagraphStyle*>(m_currPStyle->parentStyle()) : nullptr;
+	bool hasParent = m_currPStyle && m_currPStyle->hasParent() && parent && parent->hasName() && !m_currPStyle->parent().isEmpty();
+
+	markerAlignmentCombo->clear();
+	markerAlignmentCombo->addItem(tr("Legacy"), ParagraphStyle::MarkerAlignLegacy);
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlignLeft);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlignCenter);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlignRight);
+
+	if (hasParent)
+	{
+		markerAlignmentCombo->setCurrentItemByData(currentValue, m_currPStyle->isInhMarkerAlignment());
+		markerAlignmentCombo->setParentItem(markerAlignmentCombo->getItemIndexForData(static_cast<int>(parent->markerAlignment())));
+	}
+	else
+		markerAlignmentCombo->setCurrentItemByData(currentValue);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1057,11 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+
+	bool isList = (id == 2 || id == 3);
+	markerAlignmentLabel->setVisible(isList);
+	markerAlignmentCombo->setVisible(isList);
+	markerAlignmentCombo->setEnabled(isList && !parEffectIndentBox->isChecked());
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27812)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -78,6 +78,7 @@
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
 	void slotParEffectIndent(bool);
+	void slotMarkerAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27812)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -512,6 +512,7 @@
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
 	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -607,6 +608,7 @@
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
 	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	disconnect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -963,10 +965,28 @@
 		for (int i = 0; i < m_selection.count(); ++i)
 			m_selection[i]->setParEffectIndent(isOn);
 	}
+	m_pwidget->markerAlignmentCombo->setEnabled(!isOn);
 	
 	slotSelectionDirty();
 }
 
+void SMParagraphStyle::slotMarkerAlignment(int index)
+{
+	if (m_pwidget->markerAlignmentCombo->useParentValue())
+	{
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->resetMarkerAlignment();
+	}
+	else
+	{
+		auto alignment = static_cast<ParagraphStyle::MarkerAlignmentType>(m_pwidget->markerAlignmentCombo->itemData(index).toInt());
+		for (int i = 0; i < m_selection.count(); ++i)
+			m_selection[i]->setMarkerAlignment(alignment);
+	}
+
+	slotSelectionDirty();
+}
+
 void SMParagraphStyle::slotParEffectCharStyle(int index)
 {
 	QString name;
marker-alignment-v1.03.patch (29,159 bytes)   

qirat

2026-09-04 13:30

reporter   ~0054414

It preferred 'Marker Alignment' like I did in 0017243:0054228. So I went with it.

Fahad

2026-09-07 08:18

developer   ~0054433

hi @qirat i think i resolved your problem. Also, I update my patch to use marker alignment and benefit from your patch in some edge cases. In addition, fix more bugs while testing the new way.
so please test.
suffixalignv9.patch (67,864 bytes)   
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27824)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,7 +32,6 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
-
 #include "actionmanager.h"
 #include "appmodes.h"
 #include "canvas.h"
@@ -1084,7 +1083,149 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style, const void* localBlockId = nullptr)
+{
+	// Bullets have no counter, so the marker string alone identifies the list.
+	if (style.hasBullet())
+		return QLatin1String("B:") + style.bulletStr();
 
+	QString key = style.numName() + QLatin1Char('\x1f')
+			+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+			+ style.numPrefix() + QLatin1Char('\x1f')
+			+ style.numSuffix() + QLatin1Char('\x1f')
+			+ QString::number(style.numLevel());
+
+	// Identity by pointer, not position: a ParagraphStyle object's address
+	// only changes when its paragraph is created/destroyed (PARSEP
+	// insert/remove), never when unrelated text shifts positions
+	// elsewhere in the story. Using position here made every list's
+	// cache key drift on any edit before it, forcing needless rescans.
+	if (style.numName() == QLatin1String("<local block>") && localBlockId)
+		key += QLatin1Char('\x1f') + QString::number(reinterpret_cast<quintptr>(localBlockId));
+	return key;
+}
+
+// Shapes the real marker text already sitting at `position` in `text`,
+// using the frame's real ITextContext.
+static double shapedMarkerWidth(StoryText& text, int position, ITextContext* context)
+{
+	TextShaper shaper(context, text, position, true);
+	ShapedText markerText = shaper.shape(position, position + 1);
+	double width = 0.0;
+	for (const GlyphCluster& cluster : markerText.glyphs())
+	{
+		if (cluster.firstChar() == position)
+			width += cluster.width();
+	}
+	return width;
+}
+
+// Cheap character scan to find every paragraph start belonging to
+// targetListKey, then shapes ONLY those marker positions (not the
+// whole story). Used to fill a cache miss -- see maxParagraphEffectWidth().
+static double maxParagraphEffectWidthScan(PageItem_TextFrame* frame, const QString& targetListKey,
+											   const void* localBlockId, int hintPos)
+{
+	PageItem_TextFrame* shapingFrame = dynamic_cast<PageItem_TextFrame*>(frame->firstInChain());
+	if (!shapingFrame)
+		shapingFrame = frame;
+
+	StoryText& text = shapingFrame->itemText;
+	ITextContext* context = shapingFrame;
+	double maxWidth = 0.0;
+
+	if (localBlockId)
+	{
+		// localBlockId identifies the paragraph by its ParagraphStyle
+		// object's address (stable across edits elsewhere), not by
+		// position. We still need a starting POSITION to walk from, so
+		// find the paragraph currently holding this address once, then
+		// scan forward from there exactly as before.
+		int blockStartPos = -1;
+		// Fast path: the hint usually still points at the right paragraph
+		// (nothing before it moved, or the pointer itself didn't shift).
+		// Checked once, cheap -- avoids the full linear pointer search
+		// below for the common case.
+		if (hintPos >= 0 && hintPos < text.length() && &text.paragraphStyle(hintPos) == localBlockId)
+			blockStartPos = hintPos;
+		if (blockStartPos < 0)
+		{
+			// Hint stale (paragraph moved, or this is the very first
+			// lookup after a fresh block-start capture) -- fall back to
+			// the full O(n) search, same as before.
+			for (int pos = 0; pos < text.length(); ++pos)
+			{
+				if (pos > 0 && text.text(pos - 1) != SpecialChars::PARSEP)
+					continue;
+				if (&text.paragraphStyle(pos) == localBlockId)
+				{
+					blockStartPos = pos;
+					break;
+				}
+			}
+		}
+		if (blockStartPos < 0)
+			return maxWidth;
+
+		const ParagraphStyle& refStyle = text.paragraphStyle(blockStartPos);
+		int refFormat = refStyle.numFormat();
+		int refLevel  = refStyle.numLevel();
+		QString refPrefix  = refStyle.numPrefix();
+		QString refSuffix  = refStyle.numSuffix();
+
+		for (int pos = blockStartPos; pos < text.length(); ++pos)
+		{
+			if (pos > blockStartPos && text.text(pos - 1) != SpecialChars::PARSEP)
+				continue;
+
+			const ParagraphStyle& pStyle = text.paragraphStyle(pos);
+			bool sameList = pStyle.hasNum()
+					&& pStyle.numName() == QLatin1String("<local block>")
+					&& pStyle.numFormat() == refFormat
+					&& pStyle.numLevel() == refLevel
+					&& pStyle.numPrefix() == refPrefix
+					&& pStyle.numSuffix() == refSuffix;
+			if (!sameList)
+				break;
+
+			double width = shapedMarkerWidth(text, pos, context);
+			maxWidth = qMax(maxWidth, width);
+		}
+		return maxWidth;
+	}
+
+	// Named lists: numName() is already unique, so a full-story scan is
+	// still correct and simple -- unchanged from before.
+	for (int pos = 0; pos < text.length(); ++pos)
+	{
+		if (pos > 0 && text.text(pos - 1) != SpecialChars::PARSEP)
+			continue;
+
+		const ParagraphStyle& pStyle = text.paragraphStyle(pos);
+		if (!pStyle.hasNum() && !pStyle.hasBullet())
+			continue;
+		if (numListKey(pStyle) != targetListKey)
+			continue;
+
+		double width = shapedMarkerWidth(text, pos, context);
+		maxWidth = qMax(maxWidth, width);
+	}
+	return maxWidth;
+}
+
+// Cache wrapper: shared across every frame in the chain (via itemText),
+// cleared on real text/style edits. Computes only on a cache miss.
+static double maxParagraphEffectWidth(PageItem_TextFrame* frame, const QString& targetListKey,
+									   const void* localBlockId, int hintPos)
+{
+	QHash<QString, double>& cache = frame->itemText.maxParEffectWidthCache();
+	auto it = cache.constFind(targetListKey);
+	if (it != cache.constEnd())
+		return it.value();
+	double width = maxParagraphEffectWidthScan(frame, targetListKey, localBlockId, hintPos);
+	cache.insert(targetListKey, width);
+	return width;
+}
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1328,11 +1469,13 @@
 		}
 		
 		// update Bullet & number list if any.
-		if (itemText.hasTextMarks() || itemText.hasBulletOrNum() ||  itemText.marksCountChanged())
+		if (!itemText.bulletsNumSynced() &&
+				(itemText.hasTextMarks() || itemText.hasBulletOrNum() || itemText.marksCountChanged()))
 		{
 			updateBulletsNum();
 			itemText.resetMarksCountChanged();
 		}
+		itemText.setBulletsNumSynced(true);
 
 		ITextContext* context = this;
 		//TextShaper textShaper(this, itemText, firstInFrame());
@@ -1400,8 +1543,20 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		int markerPadComputedForA = -1;
+		double markerPadValue = 0.0;
+		// References into the shared story data, not local variables — a frame
+		// chain calls layout() once per frame, and local variables here would
+		// reset to -1 at the start of every frame, wrongly treating each frame's
+		// first paragraph as the start of a brand-new list.
+		const void*& localListBlockStart  = itemText.localListBlockStart();
+		int&         localListLastParaPos = itemText.localListLastParaPos();
+		int&         localListLastFormat  = itemText.localListLastFormat();
+		int&         localListLastLevel   = itemText.localListLastLevel();
+		QString&     localListLastPrefix  = itemText.localListLastPrefix();
+		QString&     localListLastSuffix  = itemText.localListLastSuffix();
+		int&         localListBlockStartHint = itemText.localListBlockStartHint();
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,20 +1589,42 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
+				style = itemText.paragraphStyle(a);
+				if (style.hasBullet() || style.hasNum())
 				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
+					BulNumMode = true;
 				}
-				else
+				if (style.hasNum() && style.numName() == QLatin1String("<local block>"))
 				{
-					autoLeftIndent = 0.0;
+					// A restarted line/row re-enters isBlockStart(a) for the SAME 'a'
+					// already processed. Without this guard, localListLastParaPos == a
+					// from the earlier pass, so the continuation check below compares
+					// nextParagraph(a) (the paragraph AFTER this one) against a itself
+					// -- always false -- wrongly starting a "new" list at every restart.
+					if (a != localListLastParaPos)
+					{
+						bool continuesRun = (localListLastParaPos >= 0)
+								&& (localListLastFormat == style.numFormat())
+								&& (localListLastLevel == style.numLevel())
+								&& (localListLastPrefix == style.numPrefix())
+								&& (localListLastSuffix == style.numSuffix())
+								&& (a == itemText.nextParagraph(localListLastParaPos) + 1);
+						if (!continuesRun)
+						{
+							localListBlockStart = &itemText.paragraphStyle(a);
+							localListBlockStartHint = a;   // record position AT THE TIME the pointer was captured
+						}
+						localListLastParaPos = a;
+						localListLastFormat  = style.numFormat();
+						localListLastLevel   = style.numLevel();
+						localListLastPrefix  = style.numPrefix();
+						localListLastSuffix  = style.numSuffix();
+					}
 				}
-				style = itemText.paragraphStyle(a);
-				if (style.hasBullet() || style.hasNum())
+				else
 				{
-					BulNumMode = true;
+					localListBlockStart  = nullptr;
+					localListLastParaPos = -1;
 				}
 			}
 
@@ -1816,43 +1993,64 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
 						}
 					}
-
-					if (BulNumMode || DropCmode)
+					// ---- Numbered & Bullet lists: align all lines with the widest marker ----
+					if (style.hasNum() || style.hasBullet())
 					{
-						if (style.parEffectIndent())
+						const QString listKey = numListKey(style, localListBlockStart);
+						double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+						// Lines WITHOUT the marker need the full width as indent
+						if (!BulNumMode)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
-
+							double fullFieldWidth = maxParEffectWidth + style.parEffectOffset();
 							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+								current.rightIndent += fullFieldWidth;
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								current.leftIndent  += fullFieldWidth;
 						}
+						// Lines WITH the marker: no extra indent here – markerPad (Left) or
+						// indentAdjust (Right/Center) will be applied later.
 					}
+
+					if (BulNumMode || DropCmode)
+					{
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+						{
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Right ||
+							style.markerAlignment() == ParagraphStyle::MarkerAlign_Center)
+						{
+							const QString listKey = numListKey(style, localListBlockStart);
+							double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+							if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Right)
+								indentAdjust = effectWidth - maxParEffectWidth;
+							else // Center
+								indentAdjust = (effectWidth - maxParEffectWidth) / 2.0;
+						}
+						if (style.direction() == ParagraphStyle::RTL)
+						{
+							current.rightIndent -= indentAdjust;
+							if (current.rightIndent < 0.0)
+								current.rightIndent = 0.0;
+						}
+						else
+						{
+							current.leftIndent -= indentAdjust;
+							if (current.leftIndent < 0.0)
+								current.leftIndent = 0.0;
+						}
+					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
 					if (rtlDropFollow)
@@ -2449,7 +2647,43 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				// markerPadValue/markerPadComputedForA are local to this loop
+				// rather than stored on LineControl, because a LineControl
+				// member would only be safe to reset in startLine() -- and a
+				// mid-line restart doesn't call startLine() again for the
+				// same marker, so a stored value would go stale on restart.
+				// Recomputing per glyph-run avoids that.
+				bool wantsMarkerPad = BulNumMode && style.hasNum()
+						&& (style.markerAlignment() == ParagraphStyle::MarkerAlign_Left
+							|| style.markerAlignment() == ParagraphStyle::MarkerAlign_Center);
+				if (wantsMarkerPad && markerPadComputedForA != a)
+				{
+					// The marker (e.g. "1.") can shape into more than one glyph
+					// cluster, all reporting the same firstChar()==a. Only compute
+					// the width on the FIRST glyph we see for this `a` — the sum
+					// must start from the run's beginning, not from whichever
+					// glyph happens to be current, or it undercounts on later
+					// glyphs of the same marker.
+					double effectWidth = 0.0;
+					for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+					{
+						const auto& glyph = glyphClusters[j];
+						if (glyph.firstChar() != a)
+							break;
+						effectWidth += glyph.width();
+					}
+					const QString listKey = numListKey(style, localListBlockStart);
+					double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+					if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Left)
+						markerPadValue = qMax(0.0, maxParEffectWidth - effectWidth);
+					else // Center: marker already moved by half the diff (indentAdjust
+						// above), so only the remaining half needs to go after it
+						// to bring the following text to the fixed column.
+						markerPadValue = qMax(0.0, (maxParEffectWidth - effectWidth) / 2.0);
+					markerPadComputedForA = a;
+				}
+				double markerPad = wantsMarkerPad ? markerPadValue : 0.0;
+				current.xPos += style.parEffectOffset() + markerPad;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2463,7 +2697,7 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + markerPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
@@ -2470,7 +2704,7 @@
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + markerPad;
 
 				if (DropCmode)
 				{
@@ -3087,7 +3321,6 @@
 
 NoRoom:
 	invalid = false;
-	
 	adjustParagraphEndings ();
 
 	if (verticalAlign > 0)
@@ -4946,6 +5179,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5039,10 +5273,18 @@
 			index--;
 			continue;
 		}
-
 	}
-
-	m_Doc->updateNumbers();
+	// The last, still-empty paragraph has no PARSEP yet, so it has no
+	// character index for the loop above to land on. Handle that one
+	// extra position explicitly, using the same rule.
+	if (itemText.length() == 0 || itemText.text(itemText.length() - 1) == SpecialChars::PARSEP)
+	{
+		const ParagraphStyle& trailing = itemText.paragraphStyle(itemText.length());
+		if (trailing.hasBullet() || trailing.hasNum())
+			itemText.insertMark(new BulNumMark(), itemText.length());
+	}
+		itemText.blockSignals(false);
+		m_Doc->updateNumbers();
 }
 
 
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString MarkerAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(MarkerAlignment))
+		newStyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignment>(attrs.valueAsInt(MarkerAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignment>(attrs.valueAsInt("MarkerAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhMarkerAlignment())
+		docu.writeAttribute("MarkerAlignment", style.markerAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27824)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9965,6 +9965,7 @@
 				if (start >= stop)
 					start = stop = currItem->itemText.normalizedCursorPosition();
 			}
+			currItem->itemText.blockSignals(true);
 			for (int pos = start; pos < stop; ++pos)
 			{
 				if (currItem->itemText.text(pos) == SpecialChars::PARSEP)
@@ -9989,6 +9990,11 @@
 				m_undoManager->action(currItem, is);
 			}
 			currItem->itemText.applyStyle(stop, newStyle, rmDirectFormatting);
+			currItem->itemText.blockSignals(false);
+			currItem->itemText.maxParEffectWidthCache().clear();
+			PageItem_TextFrame* currTextFrame = currItem->asTextFrame();
+			if (currTextFrame)
+				TextFrameSpellChecker::instance()->frameTextChanged(currTextFrame);
 			currItem->invalid = true;
 		}
 		if (currItem->isPathText())
@@ -17763,7 +17769,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17843,6 +17849,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17933,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17988,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27824)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::MarkerAlignment, markerAlignment, MarkerAlignment, ParagraphStyle::MarkerAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27824)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::MarkerAlignment ParagraphStyle::flipMarkerAlignmentForRTL(MarkerAlignment align) const
+{
+	if (align == ParagraphStyle::MarkerAlign_Left)
+		return ParagraphStyle::MarkerAlign_Right;
+	if (align == ParagraphStyle::MarkerAlign_Right)
+		return ParagraphStyle::MarkerAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::MarkerAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::MarkerAlignment parse<ParagraphStyle::MarkerAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::MarkerAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27824)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum MarkerAlignment
+	{
+		MarkerAlign_Left   = 0,
+		MarkerAlign_Center = 1,
+		MarkerAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::MarkerAlignment flipMarkerAlignmentForRTL(ParagraphStyle::MarkerAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27824)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,17 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	plainTextValid = false;
+	bulletsNumSynced = false;
+	localListLastParaPos = -1;
+	localListLastFormat  = -1;
+	localListLastLevel   = -1;
+	localListBlockStart  = nullptr;
+	localListBlockStartHint = -1;
+	localListLastPrefix.clear();
+	localListLastSuffix.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27824)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,31 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	/// true once updateBulletsNum() has synced marks for the current story
+	/// state; cleared on any edit so it reruns exactly once per real change,
+	/// not once per frame in the chain
+	bool bulletsNumSynced = false;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+	/// tracks contiguous "<local block>" numbered-list runs during layout();
+	/// must be shared across the chain, not local to layout(), because each
+	/// frame in a chain gets its own separate layout() call
+	int localListLastParaPos = -1;
+	int localListLastFormat  = -1;
+	int localListLastLevel   = -1;
+	QString localListLastPrefix;
+	QString localListLastSuffix;
+	const void* localListBlockStart = nullptr;
+	/// last known position of localListBlockStart's paragraph -- a hint only.
+	/// Checked first to skip the full O(n) pointer-search on a cache miss;
+	/// falls back to the full scan if the paragraph has actually moved.
+	int localListBlockStartHint = -1;
+
 	void clear();
 	
 	/**
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27824)
+++ scribus/text/storytext.cpp	(working copy)
@@ -561,6 +561,11 @@
 	// doesn't choke:
 	it->ch = QChar();
 	d->replaceCharStyleContextInParagraph(pos, paragraphStyle(pos+1).charStyleContext());
+	// Paragraph boundary changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 }
 
 /**
@@ -577,10 +582,22 @@
 		if (pos >= signed(d->len - 1))
 		{
 			applyStyle(pos, d->trailingStyle);
+			// #Enter should continue a list into the new empty item, not
+			// silently drop it — save the flags, erase(), then restore them.
+			bool continueBullet = d->trailingStyle.hasBullet();
+			bool continueNum = d->trailingStyle.hasNum();
 			d->trailingStyle.erase();
+			if (continueBullet)
+				d->trailingStyle.setHasBullet(true);
+			if (continueNum)
+				d->trailingStyle.setHasNum(true);
 		}
 	}
 	d->replaceCharStyleContextInParagraph(pos, it->parstyle->charStyleContext());
+	// Paragraph boundary changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 // Returns the matched story length (including skipped diacritics), or -1.
@@ -699,7 +716,20 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
-	invalidate(pos, length());
+	// #context-invalidation only needs to reach the end of the CURRENT
+	// paragraph: each paragraph's ParagraphStyle owns its own, independent
+	// m_cstyleContext (see paragraphstyle.h), so removing chars here cannot
+	// change how a downstream, untouched paragraph resolves its style parent.
+	// The emit below still uses the full range -- layout genuinely needs to
+	// know everything from pos onward shifted.
+	int contextEnd = qMin(nextParagraph(pos) + 1, length());
+	invalidateContextOnly(pos, contextEnd);
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
+
+	if (!signalsBlocked())
+		emit changed(pos, length());
+
 }
 
 void StoryText::trim()
@@ -773,6 +803,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +865,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +894,8 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1058,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1074,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,7 +1429,28 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+int& StoryText::localListLastParaPos() { return d->localListLastParaPos; }
+int& StoryText::localListLastFormat()  { return d->localListLastFormat; }
+int& StoryText::localListLastLevel()  {	return d->localListLastLevel; }
+const void*& StoryText::localListBlockStart()  { return d->localListBlockStart; }
+int& StoryText::localListBlockStartHint() { return d->localListBlockStartHint; }
+QString& StoryText::localListLastPrefix() { return d->localListLastPrefix; }
+QString& StoryText::localListLastSuffix() { return d->localListLastSuffix; }
 
+bool StoryText::bulletsNumSynced() const
+{
+	return d->bulletsNumSynced;
+}
+
+void StoryText::setBulletsNumSynced(bool synced)
+{
+	d->bulletsNumSynced = synced;
+}
+
 const CharStyle & StoryText::charStyle() const
 {
 	return charStyle(d->cursorPosition);
@@ -1599,6 +1658,11 @@
 			--i;
 		}
 	}
+	// Paragraph style changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -1630,6 +1694,12 @@
 		//		qDebug() << QString("applying parstyle %1 as defaultstyle for %2").arg(paragraphStyle(pos).name()).arg(pos);
 		d->trailingStyle.eraseStyle(style);
 	}
+	// Paragraph style changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -2249,6 +2319,11 @@
 
 void StoryText::invalidateAll()
 {
+	// Content or styles changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	d->pstyleContext.invalidate();
 	invalidate(0, length());
 }
@@ -2265,6 +2340,16 @@
 		emit changed(firstItem, endItem);
 }
 
+void StoryText::invalidateContextOnly(int firstItem, int endItem)
+{
+	for (int i = firstItem; i < endItem; ++i)
+	{
+		ParagraphStyle* par = item(i)->parstyle;
+		if (par)
+			par->charStyleContext()->invalidate();
+	}
+}
+
 StoryTextSnapshot StoryText::createSnapshot() const
 {
 	// Delegate to the static factory method
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27824)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,24 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
+	/// true once bullet/number marks have been synced for the current
+	/// story state; cleared on any edit
+	bool bulletsNumSynced() const;
+	void setBulletsNumSynced(bool synced);
+	/// shared tracking state for contiguous local-block list runs during
+	/// layout() -- see ScText_Shared for why this can't be a local variable
+	int& localListLastParaPos();
+	int& localListLastFormat();
+	int& localListLastLevel();
+	QString& localListLastPrefix();
+	QString& localListLastSuffix();
+	const void*& localListBlockStart();
+	int& localListBlockStartHint();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
@@ -335,6 +354,8 @@
 	
 	/// mark these runs as invalid, ie. need itemize and shaping
 	void invalidate(int firstRun, int lastRun);
+	/// like invalidate(), but only resets paragraph-style contexts -- no signal
+	void invalidateContextOnly(int firstItem, int endItem);
 	void removeParSep(int pos);
 	void insertParSep(int pos);
 
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27824)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -220,7 +220,8 @@
 				else if (charStyle.name() != style.peCharStyleName())
 					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
 				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				if (!charStyle.equiv(m_story.charStyle(i)))
+					m_story.setCharStyle(i, 1, charStyle);
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
@@ -229,10 +230,12 @@
 				if (charStyle.parent() == style.peCharStyleName())
 				{
 					const QString& curParent(style.hasParent() ? style.parent() : style.name());
+					CharStyle newStyle(charStyle);
 					if (doc->charStyles().contains(style.peCharStyleName()))
-						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-					m_story.setCharStyle(i, 1, charStyle);
+						newStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
+					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					if (!newStyle.equiv(m_story.charStyle(i)))
+						m_story.setCharStyle(i, 1, newStyle);
 				}
 			}
 		}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27824)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -6,7 +6,6 @@
 */
 
 #include <QSignalBlocker>
-
 #include "propertywidget_pareffect.h"
 
 #include "appmodehelper.h"
@@ -45,6 +44,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlign_Left);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlign_Center);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +188,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	markerAlignmentLabel->setVisible(isNumberedList);
+	markerAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +236,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(markerAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +275,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::MarkerAlignment displayAlign = newPStyle.markerAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipMarkerAlignmentForRTL(displayAlign);
+
+	int idx = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		markerAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +301,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +317,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +416,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +548,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleMarkerAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::MarkerAlignment>(markerAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipMarkerAlignmentForRTL(align);
+	newStyle.setMarkerAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
@@ -578,7 +592,6 @@
 		tempSelection.addItem(item);
 		m_doc->itemSelection_ApplyParagraphStyle(newStyle, &tempSelection);
 		m_doc->updateNumbers();
-
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 		connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
 	}
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27824)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleMarkerAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27824)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>295</width>
-    <height>274</height>
+    <width>315</width>
+    <height>308</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
@@ -29,7 +29,7 @@
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_6">
      <item>
-      <widget class="FormWidget" name="peLabel">
+      <widget class="FormWidget" name="peLabel" native="true">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
          <horstretch>0</horstretch>
@@ -399,7 +399,7 @@
          <number>20</number>
         </property>
         <item>
-         <widget class="FormWidget" name="peOffsetLabel">
+         <widget class="FormWidget" name="peOffsetLabel" native="true">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
             <horstretch>0</horstretch>
@@ -439,13 +439,20 @@
          </widget>
         </item>
         <item>
-         <widget class="QCheckBox" name="peIndent">
+         <widget class="QLabel" name="markerAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Marker Align:</string>
           </property>
          </widget>
         </item>
         <item>
+         <widget class="QComboBox" name="markerAlignmentCombo">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alignment of marker characters in numbered lists&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+         </widget>
+        </item>
+        <item>
          <spacer name="horizontalSpacer_5">
           <property name="orientation">
            <enum>Qt::Orientation::Horizontal</enum>
@@ -466,7 +473,7 @@
          <number>4</number>
         </property>
         <item>
-         <widget class="FormWidget" name="peCharStyleLabel">
+         <widget class="FormWidget" name="peCharStyleLabel" native="true">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
             <horstretch>0</horstretch>
@@ -557,7 +564,7 @@
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
   <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+  <tabstop>markerAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27824)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillMarkerAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillMarkerAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::MarkerAlignment displayAlign = pstyle->markerAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipMarkerAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::MarkerAlignment parentDisplayAlign = parent->markerAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipMarkerAlignmentForRTL(parentDisplayAlign);
+
+		int markerAlignIndex = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+		markerAlignmentCombo->setCurrentItem(markerAlignIndex, pstyle->isInhMarkerAlignment());
+		markerAlignmentCombo->setParentItem(markerAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::MarkerAlignment displayAlign = pstyle->markerAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipMarkerAlignmentForRTL(displayAlign);
+
+		int markerAlignIndex = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+		markerAlignmentCombo->setCurrentItem((markerAlignIndex >= 0) ? markerAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillMarkerAlignmentCombo()
+{
+	QSignalBlocker sb(markerAlignmentCombo);
+	int currIndex = markerAlignmentCombo->currentIndex();
+	markerAlignmentCombo->clear();
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlign_Left);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlign_Center);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlign_Right);
+	markerAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	markerAlignmentLabel->setVisible(isNumberedList);
+	markerAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27824)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillMarkerAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27824)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>716</width>
-    <height>507</height>
+    <height>530</height>
    </rect>
   </property>
   <layout class="QVBoxLayout">
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="markerAlignmentCombo">
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alignment of marker characters in numbered lists&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="suffixAlignmentLabel">
+               <property name="text">
+                <string>Marker Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27824)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotMarkerAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->markerAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetMarkerAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::MarkerAlignment>(m_pwidget->markerAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::MarkerAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipMarkerAlignmentForRTL(styleAlign);
+			m_selection[i]->setMarkerAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27824)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotMarkerAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
suffixalignv9.patch (67,864 bytes)   

Fahad

2026-09-08 13:56

developer   ~0054440

Update to fix compiling error
markerAlignmentv9.1.patch (67,864 bytes)   
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27824)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -32,7 +32,6 @@
 #include <QRegion>
 #include <cairo.h>
 #include <cassert>
-
 #include "actionmanager.h"
 #include "appmodes.h"
 #include "canvas.h"
@@ -1084,7 +1083,149 @@
 	return style.lineSpacing();
 }
 
+static QString numListKey(const ParagraphStyle& style, const void* localBlockId = nullptr)
+{
+	// Bullets have no counter, so the marker string alone identifies the list.
+	if (style.hasBullet())
+		return QLatin1String("B:") + style.bulletStr();
 
+	QString key = style.numName() + QLatin1Char('\x1f')
+			+ QString::number(style.numFormat()) + QLatin1Char('\x1f')
+			+ style.numPrefix() + QLatin1Char('\x1f')
+			+ style.numSuffix() + QLatin1Char('\x1f')
+			+ QString::number(style.numLevel());
+
+	// Identity by pointer, not position: a ParagraphStyle object's address
+	// only changes when its paragraph is created/destroyed (PARSEP
+	// insert/remove), never when unrelated text shifts positions
+	// elsewhere in the story. Using position here made every list's
+	// cache key drift on any edit before it, forcing needless rescans.
+	if (style.numName() == QLatin1String("<local block>") && localBlockId)
+		key += QLatin1Char('\x1f') + QString::number(reinterpret_cast<quintptr>(localBlockId));
+	return key;
+}
+
+// Shapes the real marker text already sitting at `position` in `text`,
+// using the frame's real ITextContext.
+static double shapedMarkerWidth(StoryText& text, int position, ITextContext* context)
+{
+	TextShaper shaper(context, text, position, true);
+	ShapedText markerText = shaper.shape(position, position + 1);
+	double width = 0.0;
+	for (const GlyphCluster& cluster : markerText.glyphs())
+	{
+		if (cluster.firstChar() == position)
+			width += cluster.width();
+	}
+	return width;
+}
+
+// Cheap character scan to find every paragraph start belonging to
+// targetListKey, then shapes ONLY those marker positions (not the
+// whole story). Used to fill a cache miss -- see maxParagraphEffectWidth().
+static double maxParagraphEffectWidthScan(PageItem_TextFrame* frame, const QString& targetListKey,
+											   const void* localBlockId, int hintPos)
+{
+	PageItem_TextFrame* shapingFrame = dynamic_cast<PageItem_TextFrame*>(frame->firstInChain());
+	if (!shapingFrame)
+		shapingFrame = frame;
+
+	StoryText& text = shapingFrame->itemText;
+	ITextContext* context = shapingFrame;
+	double maxWidth = 0.0;
+
+	if (localBlockId)
+	{
+		// localBlockId identifies the paragraph by its ParagraphStyle
+		// object's address (stable across edits elsewhere), not by
+		// position. We still need a starting POSITION to walk from, so
+		// find the paragraph currently holding this address once, then
+		// scan forward from there exactly as before.
+		int blockStartPos = -1;
+		// Fast path: the hint usually still points at the right paragraph
+		// (nothing before it moved, or the pointer itself didn't shift).
+		// Checked once, cheap -- avoids the full linear pointer search
+		// below for the common case.
+		if (hintPos >= 0 && hintPos < text.length() && &text.paragraphStyle(hintPos) == localBlockId)
+			blockStartPos = hintPos;
+		if (blockStartPos < 0)
+		{
+			// Hint stale (paragraph moved, or this is the very first
+			// lookup after a fresh block-start capture) -- fall back to
+			// the full O(n) search, same as before.
+			for (int pos = 0; pos < text.length(); ++pos)
+			{
+				if (pos > 0 && text.text(pos - 1) != SpecialChars::PARSEP)
+					continue;
+				if (&text.paragraphStyle(pos) == localBlockId)
+				{
+					blockStartPos = pos;
+					break;
+				}
+			}
+		}
+		if (blockStartPos < 0)
+			return maxWidth;
+
+		const ParagraphStyle& refStyle = text.paragraphStyle(blockStartPos);
+		int refFormat = refStyle.numFormat();
+		int refLevel  = refStyle.numLevel();
+		QString refPrefix  = refStyle.numPrefix();
+		QString refSuffix  = refStyle.numSuffix();
+
+		for (int pos = blockStartPos; pos < text.length(); ++pos)
+		{
+			if (pos > blockStartPos && text.text(pos - 1) != SpecialChars::PARSEP)
+				continue;
+
+			const ParagraphStyle& pStyle = text.paragraphStyle(pos);
+			bool sameList = pStyle.hasNum()
+					&& pStyle.numName() == QLatin1String("<local block>")
+					&& pStyle.numFormat() == refFormat
+					&& pStyle.numLevel() == refLevel
+					&& pStyle.numPrefix() == refPrefix
+					&& pStyle.numSuffix() == refSuffix;
+			if (!sameList)
+				break;
+
+			double width = shapedMarkerWidth(text, pos, context);
+			maxWidth = qMax(maxWidth, width);
+		}
+		return maxWidth;
+	}
+
+	// Named lists: numName() is already unique, so a full-story scan is
+	// still correct and simple -- unchanged from before.
+	for (int pos = 0; pos < text.length(); ++pos)
+	{
+		if (pos > 0 && text.text(pos - 1) != SpecialChars::PARSEP)
+			continue;
+
+		const ParagraphStyle& pStyle = text.paragraphStyle(pos);
+		if (!pStyle.hasNum() && !pStyle.hasBullet())
+			continue;
+		if (numListKey(pStyle) != targetListKey)
+			continue;
+
+		double width = shapedMarkerWidth(text, pos, context);
+		maxWidth = qMax(maxWidth, width);
+	}
+	return maxWidth;
+}
+
+// Cache wrapper: shared across every frame in the chain (via itemText),
+// cleared on real text/style edits. Computes only on a cache miss.
+static double maxParagraphEffectWidth(PageItem_TextFrame* frame, const QString& targetListKey,
+									   const void* localBlockId, int hintPos)
+{
+	QHash<QString, double>& cache = frame->itemText.maxParEffectWidthCache();
+	auto it = cache.constFind(targetListKey);
+	if (it != cache.constEnd())
+		return it.value();
+	double width = maxParagraphEffectWidthScan(frame, targetListKey, localBlockId, hintPos);
+	cache.insert(targetListKey, width);
+	return width;
+}
 // This assumes that layout() ran on the previous page and set the incomplete* vars
 // It also clears the incomplete* vars, and changes the starting position for this frame
 // The incomplete* vars are used to ensure that we don't run into an endless loop
@@ -1328,11 +1469,13 @@
 		}
 		
 		// update Bullet & number list if any.
-		if (itemText.hasTextMarks() || itemText.hasBulletOrNum() ||  itemText.marksCountChanged())
+		if (!itemText.bulletsNumSynced() &&
+				(itemText.hasTextMarks() || itemText.hasBulletOrNum() || itemText.marksCountChanged()))
 		{
 			updateBulletsNum();
 			itemText.resetMarksCountChanged();
 		}
+		itemText.setBulletsNumSynced(true);
 
 		ITextContext* context = this;
 		//TextShaper textShaper(this, itemText, firstInFrame());
@@ -1400,8 +1543,20 @@
 		setMaxY(-1);
 		double maxYAsc = 0.0, maxYDesc = 0.0;
 		int regionMinY = 0, regionMaxY= 0;
+		int markerPadComputedForA = -1;
+		double markerPadValue = 0.0;
+		// References into the shared story data, not local variables — a frame
+		// chain calls layout() once per frame, and local variables here would
+		// reset to -1 at the start of every frame, wrongly treating each frame's
+		// first paragraph as the start of a brand-new list.
+		const void*& localListBlockStart  = itemText.localListBlockStart();
+		int&         localListLastParaPos = itemText.localListLastParaPos();
+		int&         localListLastFormat  = itemText.localListLastFormat();
+		int&         localListLastLevel   = itemText.localListLastLevel();
+		QString&     localListLastPrefix  = itemText.localListLastPrefix();
+		QString&     localListLastSuffix  = itemText.localListLastSuffix();
+		int&         localListBlockStartHint = itemText.localListBlockStartHint();
 
-		double autoLeftIndent = 0.0;
 		for (int i = 0; shapedText.haveMoreText(i, glyphClusters); ++i)
 		{
 			int currentIndex = i - current.lineData.firstCluster;
@@ -1434,20 +1589,42 @@
 			BulNumMode = false;
 			if (itemText.isBlockStart(a))
 			{
-				if (currentIndex > 0)
+				style = itemText.paragraphStyle(a);
+				if (style.hasBullet() || style.hasNum())
 				{
-					int prevA = current.glyphs[currentIndex - 1].firstChar();
-					if (a != prevA)
-						autoLeftIndent = 0.0;
+					BulNumMode = true;
 				}
-				else
+				if (style.hasNum() && style.numName() == QLatin1String("<local block>"))
 				{
-					autoLeftIndent = 0.0;
+					// A restarted line/row re-enters isBlockStart(a) for the SAME 'a'
+					// already processed. Without this guard, localListLastParaPos == a
+					// from the earlier pass, so the continuation check below compares
+					// nextParagraph(a) (the paragraph AFTER this one) against a itself
+					// -- always false -- wrongly starting a "new" list at every restart.
+					if (a != localListLastParaPos)
+					{
+						bool continuesRun = (localListLastParaPos >= 0)
+								&& (localListLastFormat == style.numFormat())
+								&& (localListLastLevel == style.numLevel())
+								&& (localListLastPrefix == style.numPrefix())
+								&& (localListLastSuffix == style.numSuffix())
+								&& (a == itemText.nextParagraph(localListLastParaPos) + 1);
+						if (!continuesRun)
+						{
+							localListBlockStart = &itemText.paragraphStyle(a);
+							localListBlockStartHint = a;   // record position AT THE TIME the pointer was captured
+						}
+						localListLastParaPos = a;
+						localListLastFormat  = style.numFormat();
+						localListLastLevel   = style.numLevel();
+						localListLastPrefix  = style.numPrefix();
+						localListLastSuffix  = style.numSuffix();
+					}
 				}
-				style = itemText.paragraphStyle(a);
-				if (style.hasBullet() || style.hasNum())
+				else
 				{
-					BulNumMode = true;
+					localListBlockStart  = nullptr;
+					localListLastParaPos = -1;
 				}
 			}
 
@@ -1816,43 +1993,64 @@
 					else
 					{
 						// LTR: Original behavior
-						current.leftIndent = style.leftMargin() + autoLeftIndent;
+						current.leftIndent = style.leftMargin();
 						if (itemText.isBlockStart(a))
 						{
 							current.leftIndent += style.firstIndent();
 						}
 					}
-
-					if (BulNumMode || DropCmode)
+					// ---- Numbered & Bullet lists: align all lines with the widest marker ----
+					if (style.hasNum() || style.hasBullet())
 					{
-						if (style.parEffectIndent())
+						const QString listKey = numListKey(style, localListBlockStart);
+						double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+						// Lines WITHOUT the marker need the full width as indent
+						if (!BulNumMode)
 						{
-							double effectWidth = 0.0;
-							for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
-							{
-								const auto & glyph = glyphClusters[j];
-								if (glyph.firstChar() != a)
-									break;
-								effectWidth += glyph.width();
-							}
-
+							double fullFieldWidth = maxParEffectWidth + style.parEffectOffset();
 							if (style.direction() == ParagraphStyle::RTL)
-							{
-								current.rightIndent -= style.parEffectOffset() + effectWidth;
-								if (current.rightIndent < 0.0)
-									current.rightIndent = 0.0;
-							}
+								current.rightIndent += fullFieldWidth;
 							else
-							{
-								current.leftIndent -= style.parEffectOffset() + effectWidth;
-								if (current.leftIndent < 0.0)
-								{
-									autoLeftIndent = abs(current.leftIndent);
-									current.leftIndent = 0.0;
-								}
-							}
+								current.leftIndent  += fullFieldWidth;
 						}
+						// Lines WITH the marker: no extra indent here – markerPad (Left) or
+						// indentAdjust (Right/Center) will be applied later.
 					}
+
+					if (BulNumMode || DropCmode)
+					{
+						double effectWidth = 0.0;
+						for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+						{
+							const auto& glyph = glyphClusters[j];
+							if (glyph.firstChar() != a)
+								break;
+							effectWidth += glyph.width();
+						}
+						double indentAdjust = 0.0;
+						if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Right ||
+							style.markerAlignment() == ParagraphStyle::MarkerAlign_Center)
+						{
+							const QString listKey = numListKey(style, localListBlockStart);
+							double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+							if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Right)
+								indentAdjust = effectWidth - maxParEffectWidth;
+							else // Center
+								indentAdjust = (effectWidth - maxParEffectWidth) / 2.0;
+						}
+						if (style.direction() == ParagraphStyle::RTL)
+						{
+							current.rightIndent -= indentAdjust;
+							if (current.rightIndent < 0.0)
+								current.rightIndent = 0.0;
+						}
+						else
+						{
+							current.leftIndent -= indentAdjust;
+							if (current.leftIndent < 0.0)
+								current.leftIndent = 0.0;
+						}
+					}
 					// RTL drop-cap follow-lines: Constrain the available line width
 					// from the right margin to prevent text overlapping the right-aligned drop cap.
 					if (rtlDropFollow)
@@ -2449,7 +2647,43 @@
 			}
 			if ((DropCmode || BulNumMode) && !outs)
 			{
-				current.xPos += style.parEffectOffset();
+				// markerPadValue/markerPadComputedForA are local to this loop
+				// rather than stored on LineControl, because a LineControl
+				// member would only be safe to reset in startLine() -- and a
+				// mid-line restart doesn't call startLine() again for the
+				// same marker, so a stored value would go stale on restart.
+				// Recomputing per glyph-run avoids that.
+				bool wantsMarkerPad = BulNumMode && style.hasNum()
+						&& (style.markerAlignment() == ParagraphStyle::MarkerAlign_Left
+							|| style.markerAlignment() == ParagraphStyle::MarkerAlign_Center);
+				if (wantsMarkerPad && markerPadComputedForA != a)
+				{
+					// The marker (e.g. "1.") can shape into more than one glyph
+					// cluster, all reporting the same firstChar()==a. Only compute
+					// the width on the FIRST glyph we see for this `a` — the sum
+					// must start from the run's beginning, not from whichever
+					// glyph happens to be current, or it undercounts on later
+					// glyphs of the same marker.
+					double effectWidth = 0.0;
+					for (int j = i; shapedText.haveMoreText(j, glyphClusters); ++j)
+					{
+						const auto& glyph = glyphClusters[j];
+						if (glyph.firstChar() != a)
+							break;
+						effectWidth += glyph.width();
+					}
+					const QString listKey = numListKey(style, localListBlockStart);
+					double maxParEffectWidth = maxParagraphEffectWidth(this, listKey, localListBlockStart, localListBlockStartHint);
+					if (style.markerAlignment() == ParagraphStyle::MarkerAlign_Left)
+						markerPadValue = qMax(0.0, maxParEffectWidth - effectWidth);
+					else // Center: marker already moved by half the diff (indentAdjust
+						// above), so only the remaining half needs to go after it
+						// to bring the following text to the fixed column.
+						markerPadValue = qMax(0.0, (maxParEffectWidth - effectWidth) / 2.0);
+					markerPadComputedForA = a;
+				}
+				double markerPad = wantsMarkerPad ? markerPadValue : 0.0;
+				current.xPos += style.parEffectOffset() + markerPad;
 				// for bulleted lists, make sure offset is applied only after last bullet char
 				// for numbered lists, make sure that offset is applied only after the suffix
 				// loop over previous current.glyphs and set their extraWidth to 0.0
@@ -2463,7 +2697,7 @@
 					}
 				}
 				// set the offset for Drop Cap, Bullet & Number List
-				current.glyphs[currentIndex].extraWidth += style.parEffectOffset();
+				current.glyphs[currentIndex].extraWidth += style.parEffectOffset() + markerPad;
 				// RTL: the line is reversed at render time, so trailing extraWidth lands on
 				// the marker's OUTER (right) edge, not between the marker and the text. Shift
 				// the marker glyph to the right of its advance box so the reserved offset
@@ -2470,7 +2704,7 @@
 				// falls on the inner (left) side, next to the text. Drop caps and bullet /
 				// numbered-list markers share this offset mechanism.
 				if (style.direction() == ParagraphStyle::RTL && (DropCmode || BulNumMode))
-					current.glyphs[currentIndex].xoffset += style.parEffectOffset();
+					current.glyphs[currentIndex].xoffset += style.parEffectOffset() + markerPad;
 
 				if (DropCmode)
 				{
@@ -3087,7 +3321,6 @@
 
 NoRoom:
 	invalid = false;
-	
 	adjustParagraphEndings ();
 
 	if (verticalAlign > 0)
@@ -4946,6 +5179,7 @@
 
 void PageItem_TextFrame::updateBulletsNum()
 {
+	itemText.blockSignals(true);
 	for (int index = 0; index < itemText.length(); ++index)
 	{
 		Mark* mark = itemText.mark(index);
@@ -5039,10 +5273,18 @@
 			index--;
 			continue;
 		}
-
 	}
-
-	m_Doc->updateNumbers();
+	// The last, still-empty paragraph has no PARSEP yet, so it has no
+	// character index for the loop above to land on. Handle that one
+	// extra position explicitly, using the same rule.
+	if (itemText.length() == 0 || itemText.text(itemText.length() - 1) == SpecialChars::PARSEP)
+	{
+		const ParagraphStyle& trailing = itemText.paragraphStyle(itemText.length());
+		if (trailing.hasBullet() || trailing.hasNum())
+			itemText.insertMark(new BulNumMark(), itemText.length());
+	}
+		itemText.blockSignals(false);
+		m_Doc->updateNumbers();
 }
 
 
Index: scribus/plugins/fileloader/scribus150format/scribus150format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus150format/scribus150format.cpp	(working copy)
@@ -3123,10 +3123,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5497,8 +5493,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus150format/scribus150format_save.cpp	(working copy)
@@ -842,8 +842,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if ( ! style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if ( ! style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if ( ! style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if ( ! style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus170format/scribus170format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus170format/scribus170format.cpp	(working copy)
@@ -3131,10 +3131,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	static const QString DROP("DROP");
 	if (attrs.hasAttribute(DROP))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt(DROP)));
@@ -5567,8 +5563,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
 	if (attrs.hasAttribute("DROPLIN"))
Index: scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus170format/scribus170format_save.cpp	(working copy)
@@ -844,8 +844,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("DROP", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -3635,10 +3635,6 @@
 	if (attrs.hasAttribute(ParagraphEffectOffset))
 		newStyle.setParEffectOffset(attrs.valueAsDouble(ParagraphEffectOffset));
 
-	static const QString ParagraphEffectIndent("ParagraphEffectIndent");
-	if (attrs.hasAttribute(ParagraphEffectIndent))
-		newStyle.setParEffectIndent(attrs.valueAsDouble(ParagraphEffectIndent));
-
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("DROP"))
 		newStyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -3695,6 +3691,10 @@
 	if (attrs.hasAttribute(NumerationSuffix))
 		newStyle.setNumSuffix(attrs.valueAsString(NumerationSuffix));
 
+	static const QString MarkerAlignment("SuffixAlignment");
+	if (attrs.hasAttribute(MarkerAlignment))
+		newStyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignment>(attrs.valueAsInt(MarkerAlignment)));
+
 	static const QString NumerationRestart("NumerationRestart");
 	if (attrs.hasAttribute(NumerationRestart))
 		newStyle.setNumRestart(attrs.valueAsInt(NumerationRestart));
@@ -6683,8 +6683,6 @@
 		pstyle.setPeCharStyleName(attrs.valueAsString("ParagraphEffectCharStyle"));
 	if (attrs.hasAttribute("ParagraphEffectOffset"))
 		pstyle.setParEffectOffset(attrs.valueAsDouble("ParagraphEffectOffset"));
-	if (attrs.hasAttribute("ParagraphEffectIndent"))
-		pstyle.setParEffectIndent(attrs.valueAsDouble("ParagraphEffectIndent"));
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("DROP"))
 		pstyle.setHasDropCap(static_cast<bool>(attrs.valueAsInt("DROP")));
@@ -6721,6 +6719,8 @@
 		pstyle.setNumPrefix(attrs.valueAsString("NumerationPrefix"));
 	if (attrs.hasAttribute("NumerationSuffix"))
 		pstyle.setNumSuffix(attrs.valueAsString("NumerationSuffix"));
+	if (attrs.hasAttribute("SuffixAlignment"))
+		pstyle.setMarkerAlignment(static_cast<ParagraphStyle::MarkerAlignment>(attrs.valueAsInt("MarkerAlignment")));
 	if (attrs.hasAttribute("NumerationRestart"))
 	{
 		NumerationRange numRange = (NumerationRange) attrs.valueAsInt("NumerationRestart");
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27824)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -846,8 +846,6 @@
 		docu.writeAttribute("ParagraphEffectCharStyle", style.peCharStyleName());
 	if (!style.isInhParEffectOffset())
 		docu.writeAttribute("ParagraphEffectOffset", style.parEffectOffset());
-	if (!style.isInhParEffectIndent())
-		docu.writeAttribute("ParagraphEffectIndent", static_cast<int>(style.parEffectIndent()));
 	if (!style.isInhHasDropCap())
 		docu.writeAttribute("HasDropCap", static_cast<int>(style.hasDropCap()));
 	if (!style.isInhDropCapLines())
@@ -868,6 +866,8 @@
 		docu.writeAttribute("NumerationPrefix", style.numPrefix());
 	if (!style.isInhNumSuffix())
 		docu.writeAttribute("NumerationSuffix", style.numSuffix());
+	if (!style.isInhMarkerAlignment())
+		docu.writeAttribute("MarkerAlignment", style.markerAlignment());
 	if (!style.isInhNumStart())
 		docu.writeAttribute("NumerationStart", style.numStart());
 	if (!style.isInhNumRestart())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27824)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -9965,6 +9965,7 @@
 				if (start >= stop)
 					start = stop = currItem->itemText.normalizedCursorPosition();
 			}
+			currItem->itemText.blockSignals(true);
 			for (int pos = start; pos < stop; ++pos)
 			{
 				if (currItem->itemText.text(pos) == SpecialChars::PARSEP)
@@ -9989,6 +9990,11 @@
 				m_undoManager->action(currItem, is);
 			}
 			currItem->itemText.applyStyle(stop, newStyle, rmDirectFormatting);
+			currItem->itemText.blockSignals(false);
+			currItem->itemText.maxParEffectWidthCache().clear();
+			PageItem_TextFrame* currTextFrame = currItem->asTextFrame();
+			if (currTextFrame)
+				TextFrameSpellChecker::instance()->frameTextChanged(currTextFrame);
 			currItem->invalid = true;
 		}
 		if (currItem->isPathText())
@@ -17763,7 +17769,7 @@
 	int firstInvalidChar = -1;
 	QVector<Numeration> nums;
 	QList<int> counters;
-
+	itemText.blockSignals(true);
 	for (int pos = 0; pos < itemText.length(); ++pos)
 	{
 		if (pos != 0 && itemText.text(pos - 1) != SpecialChars::PARSEP)
@@ -17843,6 +17849,7 @@
 				firstInvalidChar = pos;
 		}
 	}
+	itemText.blockSignals(false);
 	return firstInvalidChar;
 }
 
@@ -17926,7 +17933,6 @@
 		if (item->itemText.length() <= 0)
 			continue;
 		int firstInvalidChar1 = -1;
-
 		while (pos < item->itemText.length())
 		{
 			if ((pos == 0) || (item->itemText.text(pos - 1) == SpecialChars::PARSEP))
@@ -17982,7 +17988,6 @@
 					pos = item->itemText.nextParagraph(pos) + 1;
 			}
 		}
-
 		// Update local numbering
 		int firstInvalidChar2 = updateLocalNums(item->itemText);
 
Index: scribus/styles/paragraphstyle.attrdefs.cxx
===================================================================
--- scribus/styles/paragraphstyle.attrdefs.cxx	(revision 27824)
+++ scribus/styles/paragraphstyle.attrdefs.cxx	(working copy)
@@ -40,7 +40,6 @@
 ATTRDEF(bool, hasDropCap, HasDropCap, false)
 ATTRDEF(int, dropCapLines, DropCapLines, 2)
 ATTRDEF(double, parEffectOffset, ParEffectOffset, 0.0)
-ATTRDEF(bool, parEffectIndent, ParEffectIndent, false)
 ATTRDEF(QString, peCharStyleName, PeCharStyleName,"")
 ATTRDEF(bool, hasBullet, HasBullet, false)
 ATTRDEF(QString, bulletStr, BulletStr, QString(QChar(0x2022)))
@@ -54,6 +53,7 @@
 ATTRDEF(int, numRestart, NumRestart, 0)
 ATTRDEF(bool, numOther, NumOther, false)
 ATTRDEF(bool, numHigher, NumHigher, true)
+ATTRDEF(ParagraphStyle::MarkerAlignment, markerAlignment, MarkerAlignment, ParagraphStyle::MarkerAlign_Left)
 ATTRDEF(QString, backgroundColor, BackgroundColor, "None")
 ATTRDEF(double, backgroundShade, BackgroundShade, 100)
 ATTRDEF(int, hyphenConsecutiveLines, HyphenConsecutiveLines, 2)
Index: scribus/styles/paragraphstyle.cpp
===================================================================
--- scribus/styles/paragraphstyle.cpp	(revision 27824)
+++ scribus/styles/paragraphstyle.cpp	(working copy)
@@ -208,6 +208,14 @@
 #undef ATTRDEF
 }
 
+ParagraphStyle::MarkerAlignment ParagraphStyle::flipMarkerAlignmentForRTL(MarkerAlignment align) const
+{
+	if (align == ParagraphStyle::MarkerAlign_Left)
+		return ParagraphStyle::MarkerAlign_Right;
+	if (align == ParagraphStyle::MarkerAlign_Right)
+		return ParagraphStyle::MarkerAlign_Left;
+	return align;
+}
 
 void ParagraphStyle::getNamedResources(ResourceCollection& lists) const
 {
@@ -261,6 +269,11 @@
 	return QString::number(static_cast<int>(val));
 }
 
+static QString toXMLString(ParagraphStyle::MarkerAlignment val)
+{
+	return QString::number(static_cast<int>(val));
+}
+
 static QString toXMLString(ParagraphStyle::DirectionType val)
 {
 	return QString::number(static_cast<int>(val));
@@ -345,6 +358,12 @@
 }
 
 template<>
+ParagraphStyle::MarkerAlignment parse<ParagraphStyle::MarkerAlignment>(const Xml_string& str)
+{
+	return parseEnum<ParagraphStyle::MarkerAlignment>(str);
+}
+
+template<>
 ParagraphStyle::DirectionType parse<ParagraphStyle::DirectionType>(const Xml_string& str)
 {
 	return parseEnum<ParagraphStyle::DirectionType>(str);
Index: scribus/styles/paragraphstyle.h
===================================================================
--- scribus/styles/paragraphstyle.h	(revision 27824)
+++ scribus/styles/paragraphstyle.h	(working copy)
@@ -76,6 +76,13 @@
 		CenterTab = 4
 	};
 
+	enum MarkerAlignment
+	{
+		MarkerAlign_Left   = 0,
+		MarkerAlign_Center = 1,
+		MarkerAlign_Right  = 2
+	};
+
 	struct TabRecord
 	{
 		qreal tabPosition {0.0};
@@ -113,6 +120,7 @@
 	void eraseStyle(const ParagraphStyle& other);
 	void setStyle(const ParagraphStyle& other);
 	void erase() override { eraseStyle(*this); }
+	ParagraphStyle::MarkerAlignment flipMarkerAlignmentForRTL(ParagraphStyle::MarkerAlignment align) const;
 
 	StyleContext* charStyleContext() { return & m_cstyleContext; }
 	const StyleContext* charStyleContext() const { return & m_cstyleContext; }
Index: scribus/text/sctext_shared.cpp
===================================================================
--- scribus/text/sctext_shared.cpp	(revision 27824)
+++ scribus/text/sctext_shared.cpp	(working copy)
@@ -68,6 +68,17 @@
 	if (marksCount > 0)
 		marksCountChanged = true;
 	marksCount = 0;
+	maxParEffectWidthCache.clear();
+	cachedPlainText.clear();
+	plainTextValid = false;
+	bulletsNumSynced = false;
+	localListLastParaPos = -1;
+	localListLastFormat  = -1;
+	localListLastLevel   = -1;
+	localListBlockStart  = nullptr;
+	localListBlockStartHint = -1;
+	localListLastPrefix.clear();
+	localListLastSuffix.clear();
 }
 
 ScText_Shared& ScText_Shared::operator= (const ScText_Shared& other) 
Index: scribus/text/sctext_shared.h
===================================================================
--- scribus/text/sctext_shared.h	(revision 27824)
+++ scribus/text/sctext_shared.h	(working copy)
@@ -2,6 +2,7 @@
 #define SCTEXT_SHARED_H
 
 #include <QList>
+#include <QHash>
 #include <QObject>
 #include <QString>
 #include <cassert>
@@ -35,6 +36,31 @@
 	ParagraphStyle trailingStyle;
 	CharStyle orphanedCharStyle;
 
+	/// max list-marker width per list key, shared by every frame in the chain;
+	/// cleared whenever text or styles change
+	QHash<QString, double> maxParEffectWidthCache;
+
+	/// true once updateBulletsNum() has synced marks for the current story
+	/// state; cleared on any edit so it reruns exactly once per real change,
+	/// not once per frame in the chain
+	bool bulletsNumSynced = false;
+
+	mutable QString cachedPlainText;
+	mutable bool plainTextValid = false;
+	/// tracks contiguous "<local block>" numbered-list runs during layout();
+	/// must be shared across the chain, not local to layout(), because each
+	/// frame in a chain gets its own separate layout() call
+	int localListLastParaPos = -1;
+	int localListLastFormat  = -1;
+	int localListLastLevel   = -1;
+	QString localListLastPrefix;
+	QString localListLastSuffix;
+	const void* localListBlockStart = nullptr;
+	/// last known position of localListBlockStart's paragraph -- a hint only.
+	/// Checked first to skip the full O(n) pointer-search on a cache miss;
+	/// falls back to the full scan if the paragraph has actually moved.
+	int localListBlockStartHint = -1;
+
 	void clear();
 	
 	/**
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 27824)
+++ scribus/text/storytext.cpp	(working copy)
@@ -561,6 +561,11 @@
 	// doesn't choke:
 	it->ch = QChar();
 	d->replaceCharStyleContextInParagraph(pos, paragraphStyle(pos+1).charStyleContext());
+	// Paragraph boundary changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 }
 
 /**
@@ -577,10 +582,22 @@
 		if (pos >= signed(d->len - 1))
 		{
 			applyStyle(pos, d->trailingStyle);
+			// #Enter should continue a list into the new empty item, not
+			// silently drop it — save the flags, erase(), then restore them.
+			bool continueBullet = d->trailingStyle.hasBullet();
+			bool continueNum = d->trailingStyle.hasNum();
 			d->trailingStyle.erase();
+			if (continueBullet)
+				d->trailingStyle.setHasBullet(true);
+			if (continueNum)
+				d->trailingStyle.setHasNum(true);
 		}
 	}
 	d->replaceCharStyleContextInParagraph(pos, it->parstyle->charStyleContext());
+	// Paragraph boundary changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
 }
 
 // Returns the matched story length (including skipped diacritics), or -1.
@@ -699,7 +716,20 @@
 		d->selFirst =  0;
 		d->selLast  = -1;
 	}
-	invalidate(pos, length());
+	// #context-invalidation only needs to reach the end of the CURRENT
+	// paragraph: each paragraph's ParagraphStyle owns its own, independent
+	// m_cstyleContext (see paragraphstyle.h), so removing chars here cannot
+	// change how a downstream, untouched paragraph resolves its style parent.
+	// The emit below still uses the full range -- layout genuinely needs to
+	// know everything from pos onward shifted.
+	int contextEnd = qMin(nextParagraph(pos) + 1, length());
+	invalidateContextOnly(pos, contextEnd);
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
+
+	if (!signalsBlocked())
+		emit changed(pos, length());
+
 }
 
 void StoryText::trim()
@@ -773,6 +803,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += txt.length();
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + txt.length());
 }
 
@@ -833,6 +865,8 @@
 	d->len = d->count();
 	if ((d->selLast >= d->selFirst) && (d->selFirst <= pos) && (pos <= d->selLast))
 		d->selLast += inserted;
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + inserted);
 }
 
@@ -860,7 +894,8 @@
 
 	if (oldMarksCount != d->marksCount)
 		d->marksCountChanged = true;
-	
+	d->plainTextValid = false;
+	d->bulletsNumSynced = false;
 	invalidate(pos, pos + 1);
 }
 
@@ -1023,6 +1058,8 @@
 {
 	if (length() <= 0)
 		return QString();
+	if (d->plainTextValid)
+		return d->cachedPlainText;
 
 	QChar   ch;
 	QString result;
@@ -1037,7 +1074,8 @@
 			ch = QLatin1Char('\n');
 		result += ch;
 	}
-
+	d->cachedPlainText = result;
+	d->plainTextValid  = true;
 	return result;
 }
 #if 0
@@ -1391,7 +1429,28 @@
 	d->at(pos)->setEffects(~(flags & ScStyle_NonUserStyles) & d->at(pos)->effects().value);
 }
 
+QHash<QString, double> &StoryText::maxParEffectWidthCache()
+{
+	return d->maxParEffectWidthCache;
+}
+int& StoryText::localListLastParaPos() { return d->localListLastParaPos; }
+int& StoryText::localListLastFormat()  { return d->localListLastFormat; }
+int& StoryText::localListLastLevel()  {	return d->localListLastLevel; }
+const void*& StoryText::localListBlockStart()  { return d->localListBlockStart; }
+int& StoryText::localListBlockStartHint() { return d->localListBlockStartHint; }
+QString& StoryText::localListLastPrefix() { return d->localListLastPrefix; }
+QString& StoryText::localListLastSuffix() { return d->localListLastSuffix; }
 
+bool StoryText::bulletsNumSynced() const
+{
+	return d->bulletsNumSynced;
+}
+
+void StoryText::setBulletsNumSynced(bool synced)
+{
+	d->bulletsNumSynced = synced;
+}
+
 const CharStyle & StoryText::charStyle() const
 {
 	return charStyle(d->cursorPosition);
@@ -1599,6 +1658,11 @@
 			--i;
 		}
 	}
+	// Paragraph style changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -1630,6 +1694,12 @@
 		//		qDebug() << QString("applying parstyle %1 as defaultstyle for %2").arg(paragraphStyle(pos).name()).arg(pos);
 		d->trailingStyle.eraseStyle(style);
 	}
+	// Paragraph style changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
+
 	invalidate(pos, qMin(i, length()));
 }
 
@@ -2249,6 +2319,11 @@
 
 void StoryText::invalidateAll()
 {
+	// Content or styles changed -- marker widths may no longer be valid.
+	if (!signalsBlocked())
+		d->maxParEffectWidthCache.clear();
+	d->bulletsNumSynced = false;
+
 	d->pstyleContext.invalidate();
 	invalidate(0, length());
 }
@@ -2265,6 +2340,16 @@
 		emit changed(firstItem, endItem);
 }
 
+void StoryText::invalidateContextOnly(int firstItem, int endItem)
+{
+	for (int i = firstItem; i < endItem; ++i)
+	{
+		ParagraphStyle* par = item(i)->parstyle;
+		if (par)
+			par->charStyleContext()->invalidate();
+	}
+}
+
 StoryTextSnapshot StoryText::createSnapshot() const
 {
 	// Delegate to the static factory method
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 27824)
+++ scribus/text/storytext.h	(working copy)
@@ -29,6 +29,7 @@
 #include <QObject>
 #include <QString>
 #include <QList>
+#include <QHash>
 #include <unicode/uversion.h>
 
 #include "itextsource.h"
@@ -290,6 +291,24 @@
 	void setFlag(int pos, LayoutFlags flag) override;
 	void clearFlag(int pos, LayoutFlags flag) override;
 
+	/// cache of max list-marker width per list key; shared across every
+	/// frame referencing this story, invalidated on any edit
+	QHash<QString, double>& maxParEffectWidthCache();
+
+	/// true once bullet/number marks have been synced for the current
+	/// story state; cleared on any edit
+	bool bulletsNumSynced() const;
+	void setBulletsNumSynced(bool synced);
+	/// shared tracking state for contiguous local-block list runs during
+	/// layout() -- see ScText_Shared for why this can't be a local variable
+	int& localListLastParaPos();
+	int& localListLastFormat();
+	int& localListLastLevel();
+	QString& localListLastPrefix();
+	QString& localListLastSuffix();
+	const void*& localListBlockStart();
+	int& localListBlockStartHint();
+
 //  when physical view doesn't match logical view any more:
 
 	/// call this if the shape of an embedded object changes (redos layout)
@@ -335,6 +354,8 @@
 	
 	/// mark these runs as invalid, ie. need itemize and shaping
 	void invalidate(int firstRun, int lastRun);
+	/// like invalidate(), but only resets paragraph-style contexts -- no signal
+	void invalidateContextOnly(int firstItem, int endItem);
 	void removeParSep(int pos);
 	void insertParSep(int pos);
 
Index: scribus/text/textshaper.cpp
===================================================================
--- scribus/text/textshaper.cpp	(revision 27824)
+++ scribus/text/textshaper.cpp	(working copy)
@@ -220,7 +220,8 @@
 				else if (charStyle.name() != style.peCharStyleName())
 					newStyle.setParent(doc->charStyle(style.peCharStyleName()).name());
 				charStyle.setStyle(newStyle);
-				m_story.setCharStyle(i, 1, charStyle);
+				if (!charStyle.equiv(m_story.charStyle(i)))
+					m_story.setCharStyle(i, 1, charStyle);
 			}
 			else if (!style.peCharStyleName().isEmpty())
 			{
@@ -229,10 +230,12 @@
 				if (charStyle.parent() == style.peCharStyleName())
 				{
 					const QString& curParent(style.hasParent() ? style.parent() : style.name());
+					CharStyle newStyle(charStyle);
 					if (doc->charStyles().contains(style.peCharStyleName()))
-						charStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
-					charStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
-					m_story.setCharStyle(i, 1, charStyle);
+						newStyle.eraseCharStyle(doc->charStyle(style.peCharStyleName()));
+					newStyle.setParent(doc->paragraphStyle(curParent).charStyle().name());
+					if (!newStyle.equiv(m_story.charStyle(i)))
+						m_story.setCharStyle(i, 1, newStyle);
 				}
 			}
 		}
Index: scribus/ui/propertywidget_pareffect.cpp
===================================================================
--- scribus/ui/propertywidget_pareffect.cpp	(revision 27824)
+++ scribus/ui/propertywidget_pareffect.cpp	(working copy)
@@ -6,7 +6,6 @@
 */
 
 #include <QSignalBlocker>
-
 #include "propertywidget_pareffect.h"
 
 #include "appmodehelper.h"
@@ -45,6 +44,10 @@
 	peCombo->setCurrentIndex(0);
 	setType(peCombo->currentData().toInt());
 
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlign_Left);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlign_Center);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlign_Right);
+
 	iconSetChange();
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
@@ -185,6 +188,9 @@
 		stackedWidget->setVisible(true);
 		peGroup->setVisible(true);
 	}
+	bool isNumberedList = (id == 2);
+	markerAlignmentLabel->setVisible(isNumberedList);
+	markerAlignmentCombo->setVisible(isNumberedList);
 }
 
 void PropertyWidget_ParEffect::fillBulletStrEditCombo()
@@ -230,8 +236,8 @@
 	QSignalBlocker blocker8(numSuffix);
 	QSignalBlocker blocker9(numStart);
 	QSignalBlocker blockerA(peOffset);
-	QSignalBlocker blockerB(peIndent);
 	QSignalBlocker blockerC(peCharStyleCombo);
+	QSignalBlocker blockerD(markerAlignmentCombo);
 
 	if (newPStyle.hasDropCap())
 	{
@@ -269,9 +275,16 @@
 
 	numFormatCombo->setCurrentFormat((NumFormat) newPStyle.numFormat());
 	peOffset->setValue(newPStyle.parEffectOffset() * m_unitRatio);
-	peIndent->setChecked(newPStyle.parEffectIndent());
 	showCharStyle(newPStyle.peCharStyleName());
 
+	ParagraphStyle::MarkerAlignment displayAlign = newPStyle.markerAlignment();
+	if (newPStyle.direction() == ParagraphStyle::RTL)
+		displayAlign = newPStyle.flipMarkerAlignmentForRTL(displayAlign);
+
+	int idx = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+	if (idx >= 0)
+		markerAlignmentCombo->setCurrentIndex(idx);
+
 	if (oldPeComboIndex != peCombo->currentIndex())
 		emit needsRelayout();
 }
@@ -288,7 +301,7 @@
 	connect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)), Qt::UniqueConnection);
 	connect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)), Qt::UniqueConnection);
 	connect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)), Qt::UniqueConnection);
-	connect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)), Qt::UniqueConnection);
+	connect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)), Qt::UniqueConnection);
 	connect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)), Qt::UniqueConnection);
 }
 
@@ -304,7 +317,7 @@
 	disconnect(numSuffix, SIGNAL(textChanged(QString)), this, SLOT(handleNumSuffix(QString)));
 	disconnect(numStart, SIGNAL(valueChanged(int)), this, SLOT(handleNumStart(int)));
 	disconnect(peOffset, SIGNAL(valueChanged(double)), this, SLOT(handlePEOffset(double)));
-	disconnect(peIndent, SIGNAL(toggled(bool)), this, SLOT(handlePEIndent(bool)));
+	disconnect(markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(handleMarkerAlignment(int)));
 	disconnect(peCharStyleCombo, SIGNAL(newStyle(QString)), this, SLOT(handlePECharStyle(QString)));
 }
 
@@ -403,8 +416,6 @@
 		newStyle.setHasBullet(false);
 		newStyle.setHasNum(false);
 	}
-	newStyle.setParEffectOffset(peOffset->value() / m_unitRatio);
-	newStyle.setParEffectIndent(peIndent->isChecked());
 
 	setType(peCombo->currentData().toInt());
 
@@ -537,12 +548,15 @@
 	handleChanges(m_item, newStyle);
 }
 
-void PropertyWidget_ParEffect::handlePEIndent(bool indent)
+void PropertyWidget_ParEffect::handleMarkerAlignment(int index)
 {
 	if (!m_doc || !m_item)
 		return;
 	ParagraphStyle newStyle;
-	newStyle.setParEffectIndent(indent);
+	auto align = static_cast<ParagraphStyle::MarkerAlignment>(markerAlignmentCombo->itemData(index).toInt());
+	if (m_item->currentStyle().direction() == ParagraphStyle::RTL)
+		align = newStyle.flipMarkerAlignmentForRTL(align);
+	newStyle.setMarkerAlignment(align);
 	handleChanges(m_item, newStyle);
 }
 
@@ -578,7 +592,6 @@
 		tempSelection.addItem(item);
 		m_doc->itemSelection_ApplyParagraphStyle(newStyle, &tempSelection);
 		m_doc->updateNumbers();
-
 		connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 		connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
 	}
Index: scribus/ui/propertywidget_pareffect.h
===================================================================
--- scribus/ui/propertywidget_pareffect.h	(revision 27824)
+++ scribus/ui/propertywidget_pareffect.h	(working copy)
@@ -66,7 +66,7 @@
 	void handleNumSuffix(const QString&);
 	void handleNumStart(int);
 	void handlePEOffset(double);
-	void handlePEIndent(bool);
+	void handleMarkerAlignment(int);
 	void handlePECharStyle(const QString&);
 
 private slots:
Index: scribus/ui/propertywidget_pareffectbase.ui
===================================================================
--- scribus/ui/propertywidget_pareffectbase.ui	(revision 27824)
+++ scribus/ui/propertywidget_pareffectbase.ui	(working copy)
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>295</width>
-    <height>274</height>
+    <width>315</width>
+    <height>308</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
@@ -29,7 +29,7 @@
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_6">
      <item>
-      <widget class="FormWidget" name="peLabel">
+      <widget class="FormWidget" name="peLabel" native="true">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
          <horstretch>0</horstretch>
@@ -399,7 +399,7 @@
          <number>20</number>
         </property>
         <item>
-         <widget class="FormWidget" name="peOffsetLabel">
+         <widget class="FormWidget" name="peOffsetLabel" native="true">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
             <horstretch>0</horstretch>
@@ -439,13 +439,20 @@
          </widget>
         </item>
         <item>
-         <widget class="QCheckBox" name="peIndent">
+         <widget class="QLabel" name="markerAlignmentLabel">
           <property name="text">
-           <string>Auto-Indent</string>
+           <string>Marker Align:</string>
           </property>
          </widget>
         </item>
         <item>
+         <widget class="QComboBox" name="markerAlignmentCombo">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alignment of marker characters in numbered lists&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+         </widget>
+        </item>
+        <item>
          <spacer name="horizontalSpacer_5">
           <property name="orientation">
            <enum>Qt::Orientation::Horizontal</enum>
@@ -466,7 +473,7 @@
          <number>4</number>
         </property>
         <item>
-         <widget class="FormWidget" name="peCharStyleLabel">
+         <widget class="FormWidget" name="peCharStyleLabel" native="true">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
             <horstretch>0</horstretch>
@@ -557,7 +564,7 @@
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
   <tabstop>peOffset</tabstop>
-  <tabstop>peIndent</tabstop>
+  <tabstop>markerAlignmentCombo</tabstop>
   <tabstop>peCharStyleCombo</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>bulletStrEdit</tabstop>
Index: scribus/ui/smpstylewidget.cpp
===================================================================
--- scribus/ui/smpstylewidget.cpp	(revision 27824)
+++ scribus/ui/smpstylewidget.cpp	(working copy)
@@ -25,7 +25,6 @@
 	return al == bl;
 }
 
-
 SMPStyleWidget::SMPStyleWidget(ScribusDoc* doc, StyleSet<CharStyle> *cstyles) :
 	m_Doc(doc),
 	m_cstyles(cstyles)
@@ -74,6 +73,7 @@
 	dropCapLines->setMaximum(99);
 
 	fillPECombo();
+	fillMarkerAlignmentCombo();
 	connect(peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(handleParEffectUse(int)));
 	connect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
@@ -156,7 +156,7 @@
 	numRestartCombo->blockSignals(numRestartComboBlocked);
 
 	fillPECombo();
-
+	fillMarkerAlignmentCombo();
 	backgroundColor->colorButton->setPersistentToolTip( tr("Background color of selected text"));
 	backgroundColor->setText(tr("Background"));
 }
@@ -303,8 +303,19 @@
 
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio, pstyle->isInhParEffectOffset());
 		parEffectOffset->setParentValue(parent->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent(),pstyle->isInhParEffectIndent());
-		parEffectIndentBox->setParentValue(parent->parEffectIndent());
+
+		ParagraphStyle::MarkerAlignment displayAlign = pstyle->markerAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipMarkerAlignmentForRTL(displayAlign);
+
+		ParagraphStyle::MarkerAlignment parentDisplayAlign = parent->markerAlignment();
+		if (parent->direction() == ParagraphStyle::RTL)
+			parentDisplayAlign = pstyle->flipMarkerAlignmentForRTL(parentDisplayAlign);
+
+		int markerAlignIndex = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+		markerAlignmentCombo->setCurrentItem(markerAlignIndex, pstyle->isInhMarkerAlignment());
+		markerAlignmentCombo->setParentItem(markerAlignmentCombo->findData(static_cast<int>(parentDisplayAlign)));
+
 		dropCapLines->setValue(pstyle->dropCapLines(), pstyle->isInhDropCapLines());
 		dropCapLines->setParentValue(parent->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
@@ -356,10 +367,13 @@
 		maxGlyphExtSpin->setValue(pstyle->maxGlyphExtension() * 100.0);
 		maxConsecutiveCountSpinBox->setValue(pstyle->hyphenConsecutiveLines());
 		parEffectOffset->setValue(pstyle->parEffectOffset() * unitRatio);
-		parEffectIndentBox->setChecked(pstyle->parEffectIndent());
-		parentParEffectsButton->hide();
-		disconnect(parentParEffectsButton, SIGNAL(clicked()), this, SLOT(slotParentParEffects()));
 
+		ParagraphStyle::MarkerAlignment displayAlign = pstyle->markerAlignment();
+		if (pstyle->direction() == ParagraphStyle::RTL)
+			displayAlign = pstyle->flipMarkerAlignmentForRTL(displayAlign);
+
+		int markerAlignIndex = markerAlignmentCombo->findData(static_cast<int>(displayAlign));
+		markerAlignmentCombo->setCurrentItem((markerAlignIndex >= 0) ? markerAlignIndex : 0);
 		dropCapLines->setValue(pstyle->dropCapLines());
 		bulletStrEdit->setEditText(pstyle->bulletStr());
 		setWidgetBoldFont(bulletCharLabel, false);
@@ -1000,6 +1014,17 @@
 	peCombo->setCurrentIndex(currIndex);
 }
 
+void SMPStyleWidget::fillMarkerAlignmentCombo()
+{
+	QSignalBlocker sb(markerAlignmentCombo);
+	int currIndex = markerAlignmentCombo->currentIndex();
+	markerAlignmentCombo->clear();
+	markerAlignmentCombo->addItem(tr("Left"), ParagraphStyle::MarkerAlign_Left);
+	markerAlignmentCombo->addItem(tr("Center"), ParagraphStyle::MarkerAlign_Center);
+	markerAlignmentCombo->addItem(tr("Right"), ParagraphStyle::MarkerAlign_Right);
+	markerAlignmentCombo->setCurrentIndex(currIndex);
+}
+
 void SMPStyleWidget::setParagraphEffect(int index)
 {
 	QSignalBlocker sigPECombo(peCombo);
@@ -1030,6 +1055,9 @@
 		peGroup->setVisible(false);
 		peCombo->setCurrentIndex(0);
 	}
+	bool isNumberedList = (id == 3);
+	markerAlignmentLabel->setVisible(isNumberedList);
+	markerAlignmentCombo->setVisible(isNumberedList);
 }
 
 void SMPStyleWidget::showDropCap(const QList<ParagraphStyle*> &pstyles, const QList<CharStyle> &cstyles, int unitIndex)
Index: scribus/ui/smpstylewidget.h
===================================================================
--- scribus/ui/smpstylewidget.h	(revision 27824)
+++ scribus/ui/smpstylewidget.h	(working copy)
@@ -52,6 +52,7 @@
 	void fillNumerationsCombo();
 	void fillNumRestartCombo();
 	void fillPECombo();
+	void fillMarkerAlignmentCombo();
 	void setParagraphEffect(int);
 	void showColors(const QList<ParagraphStyle*> &cstyles);
 	void showLineSpacing(const QList<ParagraphStyle*> &pstyles);
Index: scribus/ui/smpstylewidget.ui
===================================================================
--- scribus/ui/smpstylewidget.ui	(revision 27824)
+++ scribus/ui/smpstylewidget.ui	(working copy)
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>716</width>
-    <height>507</height>
+    <height>530</height>
    </rect>
   </property>
   <layout class="QVBoxLayout">
@@ -29,7 +29,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>0</number>
+      <number>1</number>
      </property>
      <widget class="QWidget" name="tab">
       <attribute name="title">
@@ -181,7 +181,7 @@
             <item>
              <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
               <item>
-               <widget class="FormWidget" name="consecutiveHyphenLabel">
+               <widget class="FormWidget" name="consecutiveHyphenLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -280,7 +280,7 @@
              <number>4</number>
             </property>
             <item row="0" column="1">
-             <widget class="FormWidget" name="minSpaceLabel">
+             <widget class="FormWidget" name="minSpaceLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -326,7 +326,7 @@
              </widget>
             </item>
             <item row="1" column="2">
-             <widget class="FormWidget" name="maxGlyphExtLabel">
+             <widget class="FormWidget" name="maxGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -379,7 +379,7 @@
              </spacer>
             </item>
             <item row="0" column="0">
-             <widget class="FormWidget" name="formWidget_3">
+             <widget class="FormWidget" name="formWidget_3" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -389,7 +389,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout">
@@ -419,7 +419,7 @@
              </widget>
             </item>
             <item row="1" column="1">
-             <widget class="FormWidget" name="minGlyphExtLabel">
+             <widget class="FormWidget" name="minGlyphExtLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -459,7 +459,7 @@
              </widget>
             </item>
             <item row="1" column="0">
-             <widget class="FormWidget" name="formWidget_4">
+             <widget class="FormWidget" name="formWidget_4" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -469,7 +469,7 @@
               <property name="label" stdset="0">
                <string/>
               </property>
-              <property name="preserveLabelSpace">
+              <property name="preserveLabelSpace" stdset="0">
                <bool>true</bool>
               </property>
               <layout class="QHBoxLayout" name="horizontalLayout_2">
@@ -597,7 +597,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="lineSpacingLabel">
+               <widget class="FormWidget" name="lineSpacingLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -673,7 +673,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="spaceAboveLabel">
+               <widget class="FormWidget" name="spaceAboveLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -710,7 +710,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="spaceBelowLabel">
+               <widget class="FormWidget" name="spaceBelowLabel" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -804,7 +804,7 @@
                <number>16</number>
               </property>
               <item>
-               <widget class="FormWidget" name="keepLabelStart">
+               <widget class="FormWidget" name="keepLabelStart" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -844,7 +844,7 @@
                </widget>
               </item>
               <item>
-               <widget class="FormWidget" name="keepLabelEnd">
+               <widget class="FormWidget" name="keepLabelEnd" native="true">
                 <property name="sizePolicy">
                  <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                   <horstretch>0</horstretch>
@@ -1021,7 +1021,7 @@
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_20">
          <item>
-          <widget class="FormWidget" name="peLabel">
+          <widget class="FormWidget" name="peLabel" native="true">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
@@ -1441,7 +1441,7 @@
               </widget>
              </item>
              <item row="0" column="6">
-              <widget class="FormWidget" name="formWidget_2">
+              <widget class="FormWidget" name="formWidget_2" native="true">
                <property name="sizePolicy">
                 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                  <horstretch>0</horstretch>
@@ -1451,10 +1451,7 @@
                <property name="label" stdset="0">
                 <string>New Set:</string>
                </property>
-               <property name="direction">
-                <enum>FormWidget::LabelPosition::Left</enum>
-               </property>
-               <property name="useSmallFont">
+               <property name="useSmallFont" stdset="0">
                 <bool>false</bool>
                </property>
                <layout class="QHBoxLayout" name="horizontalLayout_25">
@@ -1476,6 +1473,20 @@
                </layout>
               </widget>
              </item>
+             <item row="3" column="6">
+              <widget class="SMScComboBox" name="markerAlignmentCombo">
+               <property name="toolTip">
+                <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alignment of marker characters in numbered lists&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+               </property>
+              </widget>
+             </item>
+             <item row="3" column="5">
+              <widget class="QLabel" name="markerAlignmentLabel">
+               <property name="text">
+                <string>Marker Align:</string>
+               </property>
+              </widget>
+             </item>
             </layout>
            </item>
           </layout>
@@ -1506,7 +1517,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="distFromTextLabel">
+             <widget class="FormWidget" name="distFromTextLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1546,46 +1557,6 @@
              </widget>
             </item>
             <item>
-             <widget class="FormWidget" name="formWidget">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="label" stdset="0">
-               <string/>
-              </property>
-              <property name="preserveLabelSpace">
-               <bool>true</bool>
-              </property>
-              <layout class="QHBoxLayout" name="horizontalLayout_24">
-               <property name="leftMargin">
-                <number>0</number>
-               </property>
-               <property name="topMargin">
-                <number>0</number>
-               </property>
-               <property name="rightMargin">
-                <number>0</number>
-               </property>
-               <property name="bottomMargin">
-                <number>0</number>
-               </property>
-               <item>
-                <widget class="SMCheckBox" name="parEffectIndentBox">
-                 <property name="toolTip">
-                  <string>Hang Paragraph Effect before paragraph indent</string>
-                 </property>
-                 <property name="text">
-                  <string>Auto-Indent</string>
-                 </property>
-                </widget>
-               </item>
-              </layout>
-             </widget>
-            </item>
-            <item>
              <spacer name="horizontalSpacer_14">
               <property name="orientation">
                <enum>Qt::Orientation::Horizontal</enum>
@@ -1606,7 +1577,7 @@
              <number>16</number>
             </property>
             <item>
-             <widget class="FormWidget" name="parEffectCharStyleComboLabel">
+             <widget class="FormWidget" name="parEffectCharStyleComboLabel" native="true">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                 <horstretch>0</horstretch>
@@ -1792,7 +1763,6 @@
   <tabstop>parentParEffectsButton</tabstop>
   <tabstop>dropCapLines</tabstop>
   <tabstop>parEffectOffset</tabstop>
-  <tabstop>parEffectIndentBox</tabstop>
   <tabstop>parEffectCharStyleCombo</tabstop>
   <tabstop>numPrefix</tabstop>
   <tabstop>numSuffix</tabstop>
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp	(revision 27824)
+++ scribus/ui/smtextstyles.cpp	(working copy)
@@ -37,7 +37,6 @@
 #include "units.h"
 #include "util.h"
 
-
 SMParagraphStyle::SMParagraphStyle(SMCharacterStyle* cstyleItem):
 	m_cstyleItem(cstyleItem)
 {
@@ -511,7 +510,7 @@
 	connect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	connect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	connect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	connect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
+	connect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	connect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
 	connect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	connect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
@@ -606,8 +605,7 @@
 	disconnect(m_pwidget->peCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPargraphEffects(int)));
 	disconnect(m_pwidget->dropCapLines, SIGNAL(valueChanged(int)), this, SLOT(slotDropCapLines(int)));
 	disconnect(m_pwidget->parEffectOffset, SIGNAL(valueChanged(double)), this, SLOT(slotParEffectOffset()));
-	disconnect(m_pwidget->parEffectIndentBox, SIGNAL(toggled(bool)), this, SLOT(slotParEffectIndent(bool)));
-	disconnect(m_pwidget->parEffectCharStyleCombo, SIGNAL(activated(int)), this, SLOT(slotParEffectCharStyle(int)));
+	disconnect(m_pwidget->markerAlignmentCombo, SIGNAL(activated(int)), this, SLOT(slotMarkerAlignment(int)));
 	disconnect(m_pwidget->bulletStrEdit, SIGNAL(editTextChanged(QString)), this, SLOT(slotBulletStr(QString)));
 	disconnect(m_pwidget->numComboBox, SIGNAL(textActivated(QString)), this, SLOT(slotNumName(QString)));
 	disconnect(m_pwidget->numFormatCombo, SIGNAL(activated(int)), this, SLOT(slotNumFormat(int)));
@@ -953,17 +951,25 @@
 	slotSelectionDirty();
 }
 
-void SMParagraphStyle::slotParEffectIndent(bool isOn)
+void SMParagraphStyle::slotMarkerAlignment(int index)
 {
-	if (m_pwidget->parEffectIndentBox->useParentValue())
+	if (m_pwidget->markerAlignmentCombo->useParentValue())
+	{
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->resetParEffectIndent();
-	else 
+			m_selection[i]->resetMarkerAlignment();
+	}
+	else
 	{
+		auto align = static_cast<ParagraphStyle::MarkerAlignment>(m_pwidget->markerAlignmentCombo->itemData(index).toInt());
 		for (int i = 0; i < m_selection.count(); ++i)
-			m_selection[i]->setParEffectIndent(isOn);
+		{
+			ParagraphStyle::MarkerAlignment styleAlign = align;
+			if (m_selection[i]->direction() == ParagraphStyle::RTL)
+				styleAlign = m_selection[i]->flipMarkerAlignmentForRTL(styleAlign);
+			m_selection[i]->setMarkerAlignment(styleAlign);
+		}
 	}
-	
+
 	slotSelectionDirty();
 }
 
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h	(revision 27824)
+++ scribus/ui/smtextstyles.h	(working copy)
@@ -77,7 +77,7 @@
 	void slotPargraphEffects(int index);
 	void slotDropCapLines(int lines);
 	void slotParEffectOffset();
-	void slotParEffectIndent(bool);
+	void slotMarkerAlignment(int);
 	void slotParEffectCharStyle(int);
 	void slotBulletStr(const QString &str);
 	void slotNumName(const QString &str);
markerAlignmentv9.1.patch (67,864 bytes)   

qirat

2026-09-08 14:33

reporter   ~0054441

@Fahad, I am sorry if should not have done this, but the size difference between the two (my upload and yours) made me ask AI. It analysed/compared in detail (I can send you that too if you say). Here is the summary of that rated for conciseness.

Please see.

Fahad

2026-09-08 17:22

developer   ~0054442

@qirat I’d take the AI summary with a grain of salt—I can back up every point made here.

A few key corrections:

* Compatibility: As we noted on another bug, Scribus 1.7 breaks compatibility in several ways, so retaining old, broken code (like auto-indent) doesn't add value. Adding a legacy option here feels misplaced.

* Performance: Have you run your code against my stress-test file (11,250 list items)? A core goal of my patch is comprehensive caching to prevent Scribus from lagging on large files. Dissecting changes to ScText_Shared, StoryText, and scribusdoc.cpp without considering performance isn't a fair comparison—a no-cache patch inherently looks smaller than a comprehensive caching patch, which unfairly skews the reviewability and maintainability scores.

* Bug Fixes: My patch fixes several critical issues missing from marker-alignment-v1.03.patch:
  - Marker widths incorrectly sharing dimensions between a large local list and a small local list.
  - Two different, adjacent local numbered lists incorrectly merging into a single alignment block when not separated by a paragraph.
  - Misalignment on the second line of numbered and bulleted lists (fixing this is what impacts compatibility).

Ultimately, we could drop this approach entirely and adopt a fixed-width layout like LibreOffice, InDesign, and LaTeX. Dynamic marker alignment creates unnecessary complexity for the end user, as outlined above.

qirat

2026-09-09 01:15

reporter   ~0054444

@Fahad. Thanks for your explanation (I cannot argue that with it). I confirmed (as @ale stated) compatibility-wise. Here is its reply and revised evaluation:
---------------
I think several of your points are fair.

* Compatibility: agreed. If Scribus 1.7 can deliberately break compatibility here and Auto-Indent is considered obsolete/broken, preserving it or adding Legacy is not a strong requirement.

* Performance: agreed. The smaller patch looks cleaner partly because it does not solve the 11,250-item case. If the StoryText/ScText_Shared caching is what keeps large linked stories responsive, that extra complexity is justified.

* Local-list width sharing: agreed. `marker-alignment-v1.03` does not distinguish `<local block>` instances well enough, so separate local lists can share the wrong maximum width.

* Adjacent local lists: agreed. Two distinct adjacent local lists can be merged into one alignment block by the smaller patch.

* Wrapped second lines: agreed. This is a real correctness gap in the smaller patch; your implementation handles the continuation-line indent more completely.

So between the two current dynamic-width implementations, I now think `suffixalignv9` is the stronger one functionally and for scalability.

Where I would still improve `suffixalignv9`:

* Fix the persistence mismatch between `SuffixAlignment` and `MarkerAlignment`.
* Restore the existing `parEffectCharStyleCombo` disconnect in `SMParagraphStyle::disconnectSignals()`; the new marker-alignment disconnect should be added, not replace it.
* Replace manual `blockSignals(true)` / `blockSignals(false)` pairs with `QSignalBlocker` or proper restoration of the previous signal state.
* Review whether all StoryText/TextShaper optimizations are truly required for this feature; if some are independent optimizations, they would be cleaner as separate patches.
* Consider a cleaner local-list identity mechanism than exposing/storing raw `const void*` style identity, while keeping the behavior your implementation correctly achieves.

I also think your fixed-geometry alternative is worth evaluating before finalizing either dynamic approach, because it could remove much of the width-scanning, caching and list-identity complexity entirely.
---------------

IMO if others (ID, LO) are not doing this way, then it is the more reason for Scribus to have this feature.
revised.png (66,407 bytes)   
revised.png (66,407 bytes)   

Issue History

Date Modified Username Field Change
2024-07-01 13:00 ale New Issue
2024-07-01 13:02 ale Relationship added related to 0016985
2024-07-01 13:02 ale Relationship added related to 0016963
2025-02-20 09:15 ale Note Added: 0052074
2025-02-20 09:15 ale File Added: autoindent.png
2025-02-20 09:15 ale File Added: autoindent-correct.png
2025-02-20 14:10 ale Note Added: 0052075
2025-02-20 14:10 ale File Added: numbering-nitramr.png
2025-02-20 15:31 ale Note Added: 0052076
2025-02-20 15:32 ale Note Edited: 0052076
2025-02-20 20:47 ale Note Added: 0052079
2025-02-20 20:47 ale File Added: enumarte-left-aligned.png
2025-02-20 22:03 nitramr Note Added: 0052080
2025-02-20 22:03 nitramr File Added: Document-1.sla
2025-02-20 22:04 nitramr Note Edited: 0052080
2025-02-21 17:36 ale Note Added: 0052083
2025-02-21 18:59 nitramr Note Added: 0052086
2025-02-22 08:23 ale Relationship added related to 0013488
2025-02-22 09:26 ale Note Added: 0052093
2025-04-27 19:16 cbradney Category Story Editor / Text Frames => Text Frames / Story Editor
2026-02-26 15:50 qirat Note Added: 0053542
2026-02-26 15:50 qirat Note Added: 0053543
2026-07-06 03:59 Fahad Note Added: 0053980
2026-07-06 04:55 qirat Note Added: 0053985
2026-07-09 08:14 Fahad Note Added: 0054001
2026-07-09 08:14 Fahad File Added: suffixalignment v1.patch
2026-07-09 08:14 Fahad File Added: suffix-align.png
2026-07-09 12:19 qirat Note Added: 0054002
2026-07-09 12:19 qirat File Added: align_suffix_test.png
2026-07-09 13:10 qirat Note Added: 0054003
2026-07-09 13:10 qirat File Added: list-marker-alignment-v1.2-r27703-remove-autoindent-ui.patch
2026-07-10 01:02 qirat Note Added: 0054006
2026-07-13 15:01 Fahad Note Added: 0054029
2026-07-13 15:01 Fahad File Added: suffixalignment v2.patch
2026-07-14 03:29 qirat Note Added: 0054030
2026-07-14 03:29 qirat File Added: Screenshot From 2026-07-14 08-42-43.png
2026-07-14 03:29 qirat File Added: Screenshot From 2026-07-14 08-28-30.png
2026-07-14 04:04 Fahad Note Added: 0054031
2026-07-14 04:04 Fahad File Added: 1.png
2026-07-14 04:04 Fahad File Added: 2.png
2026-07-14 05:17 qirat Note Added: 0054032
2026-07-14 06:11 Fahad Note Added: 0054033
2026-07-14 11:31 Fahad Note Added: 0054034
2026-07-14 11:31 Fahad File Added: suffixalignmentv3.patch
2026-07-15 08:00 qirat Note Added: 0054038
2026-07-15 08:00 qirat File Added: Screenshot From 2026-07-15 09-43-16.png
2026-07-15 13:38 qirat Note Added: 0054041
2026-07-16 06:46 ale Note Added: 0054043
2026-07-16 06:48 ale Note Edited: 0054043
2026-07-16 10:58 qirat Note Added: 0054044
2026-07-16 11:21 Fahad Note Added: 0054045
2026-07-16 15:57 ale Note Added: 0054046
2026-07-16 15:57 ale File Added: numbered-list.png
2026-07-16 16:00 ale Note Added: 0054047
2026-07-16 16:00 ale File Added: list-watson.png
2026-07-16 16:03 ale Note Edited: 0054047
2026-07-16 19:23 qirat Note Added: 0054048
2026-07-28 08:47 Fahad Note Added: 0054077
2026-07-28 11:13 qirat Note Added: 0054090
2026-07-28 14:11 Fahad Note Added: 0054091
2026-08-04 11:57 qirat Note Added: 0054120
2026-08-04 13:36 qirat Note Added: 0054126
2026-08-04 13:36 qirat File Added: upper-and-lowercase-suffix-align.mp4
2026-08-05 04:35 Fahad Note Added: 0054130
2026-08-05 04:35 Fahad File Added: upper-and-lowercase-suffix-align-fedora44.webm
2026-08-05 06:55 qirat Note Added: 0054138
2026-08-05 06:58 qirat Note Added: 0054139
2026-08-11 06:41 Fahad Note Added: 0054196
2026-08-11 06:41 Fahad File Added: new.png
2026-08-11 06:41 Fahad File Added: suffixalignmentv4.patch
2026-08-12 02:18 qirat Note Added: 0054197
2026-08-12 04:05 Fahad Note Added: 0054198
2026-08-12 04:05 Fahad File Added: suffixalignmentv5.patch.patch
2026-08-12 04:49 qirat Note Added: 0054199
2026-08-12 07:53 qirat Note Added: 0054200
2026-08-12 07:53 qirat File Added: suffix-align-v5.jpg
2026-08-12 07:54 qirat Note Added: 0054201
2026-08-12 09:33 qirat Note Added: 0054202
2026-08-12 09:33 qirat File Added: suffix-alignment-v5.1.patch
2026-08-16 18:16 Fahad Note Added: 0054215
2026-08-16 18:16 Fahad File Added: big-list.sla.gz
2026-08-16 18:16 Fahad File Added: suffixalignv6.patch
2026-08-17 10:19 qirat Note Added: 0054218
2026-08-17 10:19 qirat File Added: suffix-align-v6.jpg
2026-08-17 10:21 qirat Note Added: 0054219
2026-08-18 14:02 Fahad Note Added: 0054226
2026-08-18 14:02 Fahad File Added: suffixalignv7.patch
2026-08-18 16:03 qirat Note Added: 0054227
2026-08-18 16:04 qirat Note Added: 0054228
2026-08-20 13:47 ale Note Added: 0054251
2026-08-20 13:47 ale File Added: list-asterisk.png
2026-08-20 13:47 ale File Added: list-first-character.gif
2026-08-20 13:47 ale File Added: list-no-hanging-indent.png
2026-08-20 13:47 ale File Added: list-roman-too-wide.png
2026-08-20 13:47 ale File Added: list-rtl-twice-apply.gif
2026-08-20 13:47 ale Note Edited: 0054251
2026-08-20 13:51 ale Note Edited: 0054251
2026-09-04 06:34 Fahad Note Added: 0054411
2026-09-04 06:34 Fahad File Added: suffixalignv8.patch
2026-09-04 13:28 qirat Note Added: 0054413
2026-09-04 13:28 qirat File Added: marker-alignment-v1.03.patch
2026-09-04 13:30 qirat Note Added: 0054414
2026-09-07 08:18 Fahad Note Added: 0054433
2026-09-07 08:18 Fahad File Added: suffixalignv9.patch
2026-09-08 13:56 Fahad Note Added: 0054440
2026-09-08 13:56 Fahad File Added: markerAlignmentv9.1.patch
2026-09-08 14:33 qirat Note Added: 0054441
2026-09-08 14:33 qirat File Added: marker-alignment-v1.03-vs-suffix-v9.png
2026-09-08 17:22 Fahad Note Added: 0054442
2026-09-09 01:15 qirat Note Added: 0054444
2026-09-09 01:15 qirat File Added: revised.png