mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-07-23 03:02:50 +03:00
Implemented UndoStack.
7GUIs Circle Drawer now uses the new UndoStack.
This commit is contained in:
@@ -87,6 +87,8 @@
|
||||
|
||||
#include <eepp/ui/uidatabind.hpp>
|
||||
|
||||
#include <eepp/ui/undostack.hpp>
|
||||
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
|
||||
#endif
|
||||
|
||||
@@ -59,9 +59,9 @@ class EE_API UINode : public Node {
|
||||
|
||||
virtual Node* setPosition( const Float& x, const Float& y );
|
||||
|
||||
Node* setPixelsPosition( const Vector2f& position );
|
||||
UINode* setPixelsPosition( const Vector2f& position );
|
||||
|
||||
Node* setPixelsPosition( const Float& x, const Float& y );
|
||||
UINode* setPixelsPosition( const Float& x, const Float& y );
|
||||
|
||||
const Vector2f& getPosition() const;
|
||||
|
||||
|
||||
238
include/eepp/ui/undostack.hpp
Normal file
238
include/eepp/ui/undostack.hpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#ifndef EE_UI_UNDOSTACK_HPP
|
||||
#define EE_UI_UNDOSTACK_HPP
|
||||
|
||||
#include <eepp/core/containers.hpp>
|
||||
#include <eepp/core/noncopyable.hpp>
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
// Absolutely inspired by Qt implementation
|
||||
|
||||
class EE_API UndoCommand : public NonCopyable {
|
||||
public:
|
||||
explicit UndoCommand( UndoCommand* parent = nullptr );
|
||||
explicit UndoCommand( const std::string& text, UndoCommand* parent = nullptr );
|
||||
virtual ~UndoCommand();
|
||||
|
||||
virtual void undo();
|
||||
|
||||
virtual void redo();
|
||||
|
||||
virtual int id() const;
|
||||
virtual bool mergeWith( const UndoCommand* other );
|
||||
|
||||
std::string text() const;
|
||||
std::string actionText() const;
|
||||
void setText( const std::string& text );
|
||||
|
||||
bool isObsolete() const;
|
||||
void setObsolete( bool obsolete );
|
||||
|
||||
int childCount() const;
|
||||
const UndoCommand* child( int index ) const;
|
||||
|
||||
protected:
|
||||
friend class UndoStack;
|
||||
|
||||
std::vector<UndoCommand*> mChilds;
|
||||
std::string mText;
|
||||
std::string mActionText;
|
||||
int mId{ -1 };
|
||||
bool mObsolete{ false };
|
||||
};
|
||||
|
||||
class EE_API UndoStack : public NonCopyable {
|
||||
public:
|
||||
UndoStack() : mIndex( 0 ), mCleanIndex( 0 ), mUndoLimit( 0 ) {}
|
||||
|
||||
~UndoStack();
|
||||
|
||||
void clear();
|
||||
|
||||
void push( UndoCommand* cmd );
|
||||
|
||||
bool canUndo() const;
|
||||
bool canRedo() const;
|
||||
std::string undoText() const;
|
||||
std::string redoText() const;
|
||||
|
||||
int count() const;
|
||||
int index() const;
|
||||
std::string text( int idx ) const;
|
||||
|
||||
bool isClean() const;
|
||||
int cleanIndex() const;
|
||||
|
||||
void beginMacro( const std::string& text );
|
||||
void endMacro();
|
||||
|
||||
void setUndoLimit( int limit );
|
||||
int undoLimit() const;
|
||||
|
||||
const UndoCommand* command( int index ) const;
|
||||
|
||||
void setClean();
|
||||
void resetClean();
|
||||
void setIndex( int idx );
|
||||
void undo();
|
||||
void redo();
|
||||
|
||||
enum EventType {
|
||||
IndexChanged,
|
||||
CleanChanged,
|
||||
CanUndoChanged,
|
||||
CanRedoChanged,
|
||||
UndoTextChanged,
|
||||
RedoTextChanged,
|
||||
};
|
||||
|
||||
class EventIndexChanged;
|
||||
class EventCleanChanged;
|
||||
class EventCanUndoChanged;
|
||||
class EventCanRedoChanged;
|
||||
class EventUndoTextChanged;
|
||||
class EventRedoTextChanged;
|
||||
|
||||
using EventId = Uint32;
|
||||
|
||||
class Event {
|
||||
public:
|
||||
Event( EventType type ) : type( type ) {}
|
||||
|
||||
EventId getCallbackId() const { return cbId; }
|
||||
|
||||
const EventType& getType() const { return type; }
|
||||
|
||||
const EventIndexChanged* asIndexChanged() const {
|
||||
return static_cast<const EventIndexChanged*>( this );
|
||||
}
|
||||
|
||||
const EventCleanChanged* asCleanChanged() const {
|
||||
return static_cast<const EventCleanChanged*>( this );
|
||||
}
|
||||
|
||||
const EventCanUndoChanged* asCanUndoChanged() const {
|
||||
return static_cast<const EventCanUndoChanged*>( this );
|
||||
}
|
||||
|
||||
const EventCanRedoChanged* asCanRedoChanged() const {
|
||||
return static_cast<const EventCanRedoChanged*>( this );
|
||||
}
|
||||
|
||||
const EventUndoTextChanged* asUndoTextChanged() const {
|
||||
return static_cast<const EventUndoTextChanged*>( this );
|
||||
}
|
||||
|
||||
const EventRedoTextChanged* asRedoTextChanged() const {
|
||||
return static_cast<const EventRedoTextChanged*>( this );
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class UndoStack;
|
||||
EventType type;
|
||||
EventId cbId;
|
||||
};
|
||||
|
||||
class EventIndexChanged : public Event {
|
||||
public:
|
||||
EventIndexChanged( int idx ) : Event( EventType::IndexChanged ), mIndex( idx ) {}
|
||||
|
||||
int index() const { return mIndex; }
|
||||
|
||||
protected:
|
||||
int mIndex;
|
||||
};
|
||||
|
||||
class EventCleanChanged : public Event {
|
||||
public:
|
||||
EventCleanChanged( bool clean ) : Event( EventType::CleanChanged ), mClean( clean ) {}
|
||||
|
||||
bool clean() const { return mClean; }
|
||||
|
||||
protected:
|
||||
bool mClean;
|
||||
};
|
||||
|
||||
class EventCanUndoChanged : public Event {
|
||||
public:
|
||||
EventCanUndoChanged( bool canUndo ) :
|
||||
Event( EventType::CanUndoChanged ), mCanUndo( canUndo ) {}
|
||||
|
||||
bool canUndo() const { return mCanUndo; }
|
||||
|
||||
protected:
|
||||
bool mCanUndo;
|
||||
};
|
||||
|
||||
class EventCanRedoChanged : public Event {
|
||||
public:
|
||||
EventCanRedoChanged( bool canRedo ) :
|
||||
Event( EventType::CanRedoChanged ), mCanRedo( canRedo ) {}
|
||||
|
||||
bool canRedo() const { return mCanRedo; }
|
||||
|
||||
protected:
|
||||
bool mCanRedo;
|
||||
};
|
||||
|
||||
class EventUndoTextChanged : public Event {
|
||||
public:
|
||||
EventUndoTextChanged( const std::string& undoText ) :
|
||||
Event( EventType::UndoTextChanged ), mUndoText( undoText ) {}
|
||||
|
||||
const std::string& undoText() const { return mUndoText; }
|
||||
|
||||
protected:
|
||||
std::string mUndoText;
|
||||
};
|
||||
|
||||
class EventRedoTextChanged : public Event {
|
||||
public:
|
||||
EventRedoTextChanged( const std::string& RedoText ) :
|
||||
Event( EventType::RedoTextChanged ), mRedoText( RedoText ) {}
|
||||
|
||||
const std::string& RedoText() const { return mRedoText; }
|
||||
|
||||
protected:
|
||||
std::string mRedoText;
|
||||
};
|
||||
|
||||
using EventCb = std::function<void( const Event* event )>;
|
||||
|
||||
EventId addEventListener( EventType type, EventCb cb );
|
||||
|
||||
EventId on( EventType type, EventCb cb );
|
||||
|
||||
void removeEventListener( EventId id );
|
||||
|
||||
protected:
|
||||
std::deque<UndoCommand*> mCommands;
|
||||
std::deque<UndoCommand*> mMacroStack;
|
||||
UnorderedMap<EventType, std::map<EventId, EventCb>> mEventsCbs;
|
||||
EventId mNextEventId{ 0 };
|
||||
int mIndex;
|
||||
int mCleanIndex;
|
||||
int mUndoLimit;
|
||||
|
||||
bool checkUndoLimit();
|
||||
|
||||
void setIndex( int idx, bool clean );
|
||||
|
||||
void sendEvent( const Event* event );
|
||||
void indexChanged( int idx );
|
||||
void cleanChanged( bool clean );
|
||||
void canUndoChanged( bool canUndo );
|
||||
void canRedoChanged( bool canRedo );
|
||||
void undoTextChanged( const std::string& undoText );
|
||||
void redoTextChanged( const std::string& redoText );
|
||||
};
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
#endif // EE_UI_UNDOSTACK_HPP
|
||||
@@ -1046,6 +1046,8 @@
|
||||
../../src/eepp/ui/uiwidgettable.cpp
|
||||
../../src/eepp/ui/uiwidgettablerow.cpp
|
||||
../../src/eepp/ui/uiwindow.cpp
|
||||
../../src/eepp/ui/undostack.cpp
|
||||
../../src/eepp/ui/undostack.hpp
|
||||
../../src/eepp/ui/widgetcommandexecuter.hpp
|
||||
../../src/eepp/window/backend/allegro5/cbackendal.hpp
|
||||
../../src/eepp/window/backend/allegro5/cclipboardal.cpp
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include <algorithm>
|
||||
#include <eepp/graphics/framebuffer.hpp>
|
||||
#include <eepp/graphics/globalbatchrenderer.hpp>
|
||||
#include <eepp/graphics/renderer/renderer.hpp>
|
||||
|
||||
@@ -114,7 +114,7 @@ Node* UINode::setPosition( const Float& x, const Float& y ) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Node* UINode::setPixelsPosition( const Vector2f& Pos ) {
|
||||
UINode* UINode::setPixelsPosition( const Vector2f& Pos ) {
|
||||
if ( mPosition != Pos ) {
|
||||
mDpPos = PixelDensity::pxToDp( Pos );
|
||||
Transformable::setPosition( Pos );
|
||||
@@ -124,7 +124,7 @@ Node* UINode::setPixelsPosition( const Vector2f& Pos ) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Node* UINode::setPixelsPosition( const Float& x, const Float& y ) {
|
||||
UINode* UINode::setPixelsPosition( const Float& x, const Float& y ) {
|
||||
setPixelsPosition( Vector2f( x, y ) );
|
||||
return this;
|
||||
}
|
||||
|
||||
495
src/eepp/ui/undostack.cpp
Normal file
495
src/eepp/ui/undostack.cpp
Normal file
@@ -0,0 +1,495 @@
|
||||
#include <eepp/core/debug.hpp>
|
||||
#include <eepp/core/memorymanager.hpp>
|
||||
#include <eepp/ui/undostack.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
UndoCommand::UndoCommand( const std::string& text, UndoCommand* parent ) : UndoCommand( parent ) {
|
||||
setText( text );
|
||||
}
|
||||
|
||||
UndoCommand::UndoCommand( UndoCommand* parent ) {
|
||||
if ( parent != nullptr )
|
||||
parent->mChilds.push_back( this );
|
||||
}
|
||||
|
||||
UndoCommand::~UndoCommand() {
|
||||
for ( const auto& child : mChilds )
|
||||
delete child;
|
||||
}
|
||||
|
||||
bool UndoCommand::isObsolete() const {
|
||||
return mObsolete;
|
||||
}
|
||||
|
||||
void UndoCommand::setObsolete( bool obsolete ) {
|
||||
mObsolete = obsolete;
|
||||
}
|
||||
|
||||
int UndoCommand::id() const {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool UndoCommand::mergeWith( const UndoCommand* ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void UndoCommand::redo() {
|
||||
for ( std::size_t i = 0; i < mChilds.size(); ++i )
|
||||
mChilds.at( i )->redo();
|
||||
}
|
||||
|
||||
void UndoCommand::undo() {
|
||||
for ( int i = mChilds.size() - 1; i >= 0; --i )
|
||||
mChilds.at( i )->undo();
|
||||
}
|
||||
|
||||
std::string UndoCommand::text() const {
|
||||
return mText;
|
||||
}
|
||||
|
||||
std::string UndoCommand::actionText() const {
|
||||
return mActionText;
|
||||
}
|
||||
|
||||
void UndoCommand::setText( const std::string& text ) {
|
||||
auto cdpos = text.find_first_of( '\n' );
|
||||
if ( cdpos != std::string::npos ) {
|
||||
mText = text.substr( 0, cdpos );
|
||||
mActionText = text.substr( cdpos + 1 );
|
||||
} else {
|
||||
mText = text;
|
||||
mActionText = text;
|
||||
}
|
||||
}
|
||||
|
||||
int UndoCommand::childCount() const {
|
||||
return mChilds.size();
|
||||
}
|
||||
|
||||
const UndoCommand* UndoCommand::child( int index ) const {
|
||||
if ( index < 0 || index >= static_cast<int>( mChilds.size() ) )
|
||||
return nullptr;
|
||||
return mChilds.at( index );
|
||||
}
|
||||
|
||||
void UndoStack::setIndex( int idx, bool clean ) {
|
||||
bool wasClean = mIndex == mCleanIndex;
|
||||
|
||||
if ( idx != mIndex ) {
|
||||
mIndex = idx;
|
||||
indexChanged( mIndex );
|
||||
canUndoChanged( canUndo() );
|
||||
undoTextChanged( undoText() );
|
||||
canRedoChanged( canRedo() );
|
||||
redoTextChanged( redoText() );
|
||||
}
|
||||
|
||||
if ( clean )
|
||||
mCleanIndex = mIndex;
|
||||
|
||||
bool isClean = mIndex == mCleanIndex;
|
||||
if ( isClean != wasClean )
|
||||
cleanChanged( isClean );
|
||||
}
|
||||
|
||||
bool UndoStack::checkUndoLimit() {
|
||||
if ( mUndoLimit <= 0 || !mMacroStack.empty() ||
|
||||
mUndoLimit >= static_cast<int>( mCommands.size() ) )
|
||||
return false;
|
||||
|
||||
int delCount = mCommands.size() - mUndoLimit;
|
||||
|
||||
for ( int i = 0; i < delCount; ++i ) {
|
||||
delete mCommands.front();
|
||||
mCommands.pop_front();
|
||||
}
|
||||
|
||||
mIndex -= delCount;
|
||||
if ( mCleanIndex != -1 ) {
|
||||
if ( mCleanIndex < delCount )
|
||||
mCleanIndex = -1; // we've deleted the clean command
|
||||
else
|
||||
mCleanIndex -= delCount;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UndoStack::~UndoStack() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void UndoStack::clear() {
|
||||
if ( mCommands.empty() )
|
||||
return;
|
||||
|
||||
bool wasClean = isClean();
|
||||
|
||||
mMacroStack.clear();
|
||||
for ( const auto& cmd : mCommands )
|
||||
delete cmd;
|
||||
mCommands.clear();
|
||||
|
||||
mIndex = 0;
|
||||
mCleanIndex = 0;
|
||||
|
||||
indexChanged( 0 );
|
||||
canUndoChanged( false );
|
||||
undoTextChanged( std::string() );
|
||||
canRedoChanged( false );
|
||||
redoTextChanged( std::string() );
|
||||
|
||||
if ( !wasClean )
|
||||
cleanChanged( true );
|
||||
}
|
||||
|
||||
void UndoStack::push( UndoCommand* cmd ) {
|
||||
if ( !cmd->isObsolete() )
|
||||
cmd->redo();
|
||||
|
||||
bool macro = !mMacroStack.empty();
|
||||
|
||||
UndoCommand* cur = nullptr;
|
||||
if ( macro ) {
|
||||
UndoCommand* macroCmd = mMacroStack.back();
|
||||
if ( !macroCmd->mChilds.empty() )
|
||||
cur = macroCmd->mChilds.back();
|
||||
} else {
|
||||
if ( mIndex > 0 )
|
||||
cur = mCommands.at( mIndex - 1 );
|
||||
while ( mIndex < static_cast<int>( mCommands.size() ) ) {
|
||||
delete mCommands.back();
|
||||
mCommands.pop_back();
|
||||
}
|
||||
if ( mCleanIndex > mIndex )
|
||||
mCleanIndex = -1; // we've deleted the clean state
|
||||
}
|
||||
|
||||
bool tryMerge = cur != nullptr && cur->id() != -1 && cur->id() == cmd->id() &&
|
||||
( macro || mIndex != mCleanIndex );
|
||||
|
||||
if ( tryMerge && cur->mergeWith( cmd ) ) {
|
||||
delete cmd;
|
||||
|
||||
if ( macro ) {
|
||||
if ( cur->isObsolete() ) {
|
||||
delete mMacroStack.back()->mChilds.back();
|
||||
mMacroStack.back()->mChilds.pop_back();
|
||||
}
|
||||
} else {
|
||||
if ( cur->isObsolete() ) {
|
||||
delete mCommands.back();
|
||||
mCommands.pop_back();
|
||||
|
||||
setIndex( mIndex - 1, false );
|
||||
} else {
|
||||
indexChanged( mIndex );
|
||||
canUndoChanged( canUndo() );
|
||||
undoTextChanged( undoText() );
|
||||
canRedoChanged( canRedo() );
|
||||
redoTextChanged( redoText() );
|
||||
}
|
||||
}
|
||||
} else if ( cmd->isObsolete() ) {
|
||||
delete cmd; // command should be deleted and NOT added to the stack
|
||||
} else {
|
||||
if ( macro ) {
|
||||
mMacroStack.back()->mChilds.push_back( cmd );
|
||||
} else {
|
||||
mCommands.push_back( cmd );
|
||||
checkUndoLimit();
|
||||
setIndex( mIndex + 1, false );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::setClean() {
|
||||
if ( unlikely( !mMacroStack.empty() ) ) {
|
||||
eePRINTL( "UndoStack::setClean(): cannot set clean in the middle of a macro" );
|
||||
return;
|
||||
}
|
||||
|
||||
setIndex( mIndex, true );
|
||||
}
|
||||
|
||||
void UndoStack::resetClean() {
|
||||
const bool wasClean = isClean();
|
||||
mCleanIndex = -1;
|
||||
if ( wasClean )
|
||||
cleanChanged( false );
|
||||
}
|
||||
|
||||
bool UndoStack::isClean() const {
|
||||
if ( !mMacroStack.empty() )
|
||||
return false;
|
||||
return mCleanIndex == mIndex;
|
||||
}
|
||||
|
||||
int UndoStack::cleanIndex() const {
|
||||
return mCleanIndex;
|
||||
}
|
||||
|
||||
void UndoStack::undo() {
|
||||
if ( mIndex == 0 )
|
||||
return;
|
||||
|
||||
if ( unlikely( !mMacroStack.empty() ) ) {
|
||||
eePRINTL( "UndoStack::undo(): cannot undo in the middle of a macro" );
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = mIndex - 1;
|
||||
UndoCommand* cmd = mCommands.at( idx );
|
||||
|
||||
if ( !cmd->isObsolete() )
|
||||
cmd->undo();
|
||||
|
||||
if ( cmd->isObsolete() ) { // A separate check is done b/c the undo command may set obsolete
|
||||
// flag
|
||||
delete mCommands[idx];
|
||||
mCommands.erase( mCommands.begin() + idx );
|
||||
|
||||
if ( mCleanIndex > idx )
|
||||
resetClean();
|
||||
}
|
||||
|
||||
setIndex( idx, false );
|
||||
}
|
||||
|
||||
void UndoStack::redo() {
|
||||
if ( mIndex == static_cast<int>( mCommands.size() ) )
|
||||
return;
|
||||
|
||||
if ( unlikely( !mMacroStack.empty() ) ) {
|
||||
eePRINTL( "UndoStack::redo(): cannot redo in the middle of a macro" );
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = mIndex;
|
||||
UndoCommand* cmd = mCommands.at( idx );
|
||||
|
||||
if ( !cmd->isObsolete() )
|
||||
cmd->redo(); // A separate check is done b/c the undo command may set obsolete flag
|
||||
|
||||
if ( cmd->isObsolete() ) {
|
||||
delete mCommands[idx];
|
||||
mCommands.erase( mCommands.begin() + idx );
|
||||
|
||||
if ( mCleanIndex > idx )
|
||||
resetClean();
|
||||
} else {
|
||||
setIndex( mIndex + 1, false );
|
||||
}
|
||||
}
|
||||
|
||||
UndoStack::EventId UndoStack::addEventListener( EventType type, EventCb cb ) {
|
||||
auto eventId = ++mNextEventId;
|
||||
mEventsCbs[type].insert( { eventId, std::move( cb ) } );
|
||||
return eventId;
|
||||
}
|
||||
|
||||
UndoStack::EventId UndoStack::on( EventType type, EventCb cb ) {
|
||||
return addEventListener( type, std::move( cb ) );
|
||||
}
|
||||
|
||||
void UndoStack::removeEventListener( EventId id ) {
|
||||
for ( auto& eventType : mEventsCbs )
|
||||
if ( eventType.second.erase( id ) > 0 )
|
||||
break;
|
||||
}
|
||||
|
||||
int UndoStack::count() const {
|
||||
return mCommands.size();
|
||||
}
|
||||
|
||||
int UndoStack::index() const {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
void UndoStack::setIndex( int idx ) {
|
||||
if ( unlikely( !mMacroStack.empty() ) ) {
|
||||
eePRINTL( "UndoStack::setIndex(): cannot set index in the middle of a macro" );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( idx < 0 )
|
||||
idx = 0;
|
||||
else if ( idx > static_cast<int>( mCommands.size() ) )
|
||||
idx = mCommands.size();
|
||||
|
||||
int i = mIndex;
|
||||
while ( i < idx ) {
|
||||
UndoCommand* cmd = mCommands.at( i );
|
||||
|
||||
if ( !cmd->isObsolete() )
|
||||
cmd->redo(); // A separate check is done b/c the undo command may set obsolete flag
|
||||
|
||||
if ( cmd->isObsolete() ) {
|
||||
delete mCommands[i];
|
||||
mCommands.erase( mCommands.begin() + i );
|
||||
|
||||
if ( mCleanIndex > i )
|
||||
resetClean();
|
||||
|
||||
idx--; // Subtract from idx because we removed a command
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
while ( i > idx ) {
|
||||
UndoCommand* cmd = mCommands.at( --i );
|
||||
|
||||
cmd->undo();
|
||||
if ( cmd->isObsolete() ) {
|
||||
delete mCommands[i];
|
||||
mCommands.erase( mCommands.begin() + i );
|
||||
|
||||
if ( mCleanIndex > i )
|
||||
resetClean();
|
||||
}
|
||||
}
|
||||
|
||||
setIndex( idx, false );
|
||||
}
|
||||
|
||||
bool UndoStack::canUndo() const {
|
||||
if ( !mMacroStack.empty() )
|
||||
return false;
|
||||
return mIndex > 0;
|
||||
}
|
||||
|
||||
bool UndoStack::canRedo() const {
|
||||
if ( !mMacroStack.empty() )
|
||||
return false;
|
||||
return mIndex < static_cast<int>( mCommands.size() );
|
||||
}
|
||||
|
||||
std::string UndoStack::undoText() const {
|
||||
if ( !mMacroStack.empty() )
|
||||
return std::string();
|
||||
if ( mIndex > 0 )
|
||||
return mCommands.at( mIndex - 1 )->actionText();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string UndoStack::redoText() const {
|
||||
if ( !mMacroStack.empty() )
|
||||
return std::string();
|
||||
if ( mIndex < static_cast<int>( mCommands.size() ) )
|
||||
return mCommands.at( mIndex )->actionText();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
void UndoStack::beginMacro( const std::string& text ) {
|
||||
UndoCommand* cmd = new UndoCommand();
|
||||
cmd->setText( text );
|
||||
|
||||
if ( mMacroStack.empty() ) {
|
||||
while ( mIndex < static_cast<int>( mCommands.size() ) ) {
|
||||
delete mCommands.back();
|
||||
mCommands.pop_back();
|
||||
}
|
||||
if ( mCleanIndex > mIndex )
|
||||
mCleanIndex = -1; // we've deleted the clean state
|
||||
mCommands.push_back( cmd );
|
||||
} else {
|
||||
mMacroStack.back()->mChilds.push_back( cmd );
|
||||
}
|
||||
mMacroStack.push_back( cmd );
|
||||
|
||||
if ( mMacroStack.size() == 1 ) {
|
||||
canUndoChanged( false );
|
||||
undoTextChanged( std::string() );
|
||||
canRedoChanged( false );
|
||||
redoTextChanged( std::string() );
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::endMacro() {
|
||||
if ( unlikely( mMacroStack.empty() ) ) {
|
||||
eePRINTL( "UndoStack::endMacro(): no matching beginMacro()" );
|
||||
return;
|
||||
}
|
||||
|
||||
mMacroStack.pop_back();
|
||||
|
||||
if ( mMacroStack.empty() ) {
|
||||
checkUndoLimit();
|
||||
setIndex( mIndex + 1, false );
|
||||
}
|
||||
}
|
||||
|
||||
const UndoCommand* UndoStack::command( int index ) const {
|
||||
if ( index < 0 || index >= static_cast<int>( mCommands.size() ) )
|
||||
return nullptr;
|
||||
return mCommands.at( index );
|
||||
}
|
||||
|
||||
std::string UndoStack::text( int idx ) const {
|
||||
if ( idx < 0 || idx >= static_cast<int>( mCommands.size() ) )
|
||||
return std::string();
|
||||
return mCommands.at( idx )->text();
|
||||
}
|
||||
|
||||
void UndoStack::setUndoLimit( int limit ) {
|
||||
if ( unlikely( !mCommands.empty() ) ) {
|
||||
eePRINTL(
|
||||
"UndoStack::setUndoLimit(): an undo limit can only be set when the stack is empty" );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( limit == mUndoLimit )
|
||||
return;
|
||||
mUndoLimit = limit;
|
||||
checkUndoLimit();
|
||||
}
|
||||
|
||||
int UndoStack::undoLimit() const {
|
||||
return mUndoLimit;
|
||||
}
|
||||
|
||||
void UndoStack::sendEvent( const Event* event ) {
|
||||
if ( 0 != mEventsCbs.count( event->getType() ) ) {
|
||||
auto eventMap = mEventsCbs[event->getType()];
|
||||
if ( eventMap.begin() != eventMap.end() ) {
|
||||
for ( const auto& e : eventMap ) {
|
||||
const_cast<Event*>( event )->cbId = e.first;
|
||||
e.second( event );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::indexChanged( int idx ) {
|
||||
EventIndexChanged event( idx );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
void UndoStack::cleanChanged( bool clean ) {
|
||||
EventCleanChanged event( clean );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
void UndoStack::canUndoChanged( bool canUndo ) {
|
||||
EventCanUndoChanged event( canUndo );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
void UndoStack::canRedoChanged( bool canRedo ) {
|
||||
EventCanRedoChanged event( canRedo );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
void UndoStack::undoTextChanged( const std::string& undoText ) {
|
||||
EventUndoTextChanged event( undoText );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
void UndoStack::redoTextChanged( const std::string& redoText ) {
|
||||
EventRedoTextChanged event( redoText );
|
||||
sendEvent( &event );
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
@@ -605,7 +605,7 @@ void Window::runMainLoop( std::function<void()> func, int fps ) {
|
||||
mMainLoop = std::move( func );
|
||||
|
||||
#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
|
||||
emscripten_set_main_loop( eepp_mainloop, std::max( mFrameData.FPS.Limit, std::max( 0, fps ) ),
|
||||
emscripten_set_main_loop( eepp_mainloop, eemax( (int)mFrameData.FPS.Limit, eemax( 0, fps ) ),
|
||||
1 );
|
||||
#else
|
||||
if ( fps >= 0 )
|
||||
|
||||
@@ -1,7 +1,106 @@
|
||||
#include <eepp/ee.hpp>
|
||||
|
||||
class ResizeCircleCommand : public UndoCommand {
|
||||
public:
|
||||
ResizeCircleCommand( UIWidget* circle, Vector2f oldPos, Float oldSize ) :
|
||||
circle( circle ),
|
||||
pos( circle->getPixelsPosition() ),
|
||||
size( circle->getPixelsSize().x ),
|
||||
oldPos( oldPos ),
|
||||
oldSize( oldSize ) {}
|
||||
|
||||
void undo() override {
|
||||
circle->setPixelsPosition( oldPos )
|
||||
->setPixelsSize( { oldSize, oldSize } )
|
||||
->setBorderRadius( oldSize / 2.f );
|
||||
}
|
||||
|
||||
void redo() override {
|
||||
circle->setPixelsPosition( pos )
|
||||
->setPixelsSize( { size, size } )
|
||||
->setBorderRadius( size / 2.f );
|
||||
}
|
||||
|
||||
protected:
|
||||
UIWidget* circle;
|
||||
Vector2f pos;
|
||||
Float size;
|
||||
Vector2f oldPos;
|
||||
Float oldSize;
|
||||
};
|
||||
|
||||
class AddCircleCommand : public UndoCommand {
|
||||
public:
|
||||
AddCircleCommand( Node* parent, Vector2f pos, Float size, UndoStack& undoStack ) :
|
||||
pos( pos ), size( size ), parent( parent ), undoStack( undoStack ) {
|
||||
create();
|
||||
}
|
||||
|
||||
virtual ~AddCircleCommand() {
|
||||
if ( !SceneManager::instance()->isShuttingDown() )
|
||||
circle->close();
|
||||
}
|
||||
|
||||
void create() {
|
||||
circle = UIWidget::New();
|
||||
circle->addClass( "circle" )->setParent( parent );
|
||||
circle->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed )
|
||||
->setPixelsSize( { size, size } )
|
||||
->setBorderRadius( size / 2.f )
|
||||
->setPixelsPosition( pos );
|
||||
circle->onClick(
|
||||
[this]( auto ) {
|
||||
auto adjustMsgBox = UIMessageBox::New( UIMessageBox::OK, "Adjust diameter..." );
|
||||
adjustMsgBox->on( Event::OnConfirm, [this]( auto ) {
|
||||
auto pos( circle->getPixelsPosition().asInt() );
|
||||
auto msgBox = UIMessageBox::New(
|
||||
UIMessageBox::OK,
|
||||
String::format( "Adjust diameter of circle at (%d, %d)", pos.x, pos.y ) );
|
||||
msgBox->getButtonOK()->setVisible( false )->setEnabled( false );
|
||||
auto slider = UISlider::NewHorizontal();
|
||||
slider->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent )
|
||||
->setLayoutMargin( Rectf( 8, 8, 8, 8 ) )
|
||||
->setParent( msgBox->getLayoutCont()->getFirstChild() )
|
||||
->setFocus()
|
||||
->toPosition( 1 );
|
||||
slider->setMinValue( 20 );
|
||||
slider->setMaxValue( 80 );
|
||||
slider->setValue( circle->getPixelsSize().x );
|
||||
Vector2f oldPos = circle->getPixelsPosition();
|
||||
Float oldSize = circle->getPixelsSize().x;
|
||||
slider->on( Event::OnValueChange, [this, slider]( auto ) {
|
||||
Float size = std::floor( slider->getValue() );
|
||||
Sizef newSize = { size, size };
|
||||
circle->setPixelsPosition(
|
||||
( circle->getPixelsPosition() + ( circle->getPixelsSize() / 2.f ) ) -
|
||||
newSize / 2.f );
|
||||
circle->setPixelsSize( newSize )->setBorderRadius( size / 2 );
|
||||
} );
|
||||
msgBox->on( Event::OnWindowClose, [this, oldPos, oldSize]( auto ) {
|
||||
if ( oldPos != circle->getPixelsPosition() ||
|
||||
oldSize != circle->getPixelsSize().x )
|
||||
undoStack.push( new ResizeCircleCommand( circle, oldPos, oldSize ) );
|
||||
} );
|
||||
msgBox->showWhenReady();
|
||||
} );
|
||||
adjustMsgBox->showWhenReady();
|
||||
},
|
||||
MouseButton::EE_BUTTON_RIGHT );
|
||||
}
|
||||
|
||||
void undo() override { circle->setVisible( false )->setEnabled( false ); }
|
||||
|
||||
void redo() override { circle->setVisible( true )->setEnabled( true ); }
|
||||
|
||||
protected:
|
||||
UIWidget* circle{ nullptr };
|
||||
Vector2f pos;
|
||||
Float size;
|
||||
Node* parent;
|
||||
UndoStack& undoStack;
|
||||
};
|
||||
|
||||
// Reference https://eugenkiss.github.io/7guis/tasks#circle
|
||||
// This is a work in progress, undo redo stack is pending
|
||||
EE_MAIN_FUNC int main( int, char** ) {
|
||||
UIApplication app( { 800, 600, "eepp - 7GUIs - Circle Drawer" } );
|
||||
UIWidget* cont = app.getUI()->loadLayoutFromString( R"xml(
|
||||
@@ -22,64 +121,29 @@ EE_MAIN_FUNC int main( int, char** ) {
|
||||
</style>
|
||||
<vbox layout_width="match_parent" layout_height="match_parent">
|
||||
<hbox padding="4dp" layout_gravity="center">
|
||||
<PushButton id="undo" text="Undo" marginRight="8dp" />
|
||||
<PushButton id="redo" text="Redo" marginLeft="8dp" />
|
||||
<PushButton id="undo" text="Undo" marginRight="8dp" enabled="false" />
|
||||
<PushButton id="redo" text="Redo" marginLeft="8dp" enabled="false" />
|
||||
</hbox>
|
||||
<Widget id="canvas" layout_width="match_parent" layout_height="0" layout_weight="1" />
|
||||
</vbox>
|
||||
)xml" );
|
||||
UndoStack undoStack;
|
||||
UIWidget* canvas = cont->find<UIWidget>( "canvas" );
|
||||
canvas->onClick( [&]( const MouseEvent* event ) {
|
||||
Float size = Math::randi( 20, 80 );
|
||||
Vector2f mousePos( event->getPosition().asFloat() - size / 2 );
|
||||
canvas->worldToNodeTranslation( mousePos );
|
||||
UIWidget* circle = UIWidget::New();
|
||||
circle->addClass( "circle" )->setParent( canvas );
|
||||
circle->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed )
|
||||
->setPixelsSize( { size, size } )
|
||||
->setBorderRadius( size / 2 )
|
||||
->setPixelsPosition( mousePos );
|
||||
// undoRedoStack->insert( circle );
|
||||
circle->onClick(
|
||||
[circle]( auto ) {
|
||||
auto adjustMsgBox = UIMessageBox::New( UIMessageBox::OK, "Adjust diameter..." );
|
||||
adjustMsgBox->on( Event::OnConfirm, [circle]( auto ) {
|
||||
auto pos( circle->getPixelsPosition().asInt() );
|
||||
auto msgBox = UIMessageBox::New(
|
||||
UIMessageBox::OK,
|
||||
String::format( "Adjust diameter of circle at (%d, %d)", pos.x, pos.y ) );
|
||||
msgBox->getButtonOK()->setVisible( false )->setEnabled( false );
|
||||
auto slider = UISlider::NewHorizontal();
|
||||
slider->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent )
|
||||
->setLayoutMargin( Rectf( 8, 8, 8, 8 ) )
|
||||
->setParent( msgBox->getLayoutCont()->getFirstChild() )
|
||||
->setFocus()
|
||||
->toPosition( 1 );
|
||||
slider->setMinValue( 20 );
|
||||
slider->setMaxValue( 80 );
|
||||
slider->setValue( circle->getPixelsSize().x );
|
||||
slider->on( Event::OnValueChange, [slider, circle]( auto ) {
|
||||
Float size = std::floor( slider->getValue() );
|
||||
Sizef newSize = { size, size };
|
||||
circle->setPixelsPosition(
|
||||
( circle->getPixelsPosition() + ( circle->getPixelsSize() / 2.f ) ) -
|
||||
newSize / 2.f );
|
||||
circle->setPixelsSize( newSize )->setBorderRadius( size / 2 );
|
||||
} );
|
||||
msgBox->on( Event::OnWindowClose, [/*circle*/]( auto ) {
|
||||
// undoRedoStack->changeSize( circle );
|
||||
} );
|
||||
msgBox->showWhenReady();
|
||||
} );
|
||||
adjustMsgBox->showWhenReady();
|
||||
},
|
||||
MouseButton::EE_BUTTON_RIGHT );
|
||||
undoStack.push( new AddCircleCommand( canvas, mousePos, size, undoStack ) );
|
||||
} );
|
||||
cont->find( "undo" )->onClick( []( auto ) {
|
||||
// undoRedoStack->undo();
|
||||
auto undoBut = cont->find( "undo" );
|
||||
auto redoBut = cont->find( "redo" );
|
||||
undoBut->onClick( [&undoStack]( auto ) { undoStack.undo(); } );
|
||||
redoBut->onClick( [&undoStack]( auto ) { undoStack.redo(); } );
|
||||
undoStack.on( UndoStack::CanUndoChanged, [undoBut]( const UndoStack::Event* event ) {
|
||||
undoBut->setEnabled( event->asCanUndoChanged()->canUndo() );
|
||||
} );
|
||||
cont->find( "redo" )->onClick( []( auto ) {
|
||||
// undoRedoStack->redo();
|
||||
undoStack.on( UndoStack::CanRedoChanged, [redoBut]( const UndoStack::Event* event ) {
|
||||
redoBut->setEnabled( event->asCanRedoChanged()->canRedo() );
|
||||
} );
|
||||
return app.run();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user