bbf695a00c
631d8d73 changed GRID_CELL_TEXT_BUTTON::OnTextChange to fire a
synthetic wxEVT_GRID_CELL_CHANGING event instead of calling
ApplyEdit(). The synthetic event did not carry the editor's current
text via SetString(), so all handlers using GetString() received an
empty string. In the Configure Paths dialog this clobbered cell
values and triggered "path cannot be empty" errors on the first
keystroke.
The synthetic event now calls SetString() with the editor's current
value. The Configure Paths validation handler also skips mid-edit
events by checking IsCellEditControlEnabled(), which is true during
typing but false during the native commit sequence. This prevents
validation dialogs from firing while the user is still typing.
Fixes https://gitlab.com/kicad/code/kicad/-/issues/22953
527 lines
14 KiB
C++
527 lines
14 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
|
|
* @author Jon Evans <jon@craftyjon.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation, either version 3 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <string_utils.h>
|
|
#include <wx/stc/stc.h>
|
|
#include <wx/dc.h>
|
|
#include <widgets/grid_text_helpers.h>
|
|
#include <widgets/wx_grid.h>
|
|
#include <scintilla_tricks.h>
|
|
#include <widgets/indicator_icon.h>
|
|
#include <kiplatform/ui.h>
|
|
|
|
|
|
//-------- GRID_CELL_TEXT_EDITOR ------------------------------------------------------
|
|
//
|
|
|
|
GRID_CELL_TEXT_EDITOR::GRID_CELL_TEXT_EDITOR() : wxGridCellTextEditor()
|
|
{}
|
|
|
|
|
|
void GRID_CELL_TEXT_EDITOR::SetValidator( const wxValidator& validator )
|
|
{
|
|
// keep our own copy because wxGridCellTextEditor's is annoyingly private
|
|
m_validator.reset( static_cast<wxValidator*>( validator.Clone() ) );
|
|
|
|
wxGridCellTextEditor::SetValidator( *m_validator );
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_EDITOR::StartingKey( wxKeyEvent& event )
|
|
{
|
|
if( m_validator )
|
|
{
|
|
m_validator->SetWindow( Text() );
|
|
m_validator->ProcessEvent( event );
|
|
}
|
|
|
|
if( event.GetSkipped() )
|
|
{
|
|
wxGridCellTextEditor::StartingKey( event );
|
|
event.Skip( false );
|
|
}
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_EDITOR::SetSize( const wxRect& aRect )
|
|
{
|
|
wxRect rect( aRect );
|
|
WX_GRID::CellEditorTransformSizeRect( rect );
|
|
|
|
#if defined( __WXMSW__ )
|
|
rect.Offset( 0, 1 );
|
|
#elif defined( __WXMAC__ )
|
|
rect.Offset( 0, 2 );
|
|
rect.SetHeight( rect.GetHeight() - 4 );
|
|
#endif
|
|
|
|
wxGridCellEditor::SetSize( rect ); // NOLINT(*-parent-virtual-call)
|
|
}
|
|
|
|
|
|
//-------- GRID_CELL_TEXT_RENDERER ------------------------------------------------------
|
|
//
|
|
|
|
GRID_CELL_TEXT_RENDERER::GRID_CELL_TEXT_RENDERER() :
|
|
wxGridCellStringRenderer()
|
|
{}
|
|
|
|
|
|
void GRID_CELL_TEXT_RENDERER::Draw( wxGrid& aGrid, wxGridCellAttr& aAttr, wxDC& aDC, const wxRect& aRect,
|
|
int aRow, int aCol, bool isSelected )
|
|
{
|
|
WX_GRID_TABLE_BASE* table = dynamic_cast<WX_GRID_TABLE_BASE*>( aGrid.GetTable() );
|
|
|
|
if( !table || !table->IsExpanderColumn( aCol ) )
|
|
return wxGridCellStringRenderer::Draw( aGrid, aAttr, aDC, aRect, aRow, aCol, isSelected );
|
|
|
|
wxString value = aGrid.GetCellValue( aRow, aCol );
|
|
|
|
wxRect rect = aRect;
|
|
rect.Inflate( -1 );
|
|
|
|
// erase background
|
|
wxGridCellRenderer::Draw( aGrid, aAttr, aDC, aRect, aRow, aCol, isSelected );
|
|
|
|
// draw the icon
|
|
int leftCut = aDC.FromDIP( 4 );
|
|
|
|
INDICATOR_ICON::ICON_ID state = ROW_ICON_PROVIDER::STATE::OFF;
|
|
|
|
if( table->GetGroupType( aRow ) == GROUP_COLLAPSED )
|
|
state = ROW_ICON_PROVIDER::STATE::CLOSED;
|
|
else if( table->GetGroupType( aRow ) == GROUP_EXPANDED )
|
|
state = ROW_ICON_PROVIDER::STATE::OPEN;
|
|
|
|
wxBitmap bitmap = static_cast<WX_GRID&>( aGrid ).GetRowIconProvider()->GetIndicatorIcon( state );
|
|
bitmap.SetScaleFactor( KIPLATFORM::UI::GetPixelScaleFactor( &aGrid ) );
|
|
|
|
aDC.DrawBitmap( bitmap,
|
|
rect.GetLeft() + leftCut,
|
|
rect.GetTop() + ( rect.GetHeight() - bitmap.GetLogicalHeight() ) / 2,
|
|
true );
|
|
|
|
leftCut += bitmap.GetLogicalWidth();
|
|
|
|
leftCut += aDC.FromDIP( 4 );
|
|
|
|
if( table->GetGroupType( aRow ) == CHILD_ITEM )
|
|
leftCut += aDC.FromDIP( 12 );
|
|
|
|
rect.x += leftCut;
|
|
rect.width -= leftCut;
|
|
|
|
// draw the text
|
|
SetTextColoursAndFont( aGrid, aAttr, aDC, isSelected );
|
|
aGrid.DrawTextRectangle( aDC, value, rect, wxALIGN_LEFT, wxALIGN_CENTRE );
|
|
}
|
|
|
|
|
|
wxSize GRID_CELL_TEXT_RENDERER::GetBestSize( wxGrid& grid, wxGridCellAttr& attr, wxDC& dc, int row, int col )
|
|
{
|
|
WX_GRID_TABLE_BASE* table = dynamic_cast<WX_GRID_TABLE_BASE*>( grid.GetTable() );
|
|
|
|
if( !table || !table->IsExpanderColumn( col ) )
|
|
return wxGridCellStringRenderer::GetBestSize( grid, attr, dc, row, col );
|
|
|
|
INDICATOR_ICON::ICON_ID state = ROW_ICON_PROVIDER::STATE::OFF;
|
|
wxBitmap bitmap = static_cast<WX_GRID&>( grid ).GetRowIconProvider()->GetIndicatorIcon( state );
|
|
|
|
bitmap.SetScaleFactor( KIPLATFORM::UI::GetPixelScaleFactor( &grid ) );
|
|
|
|
wxString text = grid.GetCellValue( row, col );
|
|
wxSize size = wxGridCellStringRenderer::DoGetBestSize( attr, dc, text );
|
|
|
|
size.x += bitmap.GetLogicalWidth() + dc.FromDIP( 8 );
|
|
|
|
if( table->GetGroupType( row ) == CHILD_ITEM )
|
|
size.x += dc.FromDIP( 12 );
|
|
|
|
size.y = std::max( size.y, dc.FromDIP( 2 ) );
|
|
|
|
return size;
|
|
}
|
|
|
|
|
|
//-------- GRID_CELL_ESCAPED_TEXT_RENDERER ------------------------------------------------------
|
|
//
|
|
|
|
GRID_CELL_ESCAPED_TEXT_RENDERER::GRID_CELL_ESCAPED_TEXT_RENDERER() :
|
|
wxGridCellStringRenderer()
|
|
{}
|
|
|
|
void GRID_CELL_ESCAPED_TEXT_RENDERER::Draw( wxGrid& aGrid, wxGridCellAttr& aAttr, wxDC& aDC, const wxRect& aRect,
|
|
int aRow, int aCol, bool isSelected )
|
|
{
|
|
wxString unescaped = UnescapeString( aGrid.GetCellValue( aRow, aCol ) );
|
|
|
|
wxRect rect = aRect;
|
|
rect.Inflate( -1 );
|
|
|
|
// erase background
|
|
wxGridCellRenderer::Draw( aGrid, aAttr, aDC, aRect, aRow, aCol, isSelected );
|
|
|
|
SetTextColoursAndFont( aGrid, aAttr, aDC, isSelected );
|
|
aGrid.DrawTextRectangle( aDC, unescaped, rect, wxALIGN_LEFT, wxALIGN_CENTRE );
|
|
}
|
|
|
|
|
|
wxSize GRID_CELL_ESCAPED_TEXT_RENDERER::GetBestSize( wxGrid& aGrid, wxGridCellAttr& aAttr, wxDC& aDC,
|
|
int aRow, int aCol )
|
|
{
|
|
wxString unescaped = UnescapeString( aGrid.GetCellValue( aRow, aCol ) );
|
|
return wxGridCellStringRenderer::DoGetBestSize( aAttr, aDC, unescaped );
|
|
}
|
|
|
|
|
|
//-------- GRID_CELL_STC_EDITOR -----------------------------------------------------------------
|
|
//
|
|
|
|
GRID_CELL_STC_EDITOR::GRID_CELL_STC_EDITOR( bool aIgnoreCase, bool aSingleLine,
|
|
std::function<void( wxStyledTextEvent&, SCINTILLA_TRICKS* )> onCharFn ) :
|
|
m_scintillaTricks( nullptr ),
|
|
m_ignoreCase( aIgnoreCase ),
|
|
m_singleLine( aSingleLine ),
|
|
m_onCharFn( std::move( onCharFn ) )
|
|
{}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::SetSize( const wxRect& aRect )
|
|
{
|
|
wxRect rect( aRect );
|
|
WX_GRID::CellEditorTransformSizeRect( rect );
|
|
|
|
#if defined( __WXMSW__ )
|
|
#if !wxCHECK_VERSION( 3, 3, 0 )
|
|
rect.Offset( -1, 0 );
|
|
// hack no longer needed with wx 3.3
|
|
rect.SetHeight( rect.GetHeight() + 6 );
|
|
#else
|
|
rect.Offset( 0, 1 );
|
|
#endif
|
|
#elif defined( __WXGTK__ )
|
|
rect.Offset( -1, 3 );
|
|
#else
|
|
rect.Offset( 1, 3 );
|
|
rect.SetWidth( rect.GetWidth() - 1 );
|
|
rect.SetHeight( rect.GetHeight() - 4 );
|
|
#endif
|
|
wxGridCellEditor::SetSize( rect );
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::Create( wxWindow* aParent, wxWindowID aId, wxEvtHandler* aEventHandler )
|
|
{
|
|
m_control = new wxStyledTextCtrl( aParent, wxID_ANY, wxDefaultPosition, wxSize( 0, 0 ),
|
|
wxBORDER_NONE );
|
|
|
|
stc_ctrl()->SetTabIndents( false );
|
|
stc_ctrl()->SetBackSpaceUnIndents( false );
|
|
stc_ctrl()->SetViewEOL( false );
|
|
stc_ctrl()->SetViewWhiteSpace( false );
|
|
stc_ctrl()->SetIndentationGuides( false );
|
|
stc_ctrl()->SetMarginWidth( 0, 0 ); // Symbol margin
|
|
stc_ctrl()->SetMarginWidth( 1, 0 ); // Line-number margin
|
|
stc_ctrl()->SetEOLMode( wxSTC_EOL_LF );
|
|
stc_ctrl()->AutoCompSetMaxWidth( 25 );
|
|
stc_ctrl()->AutoCompSetIgnoreCase( m_ignoreCase );
|
|
stc_ctrl()->UsePopUp( 0 );
|
|
|
|
// A hack which causes Scintilla to auto-size the text editor canvas
|
|
// See: https://github.com/jacobslusser/ScintillaNET/issues/216
|
|
stc_ctrl()->SetScrollWidth( 1 );
|
|
stc_ctrl()->SetScrollWidthTracking( true );
|
|
|
|
m_scintillaTricks = new SCINTILLA_TRICKS(
|
|
stc_ctrl(), wxEmptyString, m_singleLine,
|
|
|
|
// onAcceptFn
|
|
[this]( wxKeyEvent& aEvent )
|
|
{
|
|
HandleReturn( aEvent );
|
|
},
|
|
|
|
// onCharFn
|
|
[this]( wxStyledTextEvent& aEvent )
|
|
{
|
|
m_onCharFn( aEvent, m_scintillaTricks );
|
|
} );
|
|
|
|
stc_ctrl()->Bind( wxEVT_KILL_FOCUS, &GRID_CELL_STC_EDITOR::onFocusLoss, this );
|
|
|
|
wxGridCellEditor::Create( aParent, aId, aEventHandler );
|
|
}
|
|
|
|
|
|
wxStyledTextCtrl* GRID_CELL_STC_EDITOR::stc_ctrl() const
|
|
{
|
|
return static_cast<wxStyledTextCtrl*>( m_control );
|
|
}
|
|
|
|
|
|
wxString GRID_CELL_STC_EDITOR::GetValue() const
|
|
{
|
|
return stc_ctrl()->GetText();
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::StartingKey( wxKeyEvent& event )
|
|
{
|
|
int ch;
|
|
|
|
bool isPrintable;
|
|
|
|
#if wxUSE_UNICODE
|
|
ch = event.GetUnicodeKey();
|
|
|
|
if( ch != WXK_NONE )
|
|
isPrintable = true;
|
|
else
|
|
#endif // wxUSE_UNICODE
|
|
{
|
|
ch = event.GetKeyCode();
|
|
isPrintable = ch >= WXK_SPACE && ch < WXK_START;
|
|
}
|
|
|
|
switch( ch )
|
|
{
|
|
case WXK_DELETE:
|
|
// Delete the initial character when starting to edit with DELETE.
|
|
stc_ctrl()->DeleteRange( 0, 1 );
|
|
break;
|
|
|
|
case WXK_BACK:
|
|
// Delete the last character when starting to edit with BACKSPACE.
|
|
stc_ctrl()->DeleteBack();
|
|
break;
|
|
|
|
default:
|
|
if( isPrintable )
|
|
stc_ctrl()->WriteText( static_cast<wxChar>( ch ) );
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::Show( bool aShow, wxGridCellAttr* aAttr )
|
|
{
|
|
if( !aShow )
|
|
stc_ctrl()->AutoCompCancel();
|
|
|
|
wxGridCellEditor::Show( aShow, aAttr );
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::BeginEdit( int aRow, int aCol, wxGrid* aGrid )
|
|
{
|
|
auto evtHandler = static_cast<wxGridCellEditorEvtHandler*>( m_control->GetEventHandler() );
|
|
|
|
// Don't immediately end if we get a kill focus event within BeginEdit
|
|
evtHandler->SetInSetFocus( true );
|
|
|
|
m_value = aGrid->GetTable()->GetValue( aRow, aCol );
|
|
|
|
stc_ctrl()->SetFocus();
|
|
stc_ctrl()->SetText( m_value );
|
|
stc_ctrl()->SelectAll();
|
|
}
|
|
|
|
|
|
bool GRID_CELL_STC_EDITOR::EndEdit( int, int, const wxGrid*, const wxString&, wxString *aNewVal )
|
|
{
|
|
const wxString value = stc_ctrl()->GetText();
|
|
|
|
if( value == m_value )
|
|
return false;
|
|
|
|
m_value = value;
|
|
|
|
if( aNewVal )
|
|
*aNewVal = value;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::ApplyEdit( int aRow, int aCol, wxGrid* aGrid )
|
|
{
|
|
aGrid->GetTable()->SetValue( aRow, aCol, m_value );
|
|
}
|
|
|
|
|
|
void GRID_CELL_STC_EDITOR::onFocusLoss( wxFocusEvent& aEvent )
|
|
{
|
|
if( stc_ctrl() )
|
|
stc_ctrl()->AutoCompCancel();
|
|
|
|
aEvent.Skip();
|
|
}
|
|
|
|
|
|
//-------- Editor Base Class for GRID_TEXT_BUTTON_HELPERS ------------
|
|
//
|
|
// Note: this implementation is an adaptation of wxGridCellChoiceEditor
|
|
//
|
|
// Note: this class is here instead of in grid_text_button_helpers.h/cpp to
|
|
// keep from dragging a ton of stuff into kicommon.
|
|
|
|
|
|
wxString GRID_CELL_TEXT_BUTTON::GetValue() const
|
|
{
|
|
return Combo()->GetValue();
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::SetSize( const wxRect& aRect )
|
|
{
|
|
wxRect rect( aRect );
|
|
WX_GRID::CellEditorTransformSizeRect( rect );
|
|
|
|
wxGridCellEditor::SetSize( rect );
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::StartingKey( wxKeyEvent& event )
|
|
{
|
|
// Note: this is a copy of wxGridCellTextEditor's StartingKey()
|
|
|
|
// Since this is now happening in the EVT_CHAR event EmulateKeyPress is no
|
|
// longer an appropriate way to get the character into the text control.
|
|
// Do it ourselves instead. We know that if we get this far that we have
|
|
// a valid character, so not a whole lot of testing needs to be done.
|
|
|
|
// wxComboCtrl inherits from wxTextEntry, so can statically cast
|
|
wxTextEntry* textEntry = static_cast<wxTextEntry*>( Combo() );
|
|
int ch;
|
|
|
|
bool isPrintable;
|
|
|
|
#if wxUSE_UNICODE
|
|
ch = event.GetUnicodeKey();
|
|
|
|
if( ch != WXK_NONE )
|
|
isPrintable = true;
|
|
else
|
|
#endif // wxUSE_UNICODE
|
|
{
|
|
ch = event.GetKeyCode();
|
|
isPrintable = ch >= WXK_SPACE && ch < WXK_START;
|
|
}
|
|
|
|
switch( ch )
|
|
{
|
|
case WXK_DELETE:
|
|
// Delete the initial character when starting to edit with DELETE.
|
|
textEntry->Remove( 0, 1 );
|
|
break;
|
|
|
|
case WXK_BACK:
|
|
// Delete the last character when starting to edit with BACKSPACE.
|
|
{
|
|
const long pos = textEntry->GetLastPosition();
|
|
textEntry->Remove( pos - 1, pos );
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if( isPrintable )
|
|
textEntry->WriteText( static_cast<wxChar>( ch ) );
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::BeginEdit( int aRow, int aCol, wxGrid* aGrid )
|
|
{
|
|
m_grid = aGrid;
|
|
m_row = aRow;
|
|
m_col = aCol;
|
|
|
|
auto evtHandler = static_cast< wxGridCellEditorEvtHandler* >( m_control->GetEventHandler() );
|
|
|
|
// Don't immediately end if we get a kill focus event within BeginEdit
|
|
evtHandler->SetInSetFocus( true );
|
|
|
|
m_value = aGrid->GetTable()->GetValue( aRow, aCol );
|
|
|
|
Combo()->SetValue( m_value );
|
|
Combo()->SetFocus();
|
|
|
|
// Bind event to handle text changes
|
|
Combo()->Bind( wxEVT_TEXT, &GRID_CELL_TEXT_BUTTON::OnTextChange, this );
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::OnTextChange( wxCommandEvent& event )
|
|
{
|
|
if( m_grid && m_row >= 0 && m_row < m_grid->GetNumberRows() && m_col >= 0 && m_col < m_grid->GetNumberCols() )
|
|
{
|
|
wxGridEvent evt( m_grid->GetId(), wxEVT_GRID_CELL_CHANGING, m_grid, m_row, m_col );
|
|
evt.SetString( Combo()->GetValue() );
|
|
m_grid->GetEventHandler()->ProcessEvent( evt );
|
|
}
|
|
|
|
event.Skip(); // Ensure that other handlers can process this event too
|
|
}
|
|
|
|
|
|
bool GRID_CELL_TEXT_BUTTON::EndEdit( int, int, const wxGrid*, const wxString&, wxString *aNewVal )
|
|
{
|
|
Combo()->Unbind( wxEVT_TEXT, &GRID_CELL_TEXT_BUTTON::OnTextChange, this );
|
|
m_grid = nullptr;
|
|
m_row = -1;
|
|
m_col = -1;
|
|
|
|
const wxString value = Combo()->GetValue();
|
|
|
|
if( value == m_value )
|
|
return false;
|
|
|
|
m_value = value;
|
|
|
|
if( aNewVal )
|
|
*aNewVal = value;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::ApplyEdit( int aRow, int aCol, wxGrid* aGrid )
|
|
{
|
|
aGrid->GetTable()->SetValue( aRow, aCol, m_value );
|
|
}
|
|
|
|
|
|
void GRID_CELL_TEXT_BUTTON::Reset()
|
|
{
|
|
Combo()->SetValue( m_value );
|
|
}
|
|
|
|
|
|
#if wxUSE_VALIDATORS
|
|
void GRID_CELL_TEXT_BUTTON::SetValidator( const wxValidator& validator )
|
|
{
|
|
m_validator.reset( static_cast< wxValidator* >( validator.Clone() ) );
|
|
}
|
|
#endif
|
|
|
|
|