From 651e8326ca65a7ed8616db1efc466d17e71baf28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20A=C3=9Fmus?= Date: Sat, 7 Feb 2015 23:12:02 +0100 Subject: [PATCH] HaikuDepot: Base-classes for generic undo/redo support. I plan to use these in the text view and editor framework. --- src/apps/haikudepot/Jamfile | 9 +- .../haikudepot/edits_generic/CompoundEdit.cpp | 117 +++++++++ .../haikudepot/edits_generic/CompoundEdit.h | 36 +++ .../haikudepot/edits_generic/EditContext.cpp | 18 ++ .../haikudepot/edits_generic/EditContext.h | 26 ++ .../haikudepot/edits_generic/EditManager.cpp | 244 ++++++++++++++++++ .../haikudepot/edits_generic/EditManager.h | 60 +++++ .../haikudepot/edits_generic/EditStack.cpp | 49 ++++ src/apps/haikudepot/edits_generic/EditStack.h | 30 +++ .../haikudepot/edits_generic/UndoableEdit.cpp | 86 ++++++ .../haikudepot/edits_generic/UndoableEdit.h | 41 +++ 11 files changed, 715 insertions(+), 1 deletion(-) create mode 100644 src/apps/haikudepot/edits_generic/CompoundEdit.cpp create mode 100644 src/apps/haikudepot/edits_generic/CompoundEdit.h create mode 100644 src/apps/haikudepot/edits_generic/EditContext.cpp create mode 100644 src/apps/haikudepot/edits_generic/EditContext.h create mode 100644 src/apps/haikudepot/edits_generic/EditManager.cpp create mode 100644 src/apps/haikudepot/edits_generic/EditManager.h create mode 100644 src/apps/haikudepot/edits_generic/EditStack.cpp create mode 100644 src/apps/haikudepot/edits_generic/EditStack.h create mode 100644 src/apps/haikudepot/edits_generic/UndoableEdit.cpp create mode 100644 src/apps/haikudepot/edits_generic/UndoableEdit.h diff --git a/src/apps/haikudepot/Jamfile b/src/apps/haikudepot/Jamfile index 2d6a5b3077..0a38be23f6 100644 --- a/src/apps/haikudepot/Jamfile +++ b/src/apps/haikudepot/Jamfile @@ -4,7 +4,7 @@ UsePrivateHeaders interface shared package ; # source directories local sourceDirs = - model textview ui ui_generic + edits_generic model textview ui ui_generic ; local sourceDir ; @@ -15,6 +15,13 @@ for sourceDir in $(sourceDirs) { SEARCH_SOURCE += [ FDirName $(HAIKU_TOP) src servers package ] ; local textDocumentSources = + # edits_generic + CompoundEdit.cpp + EditContext.cpp + EditManager.cpp + EditStack.cpp + UndoableEdit.cpp + # textview Bullet.cpp BulletData.cpp CharacterStyle.cpp diff --git a/src/apps/haikudepot/edits_generic/CompoundEdit.cpp b/src/apps/haikudepot/edits_generic/CompoundEdit.cpp new file mode 100644 index 0000000000..7c2c405c4b --- /dev/null +++ b/src/apps/haikudepot/edits_generic/CompoundEdit.cpp @@ -0,0 +1,117 @@ +/* + * Copyright 2006-2112, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#include "CompoundEdit.h" + +#include + + +CompoundEdit::CompoundEdit(const char* name) + : UndoableEdit() + , fEdits() + , fName(name) +{ +} + + +CompoundEdit::~CompoundEdit() +{ +} + + +status_t +CompoundEdit::InitCheck() +{ + return B_OK; +} + + +status_t +CompoundEdit::Perform(EditContext& context) +{ + status_t status = B_OK; + + int32 count = fEdits.CountItems(); + int32 i = 0; + for (; i < count; i++) { + status = fEdits.ItemAtFast(i)->Perform(context); + if (status != B_OK) + break; + } + + if (status != B_OK) { + // roll back + i--; + for (; i >= 0; i--) { + fEdits.ItemAtFast(i)->Undo(context); + } + } + + return status; +} + + +status_t +CompoundEdit::Undo(EditContext& context) +{ + status_t status = B_OK; + + int32 count = fEdits.CountItems(); + int32 i = count - 1; + for (; i >= 0; i--) { + status = fEdits.ItemAtFast(i)->Undo(context); + if (status != B_OK) + break; + } + + if (status != B_OK) { + // roll back + i++; + for (; i < count; i++) { + fEdits.ItemAtFast(i)->Redo(context); + } + } + + return status; +} + + +status_t +CompoundEdit::Redo(EditContext& context) +{ + status_t status = B_OK; + + int32 count = fEdits.CountItems(); + int32 i = 0; + for (; i < count; i++) { + status = fEdits.ItemAtFast(i)->Redo(context); + if (status != B_OK) + break; + } + + if (status != B_OK) { + // roll back + i--; + for (; i >= 0; i--) { + fEdits.ItemAtFast(i)->Undo(context); + } + } + + return status; +} + + +void +CompoundEdit::GetName(BString& name) +{ + name << fName; +} + + +bool +CompoundEdit::AppendEdit(const UndoableEditRef& edit) +{ + return fEdits.Add(edit); +} diff --git a/src/apps/haikudepot/edits_generic/CompoundEdit.h b/src/apps/haikudepot/edits_generic/CompoundEdit.h new file mode 100644 index 0000000000..3edf639b06 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/CompoundEdit.h @@ -0,0 +1,36 @@ +/* + * Copyright 2006-2112, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#ifndef COMPOUND_EDIT_H +#define COMPOUND_EDIT_H + +#include + +#include "List.h" +#include "UndoableEdit.h" + +class CompoundEdit : public UndoableEdit { +public: + CompoundEdit(const char* name); + virtual ~CompoundEdit(); + + virtual status_t InitCheck(); + + virtual status_t Perform(EditContext& context); + virtual status_t Undo(EditContext& context); + virtual status_t Redo(EditContext& context); + + virtual void GetName(BString& name); + + bool AppendEdit(const UndoableEditRef& edit); + +private: + typedef List EditList; + + EditList fEdits; + BString fName; +}; + +#endif // COMPOUND_EDIT_H diff --git a/src/apps/haikudepot/edits_generic/EditContext.cpp b/src/apps/haikudepot/edits_generic/EditContext.cpp new file mode 100644 index 0000000000..afb956e1ee --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditContext.cpp @@ -0,0 +1,18 @@ +/* + * Copyright 2013 Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#include "EditContext.h" + +#include + + +EditContext::EditContext() +{ +} + + +EditContext::~EditContext() +{ +} diff --git a/src/apps/haikudepot/edits_generic/EditContext.h b/src/apps/haikudepot/edits_generic/EditContext.h new file mode 100644 index 0000000000..6f30813109 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditContext.h @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#ifndef EDIT_CONTEXT_H +#define EDIT_CONTEXT_H + +#include + +/** + * EditContext is passed to UndoableEdits in Perform(), Undo(), and Redo(). + * It provides a context in which the user performs these operations + * and can be used for example to control the selection or visible + * portion of the document to focus the user's attention on the + * elements affected by the UndoableEdit. + */ +class EditContext : public BReferenceable { +public: + EditContext(); + virtual ~EditContext(); +}; + +typedef BReference EditContextRef; + +#endif // EDIT_CONTEXT_H diff --git a/src/apps/haikudepot/edits_generic/EditManager.cpp b/src/apps/haikudepot/edits_generic/EditManager.cpp new file mode 100644 index 0000000000..b91f1954f0 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditManager.cpp @@ -0,0 +1,244 @@ +/* + * Copyright 2006-2012, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#include "EditManager.h" + +#include +#include + +#include +#include + + +EditManager::Listener::~Listener() +{ +} + + +EditManager::EditManager() +{ +} + + +EditManager::~EditManager() +{ + Clear(); +} + + +status_t +EditManager::Perform(UndoableEdit* edit, EditContext& context) +{ + if (edit == NULL) + return B_BAD_VALUE; + + return Perform(UndoableEditRef(edit, true), context); +} + + +status_t +EditManager::Perform(const UndoableEditRef& edit, EditContext& context) +{ + status_t ret = edit.Get() != NULL ? B_OK : B_BAD_VALUE; + if (ret == B_OK) + ret = edit->InitCheck(); + + if (ret == B_OK) + ret = edit->Perform(context); + + if (ret == B_OK) { + ret = _AddEdit(edit); + if (ret != B_OK) + edit->Undo(context); + } + + _NotifyListeners(); + + return ret; +} + + +status_t +EditManager::Undo(EditContext& context) +{ + status_t status = B_ERROR; + if (!fUndoHistory.IsEmpty()) { + UndoableEditRef edit(fUndoHistory.Top()); + fUndoHistory.Pop(); + status = edit->Undo(context); + if (status == B_OK) + fRedoHistory.Push(edit); + else + fUndoHistory.Push(edit); + } + + _NotifyListeners(); + + return status; +} + + +status_t +EditManager::Redo(EditContext& context) +{ + status_t status = B_ERROR; + if (!fRedoHistory.IsEmpty()) { + UndoableEditRef edit(fRedoHistory.Top()); + fRedoHistory.Pop(); + status = edit->Redo(context); + if (status == B_OK) + fUndoHistory.Push(edit); + else + fRedoHistory.Push(edit); + } + + _NotifyListeners(); + + return status; +} + + +bool +EditManager::GetUndoName(BString& name) +{ + if (!fUndoHistory.IsEmpty()) { + name << " "; + fUndoHistory.Top()->GetName(name); + return true; + } + return false; +} + + +bool +EditManager::GetRedoName(BString& name) +{ + if (!fRedoHistory.IsEmpty()) { + name << " "; + fRedoHistory.Top()->GetName(name); + return true; + } + return false; +} + + +void +EditManager::Clear() +{ + while (!fUndoHistory.IsEmpty()) + fUndoHistory.Pop(); + while (!fRedoHistory.IsEmpty()) + fRedoHistory.Pop(); + + _NotifyListeners(); +} + + +void +EditManager::Save() +{ + if (!fUndoHistory.IsEmpty()) + fEditAtSave = fUndoHistory.Top(); + + _NotifyListeners(); +} + + +bool +EditManager::IsSaved() +{ + bool saved = fUndoHistory.IsEmpty(); + if (fEditAtSave.Get() != NULL && !saved) { + if (fEditAtSave == fUndoHistory.Top()) + saved = true; + } + return saved; +} + + +// #pragma mark - + + +bool +EditManager::AddListener(Listener* listener) +{ + return fListeners.Add(listener); +} + + +void +EditManager::RemoveListener(Listener* listener) +{ + fListeners.Remove(listener); +} + + +// #pragma mark - + + +status_t +EditManager::_AddEdit(const UndoableEditRef& edit) +{ + status_t status = B_OK; + + bool add = true; + if (!fUndoHistory.IsEmpty()) { + // Try to collapse edits to a single edit + // or remove this and the previous edit if + // they reverse each other + const UndoableEditRef& top = fUndoHistory.Top(); + if (edit->UndoesPrevious(top.Get())) { + add = false; + fUndoHistory.Pop(); + } else if (top->CombineWithNext(edit.Get())) { + add = false; + // After collapsing, the edit might + // have changed it's mind about InitCheck() + // (the commands reversed each other) + if (top->InitCheck() != B_OK) { + fUndoHistory.Pop(); + } + } else if (edit->CombineWithPrevious(top.Get())) { + fUndoHistory.Pop(); + // After collapsing, the edit might + // have changed it's mind about InitCheck() + // (the commands reversed each other) + if (edit->InitCheck() != B_OK) { + add = false; + } + } + } + if (add) { + if (!fUndoHistory.Push(edit)) + status = B_NO_MEMORY; + } + + if (status == B_OK) { + // The redo stack needs to be empty + // as soon as an edit was added (also in case of collapsing) + while (!fRedoHistory.IsEmpty()) { + fRedoHistory.Pop(); + } + } + + return status; +} + + +void +EditManager::_NotifyListeners() +{ + int32 count = fListeners.CountItems(); + if (count == 0) + return; + // Iterate a copy of the list, so we don't crash if listeners + // detach themselves while being notified. + ListenerList listenersCopy(fListeners); + if (listenersCopy.CountItems() != count) + return; + for (int32 i = 0; i < count; i++) + listenersCopy.ItemAtFast(i)->EditManagerChanged(this); +} + diff --git a/src/apps/haikudepot/edits_generic/EditManager.h b/src/apps/haikudepot/edits_generic/EditManager.h new file mode 100644 index 0000000000..31dcb95996 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditManager.h @@ -0,0 +1,60 @@ +/* + * Copyright 2006-2012, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ +#ifndef EDIT_MANAGER_H +#define EDIT_MANAGER_H + +#include "EditStack.h" +#include "UndoableEdit.h" + +class BString; +class EditContext; + + +class EditManager { +public: + class Listener { + public: + virtual ~Listener(); + virtual void EditManagerChanged( + const EditManager* manager) = 0; + }; + +public: + EditManager(); + virtual ~EditManager(); + + status_t Perform(UndoableEdit* edit, + EditContext& context); + status_t Perform(const UndoableEditRef& edit, + EditContext& context); + + status_t Undo(EditContext& context); + status_t Redo(EditContext& context); + + bool GetUndoName(BString& name); + bool GetRedoName(BString& name); + + void Clear(); + void Save(); + bool IsSaved(); + + bool AddListener(Listener* listener); + void RemoveListener(Listener* listener); + +private: + status_t _AddEdit(const UndoableEditRef& edit); + + void _NotifyListeners(); + +private: + EditStack fUndoHistory; + EditStack fRedoHistory; + UndoableEditRef fEditAtSave; + + typedef List ListenerList; + ListenerList fListeners; +}; + +#endif // EDIT_MANAGER_H diff --git a/src/apps/haikudepot/edits_generic/EditStack.cpp b/src/apps/haikudepot/edits_generic/EditStack.cpp new file mode 100644 index 0000000000..9c903d6edc --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditStack.cpp @@ -0,0 +1,49 @@ +/* + * Copyright 2012, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#include "EditStack.h" + +#include + + +EditStack::EditStack() + : fEdits() +{ +} + + +EditStack::~EditStack() +{ +} + + +bool +EditStack::Push(const UndoableEditRef& edit) +{ + return fEdits.Add(edit); +} + + +UndoableEditRef +EditStack::Pop() +{ + UndoableEditRef edit(Top()); + fEdits.Remove(); + return edit; +} + + +const UndoableEditRef& +EditStack::Top() const +{ + return fEdits.LastItem(); +} + + +bool +EditStack::IsEmpty() const +{ + return fEdits.CountItems() == 0; +} diff --git a/src/apps/haikudepot/edits_generic/EditStack.h b/src/apps/haikudepot/edits_generic/EditStack.h new file mode 100644 index 0000000000..051ca9a061 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/EditStack.h @@ -0,0 +1,30 @@ +/* + * Copyright 2012, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#ifndef EDIT_STACK_H +#define EDIT_STACK_H + +#include "List.h" +#include "UndoableEdit.h" + +class EditStack { +public: + EditStack(); + virtual ~EditStack(); + + bool Push(const UndoableEditRef& edit); + UndoableEditRef Pop(); + + const UndoableEditRef& Top() const; + + bool IsEmpty() const; + +private: + typedef List EditList; + + EditList fEdits; +}; + +#endif // EDIT_STACK_H diff --git a/src/apps/haikudepot/edits_generic/UndoableEdit.cpp b/src/apps/haikudepot/edits_generic/UndoableEdit.cpp new file mode 100644 index 0000000000..fc1e2af8f2 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/UndoableEdit.cpp @@ -0,0 +1,86 @@ +/* + * Copyright 2006-2012, Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#include "UndoableEdit.h" + +#include + +#include +#include + + +//static int32 sInstanceCount = 0; + + +UndoableEdit::UndoableEdit() + : + fTimeStamp(system_time()) +{ +// sInstanceCount++; +// printf("UndoableEdits: %ld\n", sInstanceCount); +} + + +UndoableEdit::~UndoableEdit() +{ +// sInstanceCount--; +// printf("UndoableEdits: %ld\n", sInstanceCount); +} + + +status_t +UndoableEdit::InitCheck() +{ + return B_NO_INIT; +} + + +status_t +UndoableEdit::Perform(EditContext& context) +{ + return B_ERROR; +} + + +status_t +UndoableEdit::Undo(EditContext& context) +{ + return B_ERROR; +} + + +status_t +UndoableEdit::Redo(EditContext& context) +{ + return Perform(context); +} + + +void +UndoableEdit::GetName(BString& name) +{ + name << "Name of edit goes here."; +} + + +bool +UndoableEdit::UndoesPrevious(const UndoableEdit* previous) +{ + return false; +} + + +bool +UndoableEdit::CombineWithNext(const UndoableEdit* next) +{ + return false; +} + + +bool +UndoableEdit::CombineWithPrevious(const UndoableEdit* previous) +{ + return false; +} diff --git a/src/apps/haikudepot/edits_generic/UndoableEdit.h b/src/apps/haikudepot/edits_generic/UndoableEdit.h new file mode 100644 index 0000000000..d6c8ad36d2 --- /dev/null +++ b/src/apps/haikudepot/edits_generic/UndoableEdit.h @@ -0,0 +1,41 @@ +/* + * Copyright 2006-2012 Stephan Aßmus + * Distributed under the terms of the MIT License. + */ + +#ifndef UNDOABLE_EDIT_H +#define UNDOABLE_EDIT_H + +#include + +class BString; +class EditContext; + +class UndoableEdit : public BReferenceable { +public: + UndoableEdit(); + virtual ~UndoableEdit(); + + virtual status_t InitCheck(); + + virtual status_t Perform(EditContext& context); + virtual status_t Undo(EditContext& context); + virtual status_t Redo(EditContext& context); + + virtual void GetName(BString& name); + + virtual bool UndoesPrevious(const UndoableEdit* previous); + virtual bool CombineWithNext(const UndoableEdit* next); + virtual bool CombineWithPrevious( + const UndoableEdit* previous); + + inline bigtime_t TimeStamp() const + { return fTimeStamp; } + +protected: + bigtime_t fTimeStamp; +}; + +typedef BReference UndoableEditRef; + +#endif // UNDOABLE_EDIT_H