include/oox/export/drawingml.hxx | 5 + oox/qa/unit/data/outliner-list-style.odp |binary oox/qa/unit/export.cxx | 25 +++++ oox/source/export/drawingml.cxx | 129 ++++++++++++++++++++++++++++-- vcl/inc/osx/salframe.h | 2 vcl/osx/salframe.cxx | 4 vcl/osx/salnativewidgets.cxx | 133 ++++++++++++++++++++++++++++--- 7 files changed, 277 insertions(+), 21 deletions(-)
New commits: commit 5b9881108be4168d22ad08d961d767e48dff253f Author: Patrick Luby <[email protected]> AuthorDate: Sun Aug 31 11:53:05 2025 -0400 Commit: Patrick Luby <[email protected]> CommitDate: Fri Sep 26 16:14:58 2025 +0200 Fix tab item drawing failures on macOS Tahoe Starting in macOS Tahoe, there are two issues that affect drawing of tab items: 1. When compiled and run on macOS Tahoe, the tab item will fail to draw if the NSSegmentedControl is not a subview of a window whether the window is visible or not. 2. The selected tab item has the same bright background color as the default push button when the tab item is in the key window. In previous versions of macOS, the same background color is used for the selected tab item whether the tab item is enabled or not. Both issues are fixed by adding the NSSegmentedControl as a subview of a window. Note that the window must be a key window to draw the selected tab item with the new bright background color. Also, macOS Tahoe has slightly larger tab items than previous version so adjust tab item drawing bounds when built and run on macOS Tahoe. Change-Id: I56fa352d7a18f748e278f03c0e9c9bb483452466 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190438 Tested-by: Jenkins Reviewed-by: Patrick Luby <[email protected]> diff --git a/vcl/inc/osx/salframe.h b/vcl/inc/osx/salframe.h index f338a2b78e5f..f96bd6bb07bf 100644 --- a/vcl/inc/osx/salframe.h +++ b/vcl/inc/osx/salframe.h @@ -98,6 +98,8 @@ public: bool mbForceFlushScrolling; // tdf#164428 force flush after drawing a progress bar bool mbForceFlushProgressBar; + // Force flush after drawing tab items + bool mbForceFlushTabItems; // Is window in LibreOffice full screen mode bool mbInternalFullScreen; diff --git a/vcl/osx/salframe.cxx b/vcl/osx/salframe.cxx index a83c9907674b..ca5938b48431 100644 --- a/vcl/osx/salframe.cxx +++ b/vcl/osx/salframe.cxx @@ -89,6 +89,7 @@ AquaSalFrame::AquaSalFrame( SalFrame* pParent, SalFrameStyleFlags salFrameStyle mnBlinkCursorDelay( nMinBlinkCursorDelay ), mbForceFlushScrolling( false ), mbForceFlushProgressBar( false ), + mbForceFlushTabItems( false ), mbInternalFullScreen( false ), maInternalFullScreenRestoreRect( NSZeroRect ), maInternalFullScreenExpectedRect( NSZeroRect ), @@ -1185,7 +1186,7 @@ bool AquaSalFrame::doFlush() { bool bRet = false; - if( mbForceFlushScrolling || mbForceFlushProgressBar || ImplGetSVData()->maAppData.mnDispatchLevel <= 0 ) + if( mbForceFlushScrolling || mbForceFlushProgressBar || mbForceFlushTabItems || ImplGetSVData()->maAppData.mnDispatchLevel <= 0 ) { mpGraphics->Flush(); @@ -1196,6 +1197,7 @@ bool AquaSalFrame::doFlush() mbForceFlushScrolling = false; mbForceFlushProgressBar = false; + mbForceFlushTabItems = false; } return bRet; diff --git a/vcl/osx/salnativewidgets.cxx b/vcl/osx/salnativewidgets.cxx index c905fa9acf48..0b40426dcfe4 100644 --- a/vcl/osx/salnativewidgets.cxx +++ b/vcl/osx/salnativewidgets.cxx @@ -31,6 +31,7 @@ #include <osx/salnativewidgets.h> #include <osx/saldata.hxx> #include <osx/salframe.h> +#include <osx/salframeview.h> #include <premac.h> #include <Carbon/Carbon.h> @@ -460,6 +461,11 @@ static constexpr int spinButtonWidth() return 16; } +static NSWindow* createControlDrawingWindow() +{ + return [[[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, 1, 1) styleMask: NSWindowStyleMaskBorderless backing: NSBackingStoreBuffered defer: NO] autorelease]; +} + // As seen in macOS 12.3.1. All a bit odd really. const int RoundedMargin[4] = { 6, 4, 0, 3 }; @@ -937,16 +943,75 @@ bool AquaGraphicsBackendBase::performDrawNativeControl(ControlType nType, NSSegmentedControl* pCtrl = [[NSSegmentedControl alloc] initWithFrame: ctrlrect]; [pCtrl setSegmentCount: nCells]; if (bSolo) - [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH forSegment: 0]; + { +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 + if (@available(macOS 26, *)) + [pCtrl setWidth: rc.size.width forSegment: 0]; + else +#endif + [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH forSegment: 0]; + } else { - [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH/2 forSegment: 0]; - [pCtrl setWidth: rc.size.width forSegment: 1]; - [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH/2 forSegment: 2]; +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 + if (@available(macOS 26, *)) + { + [pCtrl setWidth: rc.size.width - FOCUS_RING_WIDTH/2 forSegment: 0]; + [pCtrl setWidth: rc.size.width - FOCUS_RING_WIDTH/4 forSegment: 1]; + [pCtrl setWidth: rc.size.width - FOCUS_RING_WIDTH/2 forSegment: 2]; + } + else +#endif + { + + [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH/2 forSegment: 0]; + [pCtrl setWidth: rc.size.width forSegment: 1]; + [pCtrl setWidth: rc.size.width + FOCUS_RING_WIDTH/2 forSegment: 2]; + } } [pCtrl setSelected: (nState & ControlState::SELECTED) ? YES : NO forSegment: nPaintIndex]; [pCtrl setFocusRingType: NSFocusRingTypeExterior]; + // Fix tab item drawing failures on macOS Tahoe + // Starting in macOS Tahoe, there are two issues that affect + // drawing of tab items: + // 1. When compiled and run on macOS Tahoe, the tab item will + // fail to draw if the NSSegmentedControl is not a subview + // of a window whether the window is visible or not. + // 2. The selected tab item has the same bright background + // color as the default push button when the tab item is + // in the key window. In previous versions of macOS, the + // same background color is used for the selected tab + // item whether the tab item is enabled or not. + // Both issues are fixed by adding the NSSegmentedControl + // as a subview of a window. Note that the window must be + // a key window to draw the selected tab item with the new + // bright background color. + if (@available(macOS 26, *)) + { + NSWindow *pKeyWindow = nil; + if (mpFrame) + { + // Note: use -[NSWindow isKeyWindow] as it may return + // YES even when +[NSApp keyWindow] is nil. + NSWindow *pFrameWindow = mpFrame->getNSWindow(); + if (pFrameWindow && [pFrameWindow isKindOfClass: [SalFrameWindow class]] && [pFrameWindow isKeyWindow]) + pKeyWindow = pFrameWindow; + } + + // Default to adding the NSSegmentedControl to a window + // that is not visible. However, in order to draw selected + // tab items with the new bright background color, the + // window must be visible since only visible windows can + // be the key window. In that case, add the control to + // the key window. + NSWindow *pControlDrawingWindow = pKeyWindow; + if (!pControlDrawingWindow) + pControlDrawingWindow = createControlDrawingWindow(); + if (pControlDrawingWindow) + [[pControlDrawingWindow contentView] addSubview: pCtrl positioned: NSWindowBelow relativeTo: nil]; + } + NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:context flipped:NO]]; @@ -967,26 +1032,65 @@ bool AquaGraphicsBackendBase::performDrawNativeControl(ControlType nType, [NSGraphicsContext setCurrentContext:savedContext]; + if (@available(macOS 26, *)) + [pCtrl removeFromSuperviewWithoutNeedingDisplay]; [pCtrl release]; if (nState & ControlState::FOCUSED) { - if (!bSolo) + if (bSolo) { - if (nPaintIndex == 0) +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 + if (@available(macOS 26, *)) { - rc.origin.x += FOCUS_RING_WIDTH / 2; - rc.size.width -= FOCUS_RING_WIDTH / 2; + rc.origin.y += FOCUS_RING_WIDTH / 4; + rc.size.height -= FOCUS_RING_WIDTH / 2; } - else if (nPaintIndex == 2) +#endif + } + else + { +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 + if (@available(macOS 26, *)) { - rc.size.width -= FOCUS_RING_WIDTH / 2; - rc.size.width -= FOCUS_RING_WIDTH / 2; + if (nPaintIndex == 0) + rc.origin.x += FOCUS_RING_WIDTH / 2; + else + rc.origin.x += FOCUS_RING_WIDTH / 4; + rc.origin.y += FOCUS_RING_WIDTH / 2; + if (nPaintIndex == 1) + rc.size.width -= FOCUS_RING_WIDTH / 2; + else + rc.size.width -= FOCUS_RING_WIDTH * 3 / 4; + rc.size.height -= FOCUS_RING_WIDTH; + } + else +#endif + { + if (nPaintIndex == 0) + { + rc.origin.x += FOCUS_RING_WIDTH / 2; + rc.size.width -= FOCUS_RING_WIDTH / 2; + } + else if (nPaintIndex == 2) + { + rc.size.width -= FOCUS_RING_WIDTH / 2; + rc.size.width -= FOCUS_RING_WIDTH / 2; + } } } paintFocusRect(4.0, rc, context); } + + // The Skia flushing timer appears to stop when modal windows + // (e.g. the native Open or Print dialogs) are displayed + if (@available(macOS 26, *)) + { + if (mpFrame && [NSApp modalWindow]) + mpFrame->mbForceFlushTabItems = [NSApp modalWindow]; + } + bOK=true; } break; @@ -1299,7 +1403,12 @@ bool AquaSalGraphics::getNativeControlRegion(ControlType nType, case ControlType::TabItem: { w = aCtrlBoundRect.GetWidth() + 2 * TAB_TEXT_MARGIN; - h = TAB_HEIGHT + 2; +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 + if (@available(macOS 26, *)) + h = TAB_HEIGHT + 2 + FOCUS_RING_WIDTH; + else +#endif + h = TAB_HEIGHT + 2; rNativeContentRegion = tools::Rectangle(Point(x, y), Size(w, h)); rNativeBoundingRegion = tools::Rectangle(Point(x, y), Size(w, h)); toReturn = true; commit b4d45c41ce5b9711934d79ca3c5618d13d7c0bcf Author: Miklos Vajna <[email protected]> AuthorDate: Fri Sep 26 11:52:23 2025 +0200 Commit: Miklos Vajna <[email protected]> CommitDate: Fri Sep 26 16:14:54 2025 +0200 tdf#168559 PPTX exp: fix missing non-first level list style for outline shapes Export the bugdoc to PPTX, open in PowerPoint, go to the first slide, go to the end of the first bullet, enter, tab, indent grows to a large value, while it should only grow to match the existing other already indented bullet. It seems this happens because these indents are defined in the "list style" of the shape, where we wrote the paragraph properties only for the first level, see commit 0f9dc676eefce79ea63218edd910af486a09a52b (tdf#59323: pptx export: add initial support for lstStyles in textboxes, 2021-06-16). Fix the problem by writing paragraph properties for all list levels that the outliner shape stores: Impress has 7 levels and PowerPoint has markup for 9 levels, so in practice this can be mapped without problems. A further improvement would be to make sure the outline shape on the normal page and the master page is associated correctly, and then it would not be necessary to repeat the formatting for shapes on the normal slide. Similarly, the PPTX import still needs to parse this list style of the shape, which is not yet done here, so the editing in PowerPoint is now correct, but not in Impress. Change-Id: I1aecb3c062ccbe5014d322d0561f0d2ff50ac698 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191544 Reviewed-by: Miklos Vajna <[email protected]> Tested-by: Jenkins diff --git a/include/oox/export/drawingml.hxx b/include/oox/export/drawingml.hxx index 356f4ef5cc22..d68e0e804fbe 100644 --- a/include/oox/export/drawingml.hxx +++ b/include/oox/export/drawingml.hxx @@ -458,6 +458,11 @@ public: sal_Int32 nXmlNamespace, bool bIsFontworkShape, sal_Int32 nTop, sal_Int32 nBottom, sal_Int32 nLeft, sal_Int32 nRight); + /// Writes one list level inside the list styles container. + void WriteLstStyle(const css::uno::Reference<css::text::XTextContent>& rParagraph, + bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet, + sal_Int32 nElement); /** Populates the lstStyle with the shape's text run and paragraph properties */ void WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, diff --git a/oox/qa/unit/data/outliner-list-style.odp b/oox/qa/unit/data/outliner-list-style.odp new file mode 100644 index 000000000000..dc86caf053bf Binary files /dev/null and b/oox/qa/unit/data/outliner-list-style.odp differ diff --git a/oox/qa/unit/export.cxx b/oox/qa/unit/export.cxx index 63351ae79e4b..30273cd2acf2 100644 --- a/oox/qa/unit/export.cxx +++ b/oox/qa/unit/export.cxx @@ -1432,6 +1432,31 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf163803_ImageFill) assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill"); assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill/a:srgbClr", "val", u"000000"); } + +CPPUNIT_TEST_FIXTURE(Test, testPPTXExportOutlinerListStyle) +{ + // Given a slide with an outliner shape and a matching master slide with its own outliner shape: + loadFromFile(u"outliner-list-style.odp"); + + // When saving to PPTX: + save(u"Impress Office Open XML"_ustr); + + // Then make sure that the list style of the outliner shape on the master page is written: + xmlDocUniquePtr pXmlDoc = parseExport(u"ppt/slideMasters/slideMaster1.xml"_ustr); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl1pPr", 1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 0 + // - XPath '//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr' number of nodes is incorrect + // i.e. only the first list level was written, the UI couldn't format a new 2nd level paragraph + // correctly. + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl3pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl4pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl5pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl6pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl7pPr", 1); +} } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx index 4bb458bd0111..0eef9173abbb 100644 --- a/oox/source/export/drawingml.cxx +++ b/oox/source/export/drawingml.cxx @@ -138,6 +138,7 @@ #include <svx/EnhancedCustomShape2d.hxx> #include <drawingml/presetgeometrynames.hxx> #include <docmodel/uno/UnoGradientTools.hxx> +#include <svx/svdpage.hxx> using namespace ::css; using namespace ::css::beans; @@ -3649,15 +3650,31 @@ bool DrawingML::WriteParagraphProperties(const Reference<XTextContent>& rParagra WriteParagraphTabStops( rXPropSet ); // do not end element for lstStyles since, defRPr should be stacked inside it - if( nElement != XML_lvl1pPr ) + bool bLstStyle = false; + switch (nElement) + { + case XML_lvl1pPr: + case XML_lvl2pPr: + case XML_lvl3pPr: + case XML_lvl4pPr: + case XML_lvl5pPr: + case XML_lvl6pPr: + case XML_lvl7pPr: + case XML_lvl8pPr: + case XML_lvl9pPr: + bLstStyle = true; + break; + } + if( !bLstStyle ) mpFS->endElementNS( XML_a, nElement ); return true; } -void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, +void DrawingML::WriteLstStyle(const css::uno::Reference<css::text::XTextContent>& rParagraph, bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, - const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet) + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet, + sal_Int32 nElement) { Reference<XEnumerationAccess> xAccess(rParagraph, UNO_QUERY); if (!xAccess.is()) @@ -3684,14 +3701,110 @@ void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent if (xFirstRunPropSetInfo->hasPropertyByName(u"CharHeight"_ustr)) fFirstCharHeight = xFirstRunPropSet->getPropertyValue(u"CharHeight"_ustr).get<float>(); - mpFS->startElementNS(XML_a, XML_lstStyle); - if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, XML_lvl1pPr) ) - mpFS->startElementNS(XML_a, XML_lvl1pPr); + if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, nElement) ) + mpFS->startElementNS(XML_a, nElement); WriteRunProperties(xFirstRunPropSet, false, XML_defRPr, true, rbOverridingCharHeight, rnCharHeight, GetScriptType(rRun->getString()), rXShapePropSet); - mpFS->endElementNS(XML_a, XML_lvl1pPr); - mpFS->endElementNS(XML_a, XML_lstStyle); + mpFS->endElementNS(XML_a, nElement); + } +} + +namespace +{ +/// In case xShapeProps is an outliner shape, return a paragraph enumeration describing the outline +/// text format. +uno::Reference<container::XEnumeration> GetOutlinerTextFormatParaEnum(const uno::Reference<beans::XPropertySet>& xShapeProps) +{ + SdrObject* pShape = SdrObject::getSdrObjectFromXShape(xShapeProps); + if (pShape->GetObjIdentifier() != SdrObjKind::OutlineText) + { + // Not an outliner shape. + return {}; + } + + SdrPage* pPage = pShape->getSdrPageFromSdrObject(); + if (pPage->TRG_HasMasterPage()) + { + // Not a master page, the matching master page contains the shape that provides the outline + // text format. + SdrPage& rMasterPage = pPage->TRG_GetMasterPage(); + for (const rtl::Reference<SdrObject>& pObject : rMasterPage) + { + if (pObject->GetObjIdentifier() == SdrObjKind::OutlineText) + { + pShape = pObject.get(); + break; + } + } } + + uno::Reference<text::XTextRange> xOutliner(pShape->getUnoShape(), uno::UNO_QUERY); + uno::Reference<container::XEnumerationAccess> xText(xOutliner->getText(), uno::UNO_QUERY); + return xText->createEnumeration(); +} +} + +void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, + bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet) +{ + mpFS->startElementNS(XML_a, XML_lstStyle); + + uno::Reference<container::XEnumeration> xMasterParagraphEnum = GetOutlinerTextFormatParaEnum(rXShapePropSet); + if (xMasterParagraphEnum.is()) + { + // Outliner shape, write the outline text format as multiple list levels, each with its + // paragraph properties. + sal_Int32 nLevel = 0; + while (xMasterParagraphEnum->hasMoreElements()) + { + uno::Reference<css::text::XTextContent> xParagraph(xMasterParagraphEnum->nextElement(), uno::UNO_QUERY); + sal_Int32 nElement = 0; + switch (nLevel) + { + case 0: + nElement = XML_lvl1pPr; + break; + case 1: + nElement = XML_lvl2pPr; + break; + case 2: + nElement = XML_lvl3pPr; + break; + case 3: + nElement = XML_lvl4pPr; + break; + case 4: + nElement = XML_lvl5pPr; + break; + case 5: + nElement = XML_lvl6pPr; + break; + case 6: + nElement = XML_lvl7pPr; + break; + case 7: + nElement = XML_lvl8pPr; + break; + case 8: + nElement = XML_lvl9pPr; + break; + } + if (!nElement) + { + break; + } + + WriteLstStyle(xParagraph, rbOverridingCharHeight, rnCharHeight, rXShapePropSet, nElement); + ++nLevel; + } + } + else + { + WriteLstStyle(rParagraph, rbOverridingCharHeight, rnCharHeight, rXShapePropSet, XML_lvl1pPr); + } + + mpFS->endElementNS(XML_a, XML_lstStyle); } void DrawingML::WriteParagraph( const Reference< XTextContent >& rParagraph,
