Implemented UndoStack.

7GUIs Circle Drawer now uses the new UndoStack.
This commit is contained in:
Martín Lucas Golini
2024-02-09 02:26:51 -03:00
parent 5271e2fd2e
commit f875672a3d
9 changed files with 854 additions and 54 deletions

View File

@@ -87,6 +87,8 @@
#include <eepp/ui/uidatabind.hpp>
#include <eepp/ui/undostack.hpp>
#include <eepp/ui/uiapplication.hpp>
#endif

View File

@@ -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;

View 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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
#include <algorithm>
#include <eepp/graphics/framebuffer.hpp>
#include <eepp/graphics/globalbatchrenderer.hpp>
#include <eepp/graphics/renderer/renderer.hpp>

View File

@@ -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
View 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

View File

@@ -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 )

View File

@@ -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();
}