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;