diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 9b15b04ff..991af6343 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -87,6 +87,8 @@ #include +#include + #include #endif diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp index 8f590b1c7..63f00d5f5 100644 --- a/include/eepp/ui/uinode.hpp +++ b/include/eepp/ui/uinode.hpp @@ -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; diff --git a/include/eepp/ui/undostack.hpp b/include/eepp/ui/undostack.hpp new file mode 100644 index 000000000..03093cc68 --- /dev/null +++ b/include/eepp/ui/undostack.hpp @@ -0,0 +1,238 @@ +#ifndef EE_UI_UNDOSTACK_HPP +#define EE_UI_UNDOSTACK_HPP + +#include +#include + +#include +#include +#include +#include +#include + +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 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( this ); + } + + const EventCleanChanged* asCleanChanged() const { + return static_cast( this ); + } + + const EventCanUndoChanged* asCanUndoChanged() const { + return static_cast( this ); + } + + const EventCanRedoChanged* asCanRedoChanged() const { + return static_cast( this ); + } + + const EventUndoTextChanged* asUndoTextChanged() const { + return static_cast( this ); + } + + const EventRedoTextChanged* asRedoTextChanged() const { + return static_cast( 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; + + EventId addEventListener( EventType type, EventCb cb ); + + EventId on( EventType type, EventCb cb ); + + void removeEventListener( EventId id ); + + protected: + std::deque mCommands; + std::deque mMacroStack; + UnorderedMap> 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 diff --git a/projects/linux/ee.files b/projects/linux/ee.files index e71b0552c..ffd1fb24b 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -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 diff --git a/src/eepp/scene/scenenode.cpp b/src/eepp/scene/scenenode.cpp index f4b746611..eced703e5 100644 --- a/src/eepp/scene/scenenode.cpp +++ b/src/eepp/scene/scenenode.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index d86b4abba..053e6ddac 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -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; } diff --git a/src/eepp/ui/undostack.cpp b/src/eepp/ui/undostack.cpp new file mode 100644 index 000000000..83223ac37 --- /dev/null +++ b/src/eepp/ui/undostack.cpp @@ -0,0 +1,495 @@ +#include +#include +#include + +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( 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( 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( 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( 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( 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( 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( 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( 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( mCommands.size() ) ) + return nullptr; + return mCommands.at( index ); +} + +std::string UndoStack::text( int idx ) const { + if ( idx < 0 || idx >= static_cast( 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 )->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 diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index cd62da872..1f9d06d3a 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -605,7 +605,7 @@ void Window::runMainLoop( std::function 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 ) diff --git a/src/examples/7guis/circle_drawer/circle_drawer.cpp b/src/examples/7guis/circle_drawer/circle_drawer.cpp index 8d62f379e..df98c9479 100644 --- a/src/examples/7guis/circle_drawer/circle_drawer.cpp +++ b/src/examples/7guis/circle_drawer/circle_drawer.cpp @@ -1,7 +1,106 @@ #include +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** ) { - - + + )xml" ); + UndoStack undoStack; UIWidget* canvas = cont->find( "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(); }