From ac8fa2c59acfcd33bf990bf9e173b0f3b4d416a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caol=C3=A1n=20McNamara?= Date: Sat, 14 Feb 2026 16:49:52 +0000 Subject: [PATCH] Resolves: tdf#143449 implement a blinking cursor for weldeditview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Take a snapshot of what's under the cursor first and keep it, draw the cursor and take another snapshot. Then timer just calls an invalidate on the cursor area with the cursor reason for the invalidation, and just swap those snapshots on each draw while that's the only invalidation reason. Change-Id: I49a26af292905556cd6033f814aa8175c0da69a4 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199435 Tested-by: Jenkins Reviewed-by: Caolán McNamara --- include/svx/weldeditview.hxx | 10 ++++ svx/source/dialog/weldeditview.cxx | 84 +++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/include/svx/weldeditview.hxx b/include/svx/weldeditview.hxx index 0f6232e6f97e..219432196e58 100644 --- a/include/svx/weldeditview.hxx +++ b/include/svx/weldeditview.hxx @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include class WeldEditAccessible; @@ -55,6 +57,14 @@ protected: std::unique_ptr m_xEditView; rtl::Reference m_xAccessible; + // Cursor blink support + AutoTimer m_aCursorTimer; + bool m_bCursorVisible; + tools::Rectangle m_aCachedCursorPixRect; // pixel coords of cached cursor area + ScopedVclPtrInstance m_xCursorOnDev; + ScopedVclPtrInstance m_xCursorOffDev; + DECL_DLLPRIVATE_LINK(BlinkTimerHdl, Timer*, void); + virtual void makeEditEngine(); void InitAccessible(); diff --git a/svx/source/dialog/weldeditview.cxx b/svx/source/dialog/weldeditview.cxx index 085aa7b52aeb..b5bee3addfa8 100644 --- a/svx/source/dialog/weldeditview.cxx +++ b/svx/source/dialog/weldeditview.cxx @@ -99,7 +99,22 @@ void WeldEditView::Paste() WeldEditView::WeldEditView() : m_bAcceptsTab(false) + , m_aCursorTimer("WeldEditView CursorTimer") + , m_bCursorVisible(false) { + m_aCursorTimer.SetInvokeHandler(LINK(this, WeldEditView, BlinkTimerHdl)); +} + +IMPL_LINK_NOARG(WeldEditView, BlinkTimerHdl, Timer*, void) +{ + m_bCursorVisible = !m_bCursorVisible; + if (!m_aCachedCursorPixRect.IsEmpty()) + { + OutputDevice& rDevice = EditViewOutputDevice(); + Invalidate(rDevice.PixelToLogic(m_aCachedCursorPixRect), weld::InvalidateFlags::Cursor); + } + else + Invalidate(); } // tdf#127033 want to use UI font so override makeEditEngine to enable that @@ -228,6 +243,21 @@ void WeldEditView::DoPaint(vcl::RenderContext& rRenderContext, const tools::Rect return; } + // Fast path: blink-only repaint with valid cache + weld::InvalidateFlags ePending = GetInvalidateFlags(); + if (ePending == weld::InvalidateFlags::Cursor && !m_aCachedCursorPixRect.IsEmpty()) + { + VirtualDevice& rSrc = m_bCursorVisible ? *m_xCursorOnDev : *m_xCursorOffDev; + // Blit cached bitmap in pixel coordinates to avoid rounding issues + bool bMapMode = rRenderContext.IsMapModeEnabled(); + rRenderContext.EnableMapMode(false); + rRenderContext.DrawOutDev(m_aCachedCursorPixRect.TopLeft(), + m_aCachedCursorPixRect.GetSize(), Point(0, 0), + m_aCachedCursorPixRect.GetSize(), rSrc); + rRenderContext.EnableMapMode(bMapMode); + return; + } + auto popIt = rRenderContext.ScopedPush(vcl::PushFlags::ALL); rRenderContext.SetClipRegion(); @@ -239,7 +269,45 @@ void WeldEditView::DoPaint(vcl::RenderContext& rRenderContext, const tools::Rect { pEditView->ShowCursor(false); vcl::Cursor* pCursor = pEditView->GetCursor(); - pCursor->DrawToDevice(rRenderContext); + + // Get the pixel bounding rect the cursor will occupy + tools::Rectangle aPixRect = pCursor->GetBoundRect(rRenderContext); + if (!aPixRect.IsEmpty()) + { + m_aCachedCursorPixRect = aPixRect; + Size aPixSize = aPixRect.GetSize(); + + // Cache in pixel coordinates to avoid rounding issues + bool bMapMode = rRenderContext.IsMapModeEnabled(); + rRenderContext.EnableMapMode(false); + + // Cache "cursor off" — text without cursor (before drawing cursor) + m_xCursorOffDev->SetOutputSizePixel(aPixSize); + m_xCursorOffDev->SetMapMode(MapMode(MapUnit::MapPixel)); + m_xCursorOffDev->DrawOutDev(Point(0, 0), aPixSize, aPixRect.TopLeft(), aPixSize, + rRenderContext); + + // Draw cursor, then cache "cursor on" + rRenderContext.EnableMapMode(bMapMode); + pCursor->DrawToDevice(rRenderContext); + rRenderContext.EnableMapMode(false); + + m_xCursorOnDev->SetOutputSizePixel(aPixSize); + m_xCursorOnDev->SetMapMode(MapMode(MapUnit::MapPixel)); + m_xCursorOnDev->DrawOutDev(Point(0, 0), aPixSize, aPixRect.TopLeft(), aPixSize, + rRenderContext); + + rRenderContext.EnableMapMode(bMapMode); + + // If cursor should be hidden, restore to clean state + if (!m_bCursorVisible) + { + rRenderContext.EnableMapMode(false); + rRenderContext.DrawOutDev(aPixRect.TopLeft(), aPixSize, Point(0, 0), aPixSize, + *m_xCursorOffDev); + rRenderContext.EnableMapMode(bMapMode); + } + } } // get logic selection @@ -1573,6 +1641,16 @@ void WeldEditView::GetFocus() if (pEditView) { pEditView->ShowCursor(false); + + m_bCursorVisible = true; + m_aCachedCursorPixRect = tools::Rectangle(); + sal_uInt64 nBlinkTime = Application::GetSettings().GetStyleSettings().GetCursorBlinkTime(); + if (nBlinkTime != STYLE_CURSOR_NOBLINKTIME) + { + m_aCursorTimer.SetTimeout(nBlinkTime); + m_aCursorTimer.Start(); + } + Invalidate(); // redraw with cursor } @@ -1591,6 +1669,10 @@ void WeldEditView::GetFocus() void WeldEditView::LoseFocus() { + m_aCursorTimer.Stop(); + m_bCursorVisible = false; + m_aCachedCursorPixRect = tools::Rectangle(); + weld::CustomWidgetController::LoseFocus(); Invalidate(); // redraw without cursor