From fe4de09d7187e14087963aab0108c154a46f71a6 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Tue, 26 Aug 2025 14:37:22 -0700 Subject: [PATCH] Add undo/redo functionality to dialog boxes Fixes https://gitlab.com/kicad/code/kicad/-/issues/18986 --- common/dialog_shim.cpp | 441 ++++++++++++++++++++++++++++++++++++++++- include/dialog_shim.h | 33 +++ 2 files changed, 473 insertions(+), 1 deletion(-) diff --git a/common/dialog_shim.cpp b/common/dialog_shim.cpp index 939f92069c..187ccafa17 100644 --- a/common/dialog_shim.cpp +++ b/common/dialog_shim.cpp @@ -41,6 +41,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -79,7 +82,8 @@ DIALOG_SHIM::DIALOG_SHIM( wxWindow* aParent, wxWindowID id, const wxString& titl m_qmodal_parent_disabler( nullptr ), m_parentFrame( nullptr ), m_userPositioned( false ), - m_userResized( false ) + m_userResized( false ), + m_handlingUndoRedo( false ) { KIWAY_HOLDER* kiwayHolder = nullptr; m_initialSize = size; @@ -174,6 +178,75 @@ DIALOG_SHIM::~DIALOG_SHIM() disconnectFocusHandlers( GetChildren() ); + std::function disconnectUndoRedoHandlers = + [&]( wxWindowList& children ) + { + for( wxWindow* child : children ) + { + if( wxTextCtrl* textCtrl = dynamic_cast( child ) ) + { + textCtrl->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxStyledTextCtrl* scintilla = dynamic_cast( child ) ) + { + scintilla->Unbind( wxEVT_STC_CHANGE, &DIALOG_SHIM::onStyledTextChanged, this ); + } + else if( wxComboBox* combo = dynamic_cast( child ) ) + { + combo->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + combo->Unbind( wxEVT_COMBOBOX, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxChoice* choice = dynamic_cast( child ) ) + { + choice->Unbind( wxEVT_CHOICE, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxCheckBox* check = dynamic_cast( child ) ) + { + check->Unbind( wxEVT_CHECKBOX, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxSpinCtrl* spin = dynamic_cast( child ) ) + { + spin->Unbind( wxEVT_SPINCTRL, &DIALOG_SHIM::onSpinEvent, this ); + spin->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxSpinCtrlDouble* spinD = dynamic_cast( child ) ) + { + spinD->Unbind( wxEVT_SPINCTRLDOUBLE, &DIALOG_SHIM::onSpinDoubleEvent, this ); + spinD->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxRadioButton* radio = dynamic_cast( child ) ) + { + radio->Unbind( wxEVT_RADIOBUTTON, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxRadioBox* radioBox = dynamic_cast( child ) ) + { + radioBox->Unbind( wxEVT_RADIOBOX, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxGrid* grid = dynamic_cast( child ) ) + { + grid->Unbind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SHIM::onGridCellChanged, this ); + } + else if( wxPropertyGrid* propGrid = dynamic_cast( child ) ) + { + propGrid->Unbind( wxEVT_PG_CHANGED, &DIALOG_SHIM::onPropertyGridChanged, this ); + } + else if( wxCheckListBox* checkList = dynamic_cast( child ) ) + { + checkList->Unbind( wxEVT_CHECKLISTBOX, &DIALOG_SHIM::onCommandEvent, this ); + } + else if( wxDataViewListCtrl* dataList = dynamic_cast( child ) ) + { + dataList->Unbind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_SHIM::onDataViewListChanged, this ); + } + else + { + disconnectUndoRedoHandlers( child->GetChildren() ); + } + } + }; + + disconnectUndoRedoHandlers( GetChildren() ); + // if the dialog is quasi-modal, this will end its event loop if( IsQuasiModal() ) EndQuasiModal( wxID_CANCEL ); @@ -732,6 +805,347 @@ void DIALOG_SHIM::SelectAllInTextCtrls( wxWindowList& children ) } +void DIALOG_SHIM::registerUndoRedoHandlers( wxWindowList& children ) +{ + for( wxWindow* child : children ) + { + if( wxTextCtrl* textCtrl = dynamic_cast( child ) ) + { + textCtrl->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ textCtrl ] = textCtrl->GetValue(); + } + else if( wxStyledTextCtrl* scintilla = dynamic_cast( child ) ) + { + scintilla->Bind( wxEVT_STC_CHANGE, &DIALOG_SHIM::onStyledTextChanged, this ); + m_currentValues[ scintilla ] = scintilla->GetText(); + } + else if( wxComboBox* combo = dynamic_cast( child ) ) + { + combo->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + combo->Bind( wxEVT_COMBOBOX, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ combo ] = combo->GetValue(); + } + else if( wxChoice* choice = dynamic_cast( child ) ) + { + choice->Bind( wxEVT_CHOICE, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ choice ] = static_cast( choice->GetSelection() ); + } + else if( wxCheckBox* check = dynamic_cast( child ) ) + { + check->Bind( wxEVT_CHECKBOX, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ check ] = check->GetValue(); + } + else if( wxSpinCtrl* spin = dynamic_cast( child ) ) + { + spin->Bind( wxEVT_SPINCTRL, &DIALOG_SHIM::onSpinEvent, this ); + spin->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ spin ] = static_cast( spin->GetValue() ); + } + else if( wxSpinCtrlDouble* spinD = dynamic_cast( child ) ) + { + spinD->Bind( wxEVT_SPINCTRLDOUBLE, &DIALOG_SHIM::onSpinDoubleEvent, this ); + spinD->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ spinD ] = spinD->GetValue(); + } + else if( wxRadioButton* radio = dynamic_cast( child ) ) + { + radio->Bind( wxEVT_RADIOBUTTON, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ radio ] = radio->GetValue(); + } + else if( wxRadioBox* radioBox = dynamic_cast( child ) ) + { + radioBox->Bind( wxEVT_RADIOBOX, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ radioBox ] = static_cast( radioBox->GetSelection() ); + } + else if( wxGrid* grid = dynamic_cast( child ) ) + { + grid->Bind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SHIM::onGridCellChanged, this ); + m_currentValues[ grid ] = getControlValue( grid ); + } + else if( wxPropertyGrid* propGrid = dynamic_cast( child ) ) + { + propGrid->Bind( wxEVT_PG_CHANGED, &DIALOG_SHIM::onPropertyGridChanged, this ); + m_currentValues[ propGrid ] = getControlValue( propGrid ); + } + else if( wxCheckListBox* checkList = dynamic_cast( child ) ) + { + checkList->Bind( wxEVT_CHECKLISTBOX, &DIALOG_SHIM::onCommandEvent, this ); + m_currentValues[ checkList ] = getControlValue( checkList ); + } + else if( wxDataViewListCtrl* dataList = dynamic_cast( child ) ) + { + dataList->Bind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_SHIM::onDataViewListChanged, this ); + m_currentValues[ dataList ] = getControlValue( dataList ); + } + else + { + registerUndoRedoHandlers( child->GetChildren() ); + } + } +} + + +void DIALOG_SHIM::recordControlChange( wxWindow* aCtrl ) +{ + wxVariant before = m_currentValues[ aCtrl ]; + wxVariant after = getControlValue( aCtrl ); + + if( before != after ) + { + m_undoStack.push_back( { aCtrl, before, after } ); + m_redoStack.clear(); + m_currentValues[ aCtrl ] = after; + } +} + + +void DIALOG_SHIM::onCommandEvent( wxCommandEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + + +void DIALOG_SHIM::onSpinEvent( wxSpinEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + + +void DIALOG_SHIM::onSpinDoubleEvent( wxSpinDoubleEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + + +void DIALOG_SHIM::onStyledTextChanged( wxStyledTextEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + + +void DIALOG_SHIM::onGridCellChanged( wxGridEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + +void DIALOG_SHIM::onPropertyGridChanged( wxPropertyGridEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + +void DIALOG_SHIM::onDataViewListChanged( wxDataViewEvent& aEvent ) +{ + if( !m_handlingUndoRedo ) + recordControlChange( static_cast( aEvent.GetEventObject() ) ); + + aEvent.Skip(); +} + +wxVariant DIALOG_SHIM::getControlValue( wxWindow* aCtrl ) +{ + if( wxTextCtrl* textCtrl = dynamic_cast( aCtrl ) ) + return wxVariant( textCtrl->GetValue() ); + else if( wxStyledTextCtrl* scintilla = dynamic_cast( aCtrl ) ) + return wxVariant( scintilla->GetText() ); + else if( wxComboBox* combo = dynamic_cast( aCtrl ) ) + return wxVariant( combo->GetValue() ); + else if( wxChoice* choice = dynamic_cast( aCtrl ) ) + return wxVariant( (long) choice->GetSelection() ); + else if( wxCheckBox* check = dynamic_cast( aCtrl ) ) + return wxVariant( check->GetValue() ); + else if( wxSpinCtrl* spin = dynamic_cast( aCtrl ) ) + return wxVariant( (long) spin->GetValue() ); + else if( wxSpinCtrlDouble* spinD = dynamic_cast( aCtrl ) ) + return wxVariant( spinD->GetValue() ); + else if( wxRadioButton* radio = dynamic_cast( aCtrl ) ) + return wxVariant( radio->GetValue() ); + else if( wxRadioBox* radioBox = dynamic_cast( aCtrl ) ) + return wxVariant( (long) radioBox->GetSelection() ); + else if( wxGrid* grid = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::array(); + int rows = grid->GetNumberRows(); + int cols = grid->GetNumberCols(); + for( int r = 0; r < rows; ++r ) + { + nlohmann::json row = nlohmann::json::array(); + for( int c = 0; c < cols; ++c ) + row.push_back( std::string( grid->GetCellValue( r, c ).ToUTF8() ) ); + j.push_back( row ); + } + return wxVariant( wxString( j.dump() ) ); + } + else if( wxPropertyGrid* propGrid = dynamic_cast( aCtrl ) ) + { + nlohmann::json j; + for( wxPropertyGridIterator it = propGrid->GetIterator(); !it.AtEnd(); ++it ) + { + wxPGProperty* prop = *it; + j[ prop->GetName().ToStdString() ] = prop->GetValueAsString().ToStdString(); + } + return wxVariant( wxString( j.dump() ) ); + } + else if( wxCheckListBox* checkList = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::array(); + unsigned int count = checkList->GetCount(); + for( unsigned int i = 0; i < count; ++i ) + if( checkList->IsChecked( i ) ) + j.push_back( i ); + return wxVariant( wxString( j.dump() ) ); + } + else if( wxDataViewListCtrl* dataList = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::array(); + unsigned int rows = dataList->GetItemCount(); + unsigned int cols = dataList->GetColumnCount(); + for( unsigned int r = 0; r < rows; ++r ) + { + nlohmann::json row = nlohmann::json::array(); + for( unsigned int c = 0; c < cols; ++c ) + { + wxVariant val; + dataList->GetValue( val, r, c ); + row.push_back( std::string( val.GetString().ToUTF8() ) ); + } + j.push_back( row ); + } + return wxVariant( wxString( j.dump() ) ); + } + else + return wxVariant(); +} + + +void DIALOG_SHIM::setControlValue( wxWindow* aCtrl, const wxVariant& aValue ) +{ + if( wxTextCtrl* textCtrl = dynamic_cast( aCtrl ) ) + textCtrl->SetValue( aValue.GetString() ); + else if( wxStyledTextCtrl* scintilla = dynamic_cast( aCtrl ) ) + scintilla->SetText( aValue.GetString() ); + else if( wxComboBox* combo = dynamic_cast( aCtrl ) ) + combo->SetValue( aValue.GetString() ); + else if( wxChoice* choice = dynamic_cast( aCtrl ) ) + choice->SetSelection( (int) aValue.GetLong() ); + else if( wxCheckBox* check = dynamic_cast( aCtrl ) ) + check->SetValue( aValue.GetBool() ); + else if( wxSpinCtrl* spin = dynamic_cast( aCtrl ) ) + spin->SetValue( (int) aValue.GetLong() ); + else if( wxSpinCtrlDouble* spinD = dynamic_cast( aCtrl ) ) + spinD->SetValue( aValue.GetDouble() ); + else if( wxRadioButton* radio = dynamic_cast( aCtrl ) ) + radio->SetValue( aValue.GetBool() ); + else if( wxRadioBox* radioBox = dynamic_cast( aCtrl ) ) + radioBox->SetSelection( (int) aValue.GetLong() ); + else if( wxGrid* grid = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false ); + if( j.is_array() ) + { + int rows = std::min( (int) j.size(), grid->GetNumberRows() ); + for( int r = 0; r < rows; ++r ) + { + nlohmann::json row = j[r]; + int cols = std::min( (int) row.size(), grid->GetNumberCols() ); + for( int c = 0; c < cols; ++c ) + grid->SetCellValue( r, c, wxString( row[c].get() ) ); + } + } + } + else if( wxPropertyGrid* propGrid = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false ); + if( j.is_object() ) + { + for( auto it = j.begin(); it != j.end(); ++it ) + propGrid->SetPropertyValue( wxString( it.key() ), wxString( it.value().get() ) ); + } + } + else if( wxCheckListBox* checkList = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false ); + if( j.is_array() ) + { + unsigned int count = checkList->GetCount(); + for( unsigned int i = 0; i < count; ++i ) + checkList->Check( i, false ); + for( auto& idx : j ) + { + unsigned int i = idx.get(); + if( i < count ) + checkList->Check( i, true ); + } + } + } + else if( wxDataViewListCtrl* dataList = dynamic_cast( aCtrl ) ) + { + nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false ); + if( j.is_array() ) + { + unsigned int rows = std::min( static_cast( j.size() ), static_cast( dataList->GetItemCount() ) ); + for( unsigned int r = 0; r < rows; ++r ) + { + nlohmann::json row = j[r]; + unsigned int cols = std::min( (unsigned int) row.size(), dataList->GetColumnCount() ); + for( unsigned int c = 0; c < cols; ++c ) + { + wxVariant val( wxString( row[c].get() ) ); + dataList->SetValue( val, r, c ); + } + } + } + } +} + + +void DIALOG_SHIM::doUndo() +{ + if( m_undoStack.empty() ) + return; + + m_handlingUndoRedo = true; + UNDO_STEP step = m_undoStack.back(); + m_undoStack.pop_back(); + setControlValue( step.ctrl, step.before ); + m_currentValues[ step.ctrl ] = step.before; + m_redoStack.push_back( step ); + m_handlingUndoRedo = false; +} + + +void DIALOG_SHIM::doRedo() +{ + if( m_redoStack.empty() ) + return; + + m_handlingUndoRedo = true; + UNDO_STEP step = m_redoStack.back(); + m_redoStack.pop_back(); + setControlValue( step.ctrl, step.after ); + m_currentValues[ step.ctrl ] = step.after; + m_undoStack.push_back( step ); + m_handlingUndoRedo = false; +} + + void DIALOG_SHIM::OnPaint( wxPaintEvent &event ) { if( m_firstPaintEvent ) @@ -739,6 +1153,7 @@ void DIALOG_SHIM::OnPaint( wxPaintEvent &event ) KIPLATFORM::UI::FixupCancelButtonCmdKeyCollision( this ); SelectAllInTextCtrls( GetChildren() ); + registerUndoRedoHandlers( GetChildren() ); if( m_initialFocusTarget ) KIPLATFORM::UI::ForceFocus( m_initialFocusTarget ); @@ -966,6 +1381,30 @@ void DIALOG_SHIM::onChildSetFocus( wxFocusEvent& aEvent ) void DIALOG_SHIM::OnCharHook( wxKeyEvent& aEvt ) { + int key = aEvt.GetKeyCode(); + int mods = 0; + + if( aEvt.ControlDown() ) + mods |= MD_CTRL; + if( aEvt.ShiftDown() ) + mods |= MD_SHIFT; + if( aEvt.AltDown() ) + mods |= MD_ALT; + + int hotkey = key | mods; + + // Check for standard undo/redo hotkeys + if( hotkey == (MD_CTRL + 'Z') ) + { + doUndo(); + return; + } + else if( hotkey == (MD_CTRL + MD_SHIFT + 'Z') || hotkey == (MD_CTRL + 'Y') ) + { + doRedo(); + return; + } + if( aEvt.GetKeyCode() == 'U' && aEvt.GetModifiers() == wxMOD_CONTROL ) { if( m_parentFrame ) diff --git a/include/dialog_shim.h b/include/dialog_shim.h index 1ba6278f75..921450afe8 100644 --- a/include/dialog_shim.h +++ b/include/dialog_shim.h @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include class EDA_BASE_FRAME; @@ -38,6 +40,11 @@ class UNIT_BINDER; class wxGridEvent; class wxGUIEventLoop; class wxInitDialogEvent; +class wxSpinEvent; +class wxSpinDoubleEvent; +class wxStyledTextEvent; +class wxPropertyGridEvent; +class wxDataViewEvent; /** @@ -229,6 +236,20 @@ private: std::string generateKey( const wxWindow* aWin ) const; + void registerUndoRedoHandlers( wxWindowList& aChildren ); + void recordControlChange( wxWindow* aCtrl ); + void onCommandEvent( wxCommandEvent& aEvent ); + void onSpinEvent( wxSpinEvent& aEvent ); + void onSpinDoubleEvent( wxSpinDoubleEvent& aEvent ); + void onStyledTextChanged( wxStyledTextEvent& aEvent ); + void onGridCellChanged( wxGridEvent& aEvent ); + void onPropertyGridChanged( wxPropertyGridEvent& aEvent ); + void onDataViewListChanged( wxDataViewEvent& aEvent ); + void doUndo(); + void doRedo(); + wxVariant getControlValue( wxWindow* aCtrl ); + void setControlValue( wxWindow* aCtrl, const wxVariant& aValue ); + DECLARE_EVENT_TABLE(); protected: @@ -263,6 +284,18 @@ protected: // Used to support first-esc-cancels-edit logic std::map m_beforeEditValues; std::map m_unitBinders; + + struct UNDO_STEP + { + wxWindow* ctrl; + wxVariant before; + wxVariant after; + }; + + std::vector m_undoStack; + std::vector m_redoStack; + std::map m_currentValues; + bool m_handlingUndoRedo; }; #endif // DIALOG_SHIM_