editeng/source/editeng/impedit.cxx |    7 -
 include/editeng/outliner.hxx       |    3 
 sd/source/ui/inc/OutlineView.hxx   |   18 ++
 sd/source/ui/view/outlview.cxx     |  239 ++++++++++++++++++++++++++++++++++---
 4 files changed, 251 insertions(+), 16 deletions(-)

New commits:
commit ec4da4667d7f948219d51daaf21cda8d5cd2f4ad
Author:     Armin Le Grand (collabora) <armin.legr...@collabora.com>
AuthorDate: Wed Aug 13 19:45:13 2025 +0200
Commit:     Armin Le Grand <armin.le.gr...@me.com>
CommitDate: Thu Aug 14 13:42:55 2025 +0200

    OutlinerView: Rework for standard selection visualization
    
    The OutlinerView was one of the last using XOR selections
    instead of the User-defined selection color on the resp.
    system. I changed that now.
    
    That View has no Overlay, it would have been possible to
    add it and then do TextEditOnOverlay and SelectionOnOverlay
    as in other situations. But there is nothing 'behind' the
    text, so I decided to stay at paint, but use Primitives.
    
    The TextContent (and 1st line Icons left) are now extracted
    from the EditEngine, using the mechanisms I added recently.
    That means e.g. as long as the user only scrolls, moves
    cursor or zooms, it is no longer necessary to re-create
    that text as Primitives. This is of course done when the
    text is changed (or attributes). Also not done on seleciton
    changes.
    
    Thus in most cases it no longer does: setup the EditEngine,
    set the text, format the text, layout the text and paint
    the text (again using 5 layers of VCL). The 1st part is
    now done on-demand when the text has changed, the 2nd part
    uses an SDPR if it exists instead of OutputDevice.
    
    The only change when visualization changes is to embed the
    text primitives to the StartPoint offset & embed to a
    clip primitive, and visualize transformed existing text.
    
    This makes it much faster - try a presentation with many
    slides with text and change to the EditView, scroll &
    zoom around, or change selection with keyboard/mouse.
    
    The selction uses a temporary OverlaySelection object
    to keep the creation of Primitives same and equal to
    other places.
    
    The OutlineView can have multiple OutlinerView's, but all
    of them - if used - are based on the same Outliner, so
    this gets not too complicated, only one seq<Primitives>
    is necessary.
    
    Change-Id: Ic9cea237b6d074f4fa18a0605ed80fabd0633178
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/189523
    Reviewed-by: Armin Le Grand <armin.le.gr...@me.com>
    Tested-by: Jenkins

diff --git a/editeng/source/editeng/impedit.cxx 
b/editeng/source/editeng/impedit.cxx
index 1b8853e409ca..dc1a7159a741 100644
--- a/editeng/source/editeng/impedit.cxx
+++ b/editeng/source/editeng/impedit.cxx
@@ -1643,9 +1643,14 @@ Pair ImpEditView::Scroll( tools::Long ndX, tools::Long 
ndY, ScrollRangeCheck nRa
             mpOutputWindow->Scroll( nRealDiffX, nRealDiffY, aRect, 
ScrollFlags::Clip );
         }
 
-        if (comphelper::LibreOfficeKit::isActive() || getEditViewCallbacks())
+        if (comphelper::LibreOfficeKit::isActive())
         {
             // Need to invalidate the window, otherwise no tile will be 
re-painted.
+            // NOTE:
+            // No invalidate in the sense of repaint needed, so not needed for
+            // all cases. Kepping it here so that for LibreOfficeKit this is 
still
+            // done, that may need a repaint. Just doing it will work, but do 
an
+            // extra-primitive extraction at the paint which is usually not 
needed
             GetEditViewPtr()->Invalidate();
         }
 
diff --git a/include/editeng/outliner.hxx b/include/editeng/outliner.hxx
index 80aee72f0070..34cf8d808fcf 100644
--- a/include/editeng/outliner.hxx
+++ b/include/editeng/outliner.hxx
@@ -901,6 +901,9 @@ public:
 
     // overridden in SdrOutliner
     SAL_DLLPRIVATE virtual std::optional<bool> 
GetCompatFlag(SdrCompatibilityFlag /*eFlag*/) const { return {}; };
+
+    // return FirstParaIsEmpty state
+    bool getFirstParaIsEmpty() const { return bFirstParaIsEmpty; }
 };
 
 #endif
diff --git a/sd/source/ui/inc/OutlineView.hxx b/sd/source/ui/inc/OutlineView.hxx
index fc4ef20f3440..e4e678b9568b 100644
--- a/sd/source/ui/inc/OutlineView.hxx
+++ b/sd/source/ui/inc/OutlineView.hxx
@@ -65,6 +65,18 @@ class OutlineView final
     : public SimpleOutlinerView
 {
     friend class OutlineViewModelChangeGuard;
+
+    // EditViewCallbacks
+    // NOTE: We are already derived from EditViewCallbacks in SdrObjEditView
+    virtual void EditViewInvalidate(const ::tools::Rectangle& rRect) override;
+    virtual void EditViewSelectionChange() override;
+    virtual OutputDevice& EditViewOutputDevice() const override;
+    virtual Point EditViewPointerPosPixel() const override;
+    virtual css::uno::Reference<css::datatransfer::clipboard::XClipboard> 
GetClipboard() const override;
+    virtual css::uno::Reference<css::datatransfer::dnd::XDropTarget> 
GetDropTarget() override;
+    virtual void EditViewInputContext(const InputContext& rInputContext) 
override;
+    virtual void EditViewCursorRect(const ::tools::Rectangle& rRect, int 
nExtTextInputWidth) override;
+
 public:
     OutlineView (DrawDocShell& rDocSh,
         vcl::Window* pWindow,
@@ -214,6 +226,12 @@ private:
 
     SvxLRSpaceItem maLRSpaceItem;
     Image maSlideImage;
+
+    // remember last selection rectangle vector
+    std::vector<::tools::Rectangle> maLastSelection;
+
+    // last current stripped portions
+    drawinglayer::primitive2d::Primitive2DContainer maTextContent;
 };
 
 // calls IgnoreCurrentPageChangesLevel with true in ctor and with false in dtor
diff --git a/sd/source/ui/view/outlview.cxx b/sd/source/ui/view/outlview.cxx
index e8c04ac74485..ca21dcf2001a 100644
--- a/sd/source/ui/view/outlview.cxx
+++ b/sd/source/ui/view/outlview.cxx
@@ -50,6 +50,15 @@
 #include <drawinglayer/primitive2d/textlayoutdevice.hxx>
 #include <drawinglayer/primitive2d/textprimitive2d.hxx>
 #include <vcl/metric.hxx>
+#include <svtools/optionsdrawinglayer.hxx>
+#include <svx/sdr/overlay/overlayselection.hxx>
+#include <drawinglayer/processor2d/processor2dtools.hxx>
+#include <drawinglayer/processor2d/baseprocessor2d.hxx>
+#include <vcl/dndlistenercontainer.hxx>
+#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
+#include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx>
+#include <drawinglayer/primitive2d/maskprimitive2d.hxx>
+#include <basegfx/polygon/b2dpolygontools.hxx>
 
 #include <DrawDocShell.hxx>
 #include <drawdoc.hxx>
@@ -77,6 +86,214 @@ namespace sd {
 // PROCESS_WITH_PROGRESS_THRESHOLD pages are concerned
 #define PROCESS_WITH_PROGRESS_THRESHOLD  5
 
+namespace {
+::tools::Rectangle lcl_negateRectX(const ::tools::Rectangle& rRect)
+{
+    return ::tools::Rectangle(-rRect.Right(), rRect.Top(), -rRect.Left(), 
rRect.Bottom());
+}
+
+void expandSelection(std::vector<::tools::Rectangle>& rVector, const 
::OutputDevice& rTarget)
+{
+    if (rVector.empty())
+        return;
+
+    const Size aLogicPixel(rTarget.PixelToLogic(Size(1, 1)));
+
+    for (auto& aRect : rVector)
+    {
+        aRect.SetLeft(aRect.Left() - aLogicPixel.Width());
+        aRect.SetTop(aRect.Top() - aLogicPixel.Height());
+        aRect.SetRight(aRect.Right() + aLogicPixel.Width());
+        aRect.SetBottom(aRect.Bottom() + aLogicPixel.Height());
+    }
+}
+}
+
+void OutlineView::Paint(const ::tools::Rectangle& rRect, ::sd::Window const * 
pWin)
+{
+    OutlinerView* pOlView(GetViewByWindow(pWin));
+
+    if (nullptr == pOlView)
+        // no view, no paint
+        return;
+
+    if (maTextContent.empty())
+    {
+        // For the first Paint/KeyInput/Drop an empty Outliner is turned into
+        // an Outliner with exactly one paragraph.
+        if(GetOutliner().getFirstParaIsEmpty())
+            GetOutliner().Insert(OUString());
+
+        // use TextHierarchyBreakupOutliner to get all geometry (text,
+        // Icons at LineStart for OutlinerView) embedded to the
+        // TextHierarchy.*Primitive2D groupings for better processing, plus
+        // the correct paragraph countings
+        TextHierarchyBreakupOutliner aHelper(GetOutliner());
+        GetOutliner().StripPortions(aHelper);
+        maTextContent = aHelper.getTextPortionPrimitives();
+    }
+
+    if (maTextContent.empty())
+        // no text, done
+        return;
+
+    // prepare paint
+    pOlView->HideCursor();
+
+    drawinglayer::geometry::ViewInformation2D aViewInformation2D;
+    ::OutputDevice* pTarget(pOlView->GetEditView().GetWindow()->GetOutDev());
+    aViewInformation2D.setViewTransformation(pTarget->GetViewTransformation());
+    std::unique_ptr<drawinglayer::processor2d::BaseProcessor2D> xProcessor(
+        drawinglayer::processor2d::createProcessor2DFromOutputDevice(*pTarget, 
aViewInformation2D));
+    drawinglayer::primitive2d::Primitive2DContainer aContent(maTextContent);
+
+    // get and apply StartPos
+    const Point 
aStartPos(pOlView->GetEditView().CalculateTextPaintStartPosition());
+    const bool bStartPos(0 != aStartPos.getX() || 0 != aStartPos.getY());
+
+    if (bStartPos)
+    {
+        // embed to StartPos translation
+        aContent = drawinglayer::primitive2d::Primitive2DContainer{
+            new drawinglayer::primitive2d::TransformPrimitive2D(
+                basegfx::utils::createTranslateB2DHomMatrix(aStartPos.X(), 
aStartPos.Y()),
+                std::move(aContent))};
+    }
+
+    if (!rRect.IsEmpty())
+    {
+        // clipping requested, embed to MaskPrimitive2D
+        aContent = drawinglayer::primitive2d::Primitive2DContainer{
+            new drawinglayer::primitive2d::MaskPrimitive2D(
+                basegfx::B2DPolyPolygon(basegfx::utils::createPolygonFromRect(
+                    basegfx::B2DRange(rRect.Left(), rRect.Top(), 
rRect.Right(), rRect.Bottom()))),
+                std::move(aContent))};
+    }
+
+    static bool bBlendForTest(false);
+    if(bBlendForTest)
+    {
+        aContent = drawinglayer::primitive2d::Primitive2DContainer{
+            new drawinglayer::primitive2d::ModifiedColorPrimitive2D(
+                std::move(aContent),
+                
std::make_shared<basegfx::BColorModifier_interpolate>(COL_YELLOW.getBColor(), 
0.5)) };
+    }
+
+    // render text
+    xProcessor->process(aContent);
+
+    // check for selection
+    pOlView->GetSelectionRectangles(maLastSelection);
+
+    if (!maLastSelection.empty())
+    {
+        // selection re-fetched, adapt extended state
+        expandSelection(maLastSelection, *pTarget);
+
+        // transfer to Ranges
+        std::vector<basegfx::B2DRange> aLogicRanges;
+        aLogicRanges.reserve(maLastSelection.size());
+        for (const auto& aRect : maLastSelection)
+            aLogicRanges.emplace_back(aRect.Left(), aRect.Top(), 
aRect.Right(), aRect.Bottom());
+
+        // get HilightColor & create temp OverlaySelection for primitive 
creation
+        const Color aHighlight(SvtOptionsDrawinglayer::getHilightColor());
+        const sdr::overlay::OverlaySelection aCursorOverlay(
+            sdr::overlay::OverlayType::Transparent, aHighlight, 
std::move(aLogicRanges), true);
+
+        // render selection
+        
xProcessor->process(aCursorOverlay.getOverlayObjectPrimitive2DSequence());
+    }
+
+    pOlView->ShowCursor(mbFirstPaint);
+    mbFirstPaint = false;
+}
+
+void OutlineView::EditViewInvalidate(const ::tools::Rectangle& rRect)
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    pActiveWin->Invalidate(IsNegativeX() ? lcl_negateRectX(rRect) : rRect);
+
+    // on all changes: flush TextContent to request re-creation at next paint
+    maTextContent.clear();
+}
+
+void OutlineView::EditViewSelectionChange()
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+
+    if (!maLastSelection.empty())
+    {
+        // invalidate last state
+        for (const auto& aRect : maLastSelection)
+        {
+            // invalidate full lines: Due to multiple strange callbacks to
+            // this method (MoveCursor sends in-between states when changing
+            // line up/down) it is necessary for correct text repaint to
+            // invalidate the whole line
+            ::tools::Rectangle aR(IsNegativeX() ? lcl_negateRectX(aRect) : 
aRect);
+            const ::tools::Rectangle& 
rFull(mpOutlinerViews[0]->GetOutputArea());
+            aR.SetLeft(rFull.Left());
+            aR.SetRight(rFull.Right());
+            pActiveWin->Invalidate(aR);
+        }
+    }
+
+    // update state
+    mpOutlinerViews[0]->GetSelectionRectangles(maLastSelection);
+
+    if (!maLastSelection.empty())
+    {
+        // extend ranges & invalidate new state
+        expandSelection(maLastSelection, *pActiveWin->GetOutDev());
+
+        for (const auto& aRect : maLastSelection)
+        {
+            ::tools::Rectangle aR(IsNegativeX() ? lcl_negateRectX(aRect) : 
aRect);
+            const ::tools::Rectangle& 
rFull(mpOutlinerViews[0]->GetOutputArea());
+            aR.SetLeft(rFull.Left());
+            aR.SetRight(rFull.Right());
+            pActiveWin->Invalidate(aR);
+        }
+    }
+}
+
+OutputDevice& OutlineView::EditViewOutputDevice() const
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    return *pActiveWin->GetOutDev();
+}
+
+Point OutlineView::EditViewPointerPosPixel() const
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    return pActiveWin->GetPointerPosPixel();
+}
+
+css::uno::Reference<css::datatransfer::clipboard::XClipboard> 
OutlineView::GetClipboard() const
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    return pActiveWin->GetClipboard();
+}
+
+css::uno::Reference<css::datatransfer::dnd::XDropTarget> 
OutlineView::GetDropTarget()
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    return pActiveWin->GetDropTarget();
+}
+
+void OutlineView::EditViewInputContext(const InputContext& rInputContext)
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    pActiveWin->SetInputContext(rInputContext);
+}
+
+void OutlineView::EditViewCursorRect(const ::tools::Rectangle& rRect, int 
nExtTextInputWidth)
+{
+    vcl::Window* pActiveWin(mpOutlinerViews[0]->GetEditView().GetWindow());
+    pActiveWin->SetCursorRect(&rRect, nExtTextInputWidth);
+}
+
 OutlineView::OutlineView(DrawDocShell& rDocSh, vcl::Window* pWindow,
                          OutlineViewShell& rOutlineViewShell)
     : ::sd::SimpleOutlinerView(*rDocSh.GetDoc(), pWindow->GetOutDev(), 
&rOutlineViewShell)
@@ -88,6 +305,9 @@ OutlineView::OutlineView(DrawDocShell& rDocSh, vcl::Window* 
pWindow,
     , maDocColor(COL_WHITE)
     , maLRSpaceItem(SvxIndentValue::twips(2000), SvxIndentValue::zero(), 
SvxIndentValue::zero(),
                     EE_PARA_OUTLLRSPACE)
+    , maSlideImage()
+    , maLastSelection()
+    , maTextContent()
 {
     bool bInitOutliner = false;
 
@@ -133,6 +353,9 @@ OutlineView::OutlineView(DrawDocShell& rDocSh, vcl::Window* 
pWindow,
     sd::UndoManager* pDocUndoMgr = 
dynamic_cast<sd::UndoManager*>(mpDocSh->GetUndoManager());
     if (pDocUndoMgr != nullptr)
         pDocUndoMgr->SetLinkedUndoManager(&mrOutliner.GetUndoManager());
+
+    // use EditViewCallbacks to allow showing non-XOR selection
+    mpOutlinerViews[0]->GetEditView().setEditViewCallbacks(this);
 }
 
 /**
@@ -154,6 +377,7 @@ OutlineView::~OutlineView()
     {
         if (rpView)
         {
+            rpView->GetEditView().setEditViewCallbacks(nullptr);
             mrOutliner.RemoveView( rpView.get() );
             rpView.reset();
         }
@@ -190,21 +414,6 @@ void OutlineView::DisconnectFromApplication()
     Application::RemoveEventListener(LINK(this, OutlineView, 
AppEventListenerHdl));
 }
 
-void OutlineView::Paint(const ::tools::Rectangle& rRect, ::sd::Window const * 
pWin)
-{
-    OutlinerView* pOlView = GetViewByWindow(pWin);
-
-    if (pOlView)
-    {
-        pOlView->HideCursor();
-        pOlView->DrawText_ToEditView(rRect);
-
-        pOlView->ShowCursor(mbFirstPaint);
-
-        mbFirstPaint = false;
-    }
-}
-
 void OutlineView::AddDeviceToPaintView(OutputDevice& rDev, vcl::Window* 
pWindow)
 {
     bool bAdded = false;

Reply via email to