/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This file is part of the LibreOffice project.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This file incorporates work covered by the following license notice:
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright
* ownership. The ASF licenses this file to you under the Apache
* License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
*/
#include <UndoManager.hxx>
#include <libxml/xmlwriter.h>
#include <doc.hxx>
#include <docsh.hxx>
#include <utility>
#include <view.hxx>
#include <drawdoc.hxx>
#include <ndarr.hxx>
#include <pam.hxx>
#include <swundo.hxx>
#include <UndoCore.hxx>
#include <wrtsh.hxx>
#include <editsh.hxx>
#include <unobaseclass.hxx>
#include <IDocumentDrawModelAccess.hxx>
#include <IDocumentRedlineAccess.hxx>
#include <IDocumentState.hxx>
#include <comphelper/lok.hxx>
#include <assert.h>
#include <sfx2/viewfrm.hxx>
#include <sfx2/bindings.hxx>
#include <osl/diagnose.h>
#include <o3tl/temporary.hxx>
#include <UndoInsert.hxx>
using namespace ::com::sun::star;
// the undo array should never grow beyond this limit:
#define UNDO_ACTION_LIMIT (USHRT_MAX - 1000)
namespace sw {
UndoManager::UndoManager(std::shared_ptr<SwNodes> xUndoNodes,
IDocumentDrawModelAccess & rDrawModelAccess,
IDocumentRedlineAccess & rRedlineAccess,
IDocumentState & rState)
: m_rDrawModelAccess(rDrawModelAccess)
, m_rRedlineAccess(rRedlineAccess)
, m_rState(rState)
, m_xUndoNodes(std::move(xUndoNodes))
, m_bGroupUndo(true)
, m_bDrawUndo(true)
, m_bRepair(false)
, m_bLockUndoNoModifiedPosition(false)
, m_isAddWithIgnoreRepeat(false)
, m_UndoSaveMark(MARK_INVALID)
, m_pDocShell(nullptr)
, m_pView(nullptr)
{
assert(bool(m_xUndoNodes));
// writer expects it to be disabled initially
// Undo is enabled by SwEditShell constructor
SdrUndoManager::EnableUndo(false);
}
SwNodes const& UndoManager::GetUndoNodes() const
{
return *m_xUndoNodes;
}
SwNodes & UndoManager::GetUndoNodes()
{
return *m_xUndoNodes;
}
bool UndoManager::IsUndoNodes(SwNodes const& rNodes) const
{
return & rNodes == m_xUndoNodes.get();
}
void UndoManager::SetDocShell(SwDocShell* pDocShell)
{
m_pDocShell = pDocShell;
}
void UndoManager::SetView(SwView* pView)
{
m_pView = pView;
}
size_t UndoManager::GetUndoActionCount(const bool bCurrentLevel) const
{
size_t nRet = SdrUndoManager::GetUndoActionCount(bCurrentLevel);
if (!comphelper::LibreOfficeKit::isActive() || !m_pView)
return nRet;
if (!nRet || !SdrUndoManager::GetUndoActionCount())
return nRet;
const SfxUndoAction* pAction = SdrUndoManager::GetUndoAction();
if (!pAction)
return nRet;
if (!m_bRepair)
{
// If another view created the last undo action, prevent undoing it from this view.
ViewShellId nViewShellId = m_pView->GetViewShellId();
if (pAction->GetViewShellId() != nViewShellId)
nRet = 0;
}
return nRet;
}
size_t UndoManager::GetRedoActionCount(const bool bCurrentLevel) const
{
size_t nRet = SdrUndoManager::GetRedoActionCount(bCurrentLevel);
if (!comphelper::LibreOfficeKit::isActive() || !m_pView)
return nRet;
if (!nRet || !SdrUndoManager::GetRedoActionCount())
return nRet;
const SfxUndoAction* pAction = SdrUndoManager::GetRedoAction();
if (!pAction)
return nRet;
if (!m_bRepair)
{
// If another view created the first redo action, prevent redoing it from this view.
ViewShellId nViewShellId = m_pView->GetViewShellId();
if (pAction->GetViewShellId() != nViewShellId)
nRet = 0;
}
return nRet;
}
void UndoManager::DoUndo(bool const bDoUndo)
{
if(!isTextEditActive())
{
EnableUndo(bDoUndo);
SwDrawModel*const pSdrModel = m_rDrawModelAccess.GetDrawModel();
if( pSdrModel )
{
pSdrModel->EnableUndo(bDoUndo);
}
}
}
bool UndoManager::DoesUndo() const
{
if(isTextEditActive())
{
return false;
}
else
{
return IsUndoEnabled();
}
}
void UndoManager::DoGroupUndo(bool const bDoUndo)
{
m_bGroupUndo = bDoUndo;
}
bool UndoManager::DoesGroupUndo() const
{
return m_bGroupUndo;
}
void UndoManager::DoDrawUndo(bool const bDoUndo)
{
m_bDrawUndo = bDoUndo;
}
bool UndoManager::DoesDrawUndo() const
{
return m_bDrawUndo;
}
void UndoManager::DoRepair(bool bRepair)
{
m_bRepair = bRepair;
}
bool UndoManager::DoesRepair() const
{
return m_bRepair;
}
bool UndoManager::IsUndoNoResetModified() const
{
return MARK_INVALID == m_UndoSaveMark;
}
void UndoManager::SetUndoNoResetModified()
{
if (MARK_INVALID != m_UndoSaveMark)
{
RemoveMark(m_UndoSaveMark);
m_UndoSaveMark = MARK_INVALID;
}
}
void UndoManager::SetUndoNoModifiedPosition()
{
if (!m_bLockUndoNoModifiedPosition)
{
m_UndoSaveMark = MarkTopUndoAction();
}
}
void UndoManager::LockUndoNoModifiedPosition()
{
m_bLockUndoNoModifiedPosition = true;
}
void UndoManager::UnLockUndoNoModifiedPosition()
{
m_bLockUndoNoModifiedPosition = false;
}
SwUndo* UndoManager::GetLastUndo()
{
if (!SdrUndoManager::GetUndoActionCount())
{
return nullptr;
}
SfxUndoAction *const pAction( SdrUndoManager::GetUndoAction() );
return dynamic_cast<SwUndo*>(pAction);
}
void UndoManager::AppendUndo(std::unique_ptr<SwUndo> pUndo)
{
AddUndoAction(std::move(pUndo));
}
void UndoManager::ClearRedo()
{
return SdrUndoManager::ImplClearRedo_NoLock(TopLevel);
}
void UndoManager::DelAllUndoObj()
{
::sw::UndoGuard const undoGuard(*this);
SdrUndoManager::ClearAllLevels();
m_UndoSaveMark = MARK_INVALID;
}
SwUndoId
UndoManager::StartUndo(SwUndoId const i_eUndoId,
SwRewriter const*const pRewriter)
{
if (!IsUndoEnabled())
{
return SwUndoId::EMPTY;
}
SwUndoId const eUndoId( (i_eUndoId == SwUndoId::EMPTY) ? SwUndoId::START : i_eUndoId );
assert(SwUndoId::END != eUndoId);
OUString comment( (SwUndoId::START == eUndoId)
? u"??"_ustr
: GetUndoComment(eUndoId) );
if (pRewriter)
{
assert(SwUndoId::START != eUndoId);
comment = pRewriter->Apply(comment);
}
ViewShellId nViewShellId(-1);
if (m_pDocShell)
{
if (const SwView* pView = m_pDocShell->GetView())
nViewShellId = pView->GetViewShellId();
}
SdrUndoManager::EnterListAction(comment, comment, static_cast<sal_uInt16>(eUndoId), nViewShellId);
return eUndoId;
}
SwUndoId
UndoManager::EndUndo(SwUndoId eUndoId, SwRewriter const*const pRewriter)
{
if (!IsUndoEnabled())
{
return SwUndoId::EMPTY;
}
if ((eUndoId == SwUndoId::EMPTY) || (SwUndoId::START == eUndoId))
eUndoId = SwUndoId::END;
OSL_ENSURE(!((SwUndoId::END == eUndoId) && pRewriter),
"EndUndo(): no Undo ID, but rewriter given?");
SfxUndoAction *const pLastUndo(
(0 == SdrUndoManager::GetUndoActionCount())
? nullptr : SdrUndoManager::GetUndoAction() );
int const nCount = LeaveListAction();
if (nCount) // otherwise: empty list action not inserted!
{
assert(pLastUndo);
assert(SwUndoId::START != eUndoId);
auto pListAction = dynamic_cast<SfxListUndoAction*>(SdrUndoManager::GetUndoAction());
assert(pListAction);
if (SwUndoId::END != eUndoId)
{
OSL_ENSURE(static_cast<SwUndoId>(pListAction->GetId()) == eUndoId,
"EndUndo(): given ID different from StartUndo()");
// comment set by caller of EndUndo
OUString comment = GetUndoComment(eUndoId);
if (pRewriter)
{
comment = pRewriter->Apply(comment);
}
pListAction->SetComment(comment);
}
else if (SwUndoId::START != static_cast<SwUndoId>(pListAction->GetId()))
{
// comment set by caller of StartUndo: nothing to do here
}
else if (pLastUndo)
{
// comment was not set at StartUndo or EndUndo:
// take comment of last contained action
// (note that this works recursively, i.e. the last contained
// action may be a list action created by StartUndo/EndUndo)
OUString const comment(pLastUndo->GetComment());
pListAction->SetComment(comment);
}
else
{
OSL_ENSURE(false, "EndUndo(): no comment?");
}
}
return eUndoId;
}
/**
* Checks if the topmost undo action owned by pView is independent from the topmost action undo
* action.
*/
bool UndoManager::IsViewUndoActionIndependent(const SwView* pView, sal_uInt16& rOffset) const
{
if (GetUndoActionCount() <= 1)
{
// Single or less undo, owned by another view.
return false;
}
if (!pView)
{
return false;
}
// Last undo action that doesn't belong to the view.
const SfxUndoAction* pTopAction = GetUndoAction();
ViewShellId nViewId = pView->GetViewShellId();
// Earlier undo action that belongs to the view, but is not the top one.
const SfxUndoAction* pViewAction = nullptr;
size_t nOffset = 0;
for (size_t i = 0; i < GetUndoActionCount(); ++i)
{
const SfxUndoAction* pAction = GetUndoAction(i);
if (pAction->GetViewShellId() == nViewId)
{
pViewAction = pAction;
nOffset = i;
break;
}
}
if (!pViewAction)
{
// Found no earlier undo action that belongs to the view.
return false;
}
auto pTopSwAction = dynamic_cast<const SwUndo*>(pTopAction);
if (!pTopSwAction || pTopSwAction->GetId() != SwUndoId::TYPING)
{
return false;
}
auto pViewSwAction = dynamic_cast<const SwUndo*>(pViewAction);
if (!pViewSwAction || pViewSwAction->GetId() != SwUndoId::TYPING)
{
return false;
}
const auto& rTopInsert = *static_cast<const SwUndoInsert*>(pTopSwAction);
const auto& rViewInsert = *static_cast<const SwUndoInsert*>(pViewSwAction);
for (size_t i = 0; i < GetRedoActionCount(); ++i)
{
auto pRedoAction = dynamic_cast<const SwUndo*>(GetRedoAction(i));
if (!pRedoAction || pRedoAction->GetId() != SwUndoId::TYPING)
{
return false;
}
const auto& rRedoInsert = *static_cast<const SwUndoInsert*>(pRedoAction);
if (!rViewInsert.IsIndependent(rRedoInsert) && rRedoInsert.GetViewShellId() != nViewId)
{
// Dependent redo action and owned by another view.
return false;
}
}
if (!rViewInsert.IsIndependent(rTopInsert))
{
return false;
}
rOffset = nOffset;
return true;
}
bool
UndoManager::GetLastUndoInfo(
OUString *const o_pStr, SwUndoId *const o_pId, const SwView* pView) const
{
// this is actually expected to work on the current level,
// but that was really not obvious from the previous implementation...
if (!SdrUndoManager::GetUndoActionCount())
{
return false;
}
SfxUndoAction *const pAction( SdrUndoManager::GetUndoAction() );
if (comphelper::LibreOfficeKit::isActive() && !m_bRepair)
{
// If another view created the undo action, prevent undoing it from this view.
ViewShellId nViewShellId = pView ? pView->GetViewShellId() : m_pDocShell->GetView()->GetViewShellId();
// Unless we know that the other view's undo action is independent from us.
if (pAction->GetViewShellId() != nViewShellId
&& !IsViewUndoActionIndependent(pView, o3tl::temporary(sal_uInt16())))
{
if (o_pId)
{
*o_pId = SwUndoId::CONFLICT;
}
return false;
}
}
if (o_pStr)
{
*o_pStr = pAction->GetComment();
}
if (o_pId)
{
if (auto pListAction = dynamic_cast<const SfxListUndoAction*>(pAction))
*o_pId = static_cast<SwUndoId>(pListAction->GetId());
else if (auto pSwAction = dynamic_cast<const SwUndo*>(pAction))
*o_pId = pSwAction->GetId();
else
*o_pId = SwUndoId::EMPTY;
}
return true;
}
SwUndoComments_t UndoManager::GetUndoComments() const
{
OSL_ENSURE(!SdrUndoManager::IsInListAction(),
"GetUndoComments() called while in list action?");
SwUndoComments_t ret;
const size_t nUndoCount(SdrUndoManager::GetUndoActionCount(TopLevel));
for (size_t n = 0; n < nUndoCount; ++n)
{
OUString const comment(
SdrUndoManager::GetUndoActionComment(n, TopLevel));
ret.push_back(comment);
}
return ret;
}
bool UndoManager::GetFirstRedoInfo(OUString *const o_pStr,
SwUndoId *const o_pId,
const SwView* pView) const
{
if (!SdrUndoManager::GetRedoActionCount())
{
return false;
}
SfxUndoAction *const pAction( SdrUndoManager::GetRedoAction() );
if ( pAction == nullptr )
{
return false;
}
if (comphelper::LibreOfficeKit::isActive() && !m_bRepair)
{
// If another view created the undo action, prevent redoing it from this view.
ViewShellId nViewShellId = pView ? pView->GetViewShellId() : m_pDocShell->GetView()->GetViewShellId();
if (pAction->GetViewShellId() != nViewShellId)
{
if (o_pId)
{
*o_pId = SwUndoId::CONFLICT;
}
return false;
}
}
if (o_pStr)
{
*o_pStr = pAction->GetComment();
}
if (o_pId)
{
if (auto pListAction = dynamic_cast<const SfxListUndoAction*>(pAction))
*o_pId = static_cast<SwUndoId>(pListAction->GetId());
else if (auto pSwAction = dynamic_cast<const SwUndo*>(pAction))
*o_pId = pSwAction->GetId();
else
*o_pId = SwUndoId::EMPTY;
}
return true;
}
SwUndoComments_t UndoManager::GetRedoComments() const
{
OSL_ENSURE(!SdrUndoManager::IsInListAction(),
"GetRedoComments() called while in list action?");
SwUndoComments_t ret;
const size_t nRedoCount(SdrUndoManager::GetRedoActionCount(TopLevel));
for (size_t n = 0; n < nRedoCount; ++n)
{
OUString const comment(
SdrUndoManager::GetRedoActionComment(n, TopLevel));
ret.push_back(comment);
}
return ret;
}
SwUndoId UndoManager::GetRepeatInfo(OUString *const o_pStr) const
{
SwUndoId nRepeatId(SwUndoId::EMPTY);
GetLastUndoInfo(o_pStr, & nRepeatId);
if( SwUndoId::REPEAT_START <= nRepeatId && SwUndoId::REPEAT_END > nRepeatId )
{
return nRepeatId;
}
if (o_pStr) // not repeatable -> clear comment
{
o_pStr->clear();
}
return SwUndoId::EMPTY;
}
SwUndo * UndoManager::RemoveLastUndo()
{
if (SdrUndoManager::GetRedoActionCount() ||
SdrUndoManager::GetRedoActionCount(TopLevel))
{
OSL_ENSURE(false, "RemoveLastUndoAction(): there are Redo actions?");
return nullptr;
}
if (!SdrUndoManager::GetUndoActionCount())
{
OSL_ENSURE(false, "RemoveLastUndoAction(): no Undo actions");
return nullptr;
}
SfxUndoAction *const pLastUndo(GetUndoAction());
SdrUndoManager::RemoveLastUndoAction();
return dynamic_cast<SwUndo *>(pLastUndo);
}
// SfxUndoManager
void UndoManager::AddUndoAction(std::unique_ptr<SfxUndoAction> pAction, bool bTryMerge)
{
SwUndo *const pUndo( dynamic_cast<SwUndo *>(pAction.get()) );
if (pUndo)
{
if (RedlineFlags::NONE == pUndo->GetRedlineFlags())
{
pUndo->SetRedlineFlags( m_rRedlineAccess.GetRedlineFlags() );
}
if (m_isAddWithIgnoreRepeat)
{
pUndo->IgnoreRepeat();
}
}
SdrUndoManager::AddUndoAction(std::move(pAction), bTryMerge);
if (m_pDocShell)
{
SfxViewFrame* pViewFrame = SfxViewFrame::GetFirst( m_pDocShell );
while ( pViewFrame )
{
pViewFrame->GetBindings().Invalidate( SID_UNDO );
pViewFrame->GetBindings().Invalidate( SID_REDO );
pViewFrame = SfxViewFrame::GetNext( *pViewFrame, m_pDocShell );
}
}
// if the undo nodes array is too large, delete some actions
while (UNDO_ACTION_LIMIT < sal_Int32(GetUndoNodes().Count()))
{
RemoveOldestUndoAction();
}
}
namespace {
class CursorGuard
{
public:
CursorGuard(SwEditShell & rShell, bool const bSave)
: m_rShell(rShell)
, m_bSaveCursor(bSave)
{
if (m_bSaveCursor)
{
m_rShell.Push(); // prevent modification of current cursor
}
}
~CursorGuard() COVERITY_NOEXCEPT_FALSE
{
if (m_bSaveCursor)
{
m_rShell.Pop(SwCursorShell::PopMode::DeleteCurrent);
}
}
private:
SwEditShell & m_rShell;
bool const m_bSaveCursor;
};
}
bool UndoManager::impl_DoUndoRedo(UndoOrRedoType undoOrRedo, size_t nUndoOffset)
{
SwDoc & rDoc(GetUndoNodes().GetDoc());
UnoActionContext c(& rDoc); // exception-safe StartAllAction/EndAllAction
SwView* pViewShell = dynamic_cast<SwView*>(SfxViewShell::Current());
SwEditShell *const pEditShell(
comphelper::LibreOfficeKit::isActive() && pViewShell ? pViewShell->GetWrtShellPtr()
: rDoc.GetEditShell());
OSL_ENSURE(pEditShell, "sw::UndoManager needs a SwEditShell!");
if (!pEditShell)
{
throw uno::RuntimeException();
}
// in case the model has controllers locked, the Undo should not
// change the view cursors!
bool const bSaveCursors(pEditShell->CursorsLocked());
CursorGuard aCursorGuard(*pEditShell, bSaveCursors);
if (!bSaveCursors)
{
// (in case Undo was called via API) clear the cursors:
pEditShell->KillPams();
pEditShell->SetMark();
pEditShell->ClearMark();
}
bool bRet(false);
::sw::UndoRedoContext context(rDoc, *pEditShell);
context.SetUndoOffset(nUndoOffset);
// N.B. these may throw!
if (UndoOrRedoType::Undo == undoOrRedo)
{
bRet = SdrUndoManager::UndoWithContext(context);
}
else
{
bRet = SdrUndoManager::RedoWithContext(context);
}
if (bRet)
{
// if we are at the "last save" position, the document is not modified
if (SdrUndoManager::HasTopUndoActionMark(m_UndoSaveMark))
{
m_rState.ResetModified();
}
else
{
m_rState.SetModified();
}
}
pEditShell->HandleUndoRedoContext(context);
return bRet;
}
bool UndoManager::Undo() { return UndoWithOffset(0); }
bool UndoManager::UndoWithOffset(size_t nUndoOffset)
{
if(isTextEditActive())
{
return SdrUndoManager::Undo();
}
else
{
return impl_DoUndoRedo(UndoOrRedoType::Undo, nUndoOffset);
}
}
bool UndoManager::Redo()
{
if(isTextEditActive())
{
return SdrUndoManager::Redo();
}
else
{
return impl_DoUndoRedo(UndoOrRedoType::Redo, /*nUndoOffset=*/0);
}
}
void UndoManager::dumpAsXml(xmlTextWriterPtr pWriter) const
{
(void)xmlTextWriterStartElement(pWriter, BAD_CAST("swUndoManager"));
SdrUndoManager::dumpAsXml(pWriter);
(void)xmlTextWriterStartElement(pWriter, BAD_CAST("m_xUndoNodes"));
m_xUndoNodes->dumpAsXml(pWriter);
(void)xmlTextWriterEndElement(pWriter);
(void)xmlTextWriterEndElement(pWriter);
}
void UndoManager::EmptyActionsChanged()
{
if (m_pDocShell)
{
m_pDocShell->Broadcast(SfxHint(SfxHintId::DocumentRepair));
}
}
/** N.B.: this does _not_ call SdrUndoManager::Repeat because it is not
possible to wrap a list action around it:
calling EnterListAction here will cause SdrUndoManager::Repeat
to repeat the list action!
*/
bool UndoManager::Repeat(::sw::RepeatContext & rContext,
sal_uInt16 const nRepeatCount)
{
if (SdrUndoManager::IsInListAction())
{
OSL_ENSURE(false, "repeat in open list action???");
return false;
}
if (!SdrUndoManager::GetUndoActionCount(TopLevel))
{
return false;
}
SfxUndoAction *const pRepeatAction(GetUndoAction());
assert(pRepeatAction);
if (!pRepeatAction->CanRepeat(rContext))
{
return false;
}
OUString const comment(pRepeatAction->GetComment());
OUString const rcomment(pRepeatAction->GetRepeatComment(rContext));
SwUndoId nId;
if (auto const* const pSwAction = dynamic_cast<SwUndo*>(pRepeatAction))
nId = pSwAction->GetId();
else if (auto const* const pListAction = dynamic_cast<SfxListUndoAction*>(pRepeatAction))
nId = static_cast<SwUndoId>(pListAction->GetId());
else
return false;
if (DoesUndo())
{
ViewShellId nViewShellId(-1);
if (m_pDocShell)
{
if (const SwView* pView = m_pDocShell->GetView())
nViewShellId = pView->GetViewShellId();
}
EnterListAction(comment, rcomment, static_cast<sal_uInt16>(nId), nViewShellId);
}
SwPaM* pTmp = rContext.m_pCurrentPaM;
for(SwPaM& rPaM : rContext.GetRepeatPaM().GetRingContainer())
{ // iterate over ring
rContext.m_pCurrentPaM = &rPaM;
if (DoesUndo() && & rPaM != pTmp)
{
m_isAddWithIgnoreRepeat = true;
}
for (sal_uInt16 nRptCnt = nRepeatCount; nRptCnt > 0; --nRptCnt)
{
pRepeatAction->Repeat(rContext);
}
if (DoesUndo() && & rPaM != pTmp)
{
m_isAddWithIgnoreRepeat = false;
}
rContext.m_bDeleteRepeated = false; // reset for next PaM
}
rContext.m_pCurrentPaM = pTmp;
if (DoesUndo())
{
LeaveListAction();
}
return true;
}
} // namespace sw
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
↑ V522 There might be dereferencing of a potential null pointer 'pListAction'.