Files
Seth Hillbrand 80c4b1b4df Strip whitespace from bus alias names and members
Bus alias names and member names entered with leading or trailing
whitespace caused silent connection failures because the lookup
used the trimmed name from the schematic label while the definition
retained the untrimmed name.

Centralize sanitization in BUS_ALIAS by trimming whitespace in
SetName() and adding AddMember/SetMembers/ClearMembers methods
that trim and reject empty strings. Remove the mutable Members()
accessor to prevent bypassing the sanitization. Update all callers
across parsers, importers, and the dialog to use the new API.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/19971
2026-02-17 11:43:25 -08:00

533 lines
17 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2018 CERN
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* @author Jon Evans <[email protected]>
*
* 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 <algorithm>
#include <wx/tokenzr.h>
#include <widgets/wx_grid.h>
#include <widgets/std_bitmap_button.h>
#include <confirm.h>
#include <project/project_file.h>
#include <sch_edit_frame.h>
#include <schematic.h>
#include <dialogs/panel_setup_buses.h>
#include "grid_tricks.h"
#include <wx/clipbrd.h>
PANEL_SETUP_BUSES::PANEL_SETUP_BUSES( wxWindow* aWindow, SCH_EDIT_FRAME* aFrame ) :
PANEL_SETUP_BUSES_BASE( aWindow ),
m_frame( aFrame ),
m_lastAlias( 0 ),
m_membersGridDirty( false ),
m_errorGrid( nullptr ),
m_errorRow( -1 )
{
m_membersLabelTemplate = m_membersLabel->GetLabel();
m_addAlias->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
m_deleteAlias->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
m_addMember->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
m_removeMember->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
m_source->SetFont( KIUI::GetSmallInfoFont( aWindow ) );
m_aliasesGrid->OverrideMinSize( 0.6, 0.3 );
m_membersGrid->OverrideMinSize( 0.6, 0.3 );
m_aliasesGrid->SetSelectionMode( wxGrid::wxGridSelectRows );
m_membersGrid->SetSelectionMode( wxGrid::wxGridSelectRows );
m_aliasesGrid->PushEventHandler( new GRID_TRICKS( m_aliasesGrid,
[this]( wxCommandEvent& aEvent )
{
OnAddAlias( aEvent );
} ) );
m_membersGrid->PushEventHandler( new GRID_TRICKS( m_membersGrid,
[this]( wxCommandEvent& aEvent )
{
OnAddMember( aEvent );
} ) );
m_aliasesGrid->SetUseNativeColLabels();
m_membersGrid->SetUseNativeColLabels();
// wxFormBuilder doesn't include this event...
m_aliasesGrid->Connect( wxEVT_GRID_CELL_CHANGING,
wxGridEventHandler( PANEL_SETUP_BUSES::OnAliasesGridCellChanging ),
nullptr, this );
m_membersGrid->Connect( wxEVT_GRID_CELL_CHANGING,
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanging ),
nullptr, this );
m_membersGrid->Connect( wxEVT_GRID_CELL_CHANGED,
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanged ),
nullptr, this );
Layout();
}
PANEL_SETUP_BUSES::~PANEL_SETUP_BUSES()
{
// Delete the GRID_TRICKS.
m_aliasesGrid->PopEventHandler( true );
m_membersGrid->PopEventHandler( true );
m_aliasesGrid->Disconnect( wxEVT_GRID_CELL_CHANGING,
wxGridEventHandler( PANEL_SETUP_BUSES::OnAliasesGridCellChanging ),
nullptr, this );
m_membersGrid->Disconnect( wxEVT_GRID_CELL_CHANGING,
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanging ),
nullptr, this );
m_membersGrid->Disconnect( wxEVT_GRID_CELL_CHANGED,
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanged ),
nullptr, this );
}
void PANEL_SETUP_BUSES::loadAliases()
{
m_aliases.clear();
const auto& projectAliases = m_frame->Prj().GetProjectFile().m_BusAliases;
std::vector<std::pair<wxString, std::vector<wxString>>> aliasList( projectAliases.begin(),
projectAliases.end() );
std::sort( aliasList.begin(), aliasList.end(),
[]( const std::pair<wxString, std::vector<wxString>>& a,
const std::pair<wxString, std::vector<wxString>>& b )
{
return a.first.CmpNoCase( b.first ) < 0;
} );
for( const auto& alias : aliasList )
{
std::shared_ptr<BUS_ALIAS> entry = std::make_shared<BUS_ALIAS>();
entry->SetName( alias.first );
entry->SetMembers( alias.second );
m_aliases.push_back( entry );
}
int ii = 0;
m_aliasesGrid->ClearRows();
m_aliasesGrid->AppendRows( m_aliases.size() );
for( const std::shared_ptr<BUS_ALIAS>& alias : m_aliases )
m_aliasesGrid->SetCellValue( ii++, 0, alias->GetName() );
m_membersBook->SetSelection( 1 );
}
bool PANEL_SETUP_BUSES::TransferDataToWindow()
{
loadAliases();
return true;
}
bool PANEL_SETUP_BUSES::TransferDataFromWindow()
{
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
return false;
// Copy names back just in case they didn't get caught on the GridCellChanging event
for( int ii = 0; ii < m_aliasesGrid->GetNumberRows(); ++ii )
m_aliases[ii]->SetName( m_aliasesGrid->GetCellValue( ii, 0 ) );
// Associate the respective members with the last alias that is active.
updateAliasMembers( m_lastAlias );
m_frame->Schematic().SetBusAliases( m_aliases );
return true;
}
void PANEL_SETUP_BUSES::OnAddAlias( wxCommandEvent& aEvent )
{
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
return;
m_aliasesGrid->OnAddRow(
[&]() -> std::pair<int, int>
{
// New aliases are stored at the project level
m_aliases.push_back( std::make_shared<BUS_ALIAS>() );
int row = m_aliasesGrid->GetNumberRows();
// Associate the respective members with the previous alias. This ensures that the association
// starts correctly when adding more than one row.
// But in order to avoid overwriting the members of the last (row - 1) alias with those of the
// selected alias (since the user may choose a different alias), the alias member update here
// should only happen if the current alias (m_lastAlias) is the last one (row - 1).
if( ( row > 0 ) && ( m_lastAlias == ( row - 1 ) ) )
updateAliasMembers( row - 1 );
m_aliasesGrid->AppendRows();
return { row, 0 };
} );
}
void PANEL_SETUP_BUSES::OnDeleteAlias( wxCommandEvent& aEvent )
{
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
return;
m_aliasesGrid->OnDeleteRows(
[&]( int row )
{
// Clear the members grid first so we don't try to write it back to a deleted alias
m_membersGrid->ClearRows();
m_lastAlias = -1;
m_lastAliasName = wxEmptyString;
m_aliases.erase( m_aliases.begin() + row );
m_aliasesGrid->DeleteRows( row, 1 );
} );
}
void PANEL_SETUP_BUSES::OnAddMember( wxCommandEvent& aEvent )
{
m_membersGrid->OnAddRow(
[&]() -> std::pair<int, int>
{
int row = m_membersGrid->GetNumberRows();
m_membersGrid->AppendRows();
/*
* Check if the clipboard contains text data.
*
* - If `clipboardHasText` is true, select the specified row in the members grid to allow
* our custom context menu to paste the clipboard .
* - Otherwise, enable and display the cell edit control, allowing the user to manually edit
* the cell.
*/
bool clipboardHasText = false;
if( wxTheClipboard->Open() )
{
if( wxTheClipboard->IsSupported( wxDF_TEXT )
|| wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
{
clipboardHasText = true;
}
wxTheClipboard->Close();
}
if( clipboardHasText )
return { row, -1 };
else
return { row, 0 };
} );
}
void PANEL_SETUP_BUSES::OnRemoveMember( wxCommandEvent& aEvent )
{
m_membersGrid->OnDeleteRows(
[&]( int row )
{
m_membersGrid->DeleteRows( row, 1 );
// Update the member list of the current bus alias from the members grid
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ m_lastAlias ];
alias->ClearMembers();
for( int ii = 0; ii < m_membersGrid->GetNumberRows(); ++ii )
alias->AddMember( m_membersGrid->GetCellValue( ii, 0 ) );
} );
}
void PANEL_SETUP_BUSES::OnAliasesGridCellChanging( wxGridEvent& event )
{
int row = event.GetRow();
if( row >= 0 )
{
wxString name = event.GetString();
for( int ii = 0; ii < m_aliasesGrid->GetNumberRows(); ++ii )
{
if( ii == event.GetRow() )
continue;
if( name == m_aliasesGrid->GetCellValue( ii, 0 ) )
{
m_errorMsg = wxString::Format( _( "Alias name '%s' already in use." ), name );
m_errorGrid = m_aliasesGrid;
m_errorRow = row;
event.Veto();
return;
}
}
m_aliases[ row ]->SetName( name );
}
}
void PANEL_SETUP_BUSES::OnMemberGridCellChanging( wxGridEvent& event )
{
int row = event.GetRow();
if( row >= 0 )
{
wxString name = event.GetString();
if( name.IsEmpty() )
{
m_errorMsg = _( "Member net/alias name cannot be empty." );
m_errorGrid = m_membersGrid;
m_errorRow = event.GetRow();
event.Veto();
return;
}
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ m_lastAlias ];
alias->ClearMembers();
for( int ii = 0; ii < m_membersGrid->GetNumberRows(); ++ii )
{
if( ii == row )
{
// Parse a space-separated list and add each one
wxStringTokenizer tok( name, " " );
if( tok.CountTokens() > 1 )
{
m_membersGridDirty = true;
Bind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
}
while( tok.HasMoreTokens() )
alias->AddMember( tok.GetNextToken() );
}
else
{
alias->AddMember( m_membersGrid->GetCellValue( ii, 0 ) );
}
}
}
}
void PANEL_SETUP_BUSES::OnMemberGridCellChanged( wxGridEvent& event )
{
if( event.GetRow() >= 0 && m_lastAlias >= 0
&& m_lastAlias < (int) m_aliases.size() )
{
updateAliasMembers( m_lastAlias );
}
event.Skip();
}
void PANEL_SETUP_BUSES::doReloadMembersGrid()
{
if( m_lastAlias >= 0 && m_lastAlias < m_aliasesGrid->GetNumberRows() )
{
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ m_lastAlias ];
wxString source;
wxString membersLabel;
membersLabel.Printf( m_membersLabelTemplate, m_lastAliasName );
m_source->SetLabel( source );
m_membersLabel->SetLabel( membersLabel );
m_membersGrid->ClearRows();
m_membersGrid->AppendRows( alias->Members().size() );
int ii = 0;
for( const wxString& member : alias->Members() )
m_membersGrid->SetCellValue( ii++, 0, member );
}
m_membersGridDirty = false;
}
void PANEL_SETUP_BUSES::reloadMembersGridOnIdle( wxIdleEvent& aEvent )
{
if( m_membersGridDirty )
doReloadMembersGrid();
Unbind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
}
void PANEL_SETUP_BUSES::OnSizeGrid( wxSizeEvent& event )
{
auto setColSize =
[]( WX_GRID* grid )
{
int colSize = std::max( grid->GetClientSize().x, grid->GetVisibleWidth( 0 ) );
if( grid->GetColSize( 0 ) != colSize )
grid->SetColSize( 0, colSize );
};
setColSize( m_aliasesGrid );
setColSize( m_membersGrid );
// Always propagate for a grid repaint (needed if the height changes, as well as width)
event.Skip();
}
void PANEL_SETUP_BUSES::OnUpdateUI( wxUpdateUIEvent& event )
{
// Handle a grid error. This is delayed to OnUpdateUI so that we can change focus
// even when the original validation was triggered from a killFocus event.
if( !m_errorMsg.IsEmpty() )
{
// We will re-enter this routine when the error dialog is displayed, so make
// sure we don't keep putting up more dialogs.
wxString errorMsg = m_errorMsg;
m_errorMsg = wxEmptyString;
wxWindow* topLevelParent = wxGetTopLevelParent( this );
DisplayErrorMessage( topLevelParent, errorMsg );
m_errorGrid->SetFocus();
m_errorGrid->MakeCellVisible( m_errorRow, 0 );
m_errorGrid->SetGridCursor( m_errorRow, 0 );
m_errorGrid->EnableCellEditControl( true );
m_errorGrid->ShowCellEditControl();
return;
}
if( !m_membersGrid->IsCellEditControlShown() )
{
int row = -1;
wxString aliasName;
if( m_aliasesGrid->IsCellEditControlShown() )
{
row = m_aliasesGrid->GetGridCursorRow();
wxGridCellEditor* cellEditor = m_aliasesGrid->GetCellEditor( row, 0 );
if( wxTextEntry* txt = dynamic_cast<wxTextEntry*>( cellEditor->GetControl() ) )
aliasName = txt->GetValue();
cellEditor->DecRef();
}
else if( m_aliasesGrid->GetGridCursorRow() >= 0 )
{
row = m_aliasesGrid->GetGridCursorRow();
aliasName = m_aliasesGrid->GetCellValue( row, 0 );
}
else if( m_lastAlias >= 0 && m_lastAlias < m_aliasesGrid->GetNumberRows() )
{
row = m_lastAlias;
aliasName = m_lastAliasName;
}
if( row < 0 )
{
m_membersBook->SetSelection( 1 );
}
else if( row != m_lastAlias || aliasName != m_lastAliasName )
{
m_lastAlias = row;
m_lastAliasName = aliasName;
m_membersBook->SetSelection( 0 );
m_membersBook->GetPage( 0 )->Layout();
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ row ];
alias->SetName( aliasName );
m_membersGridDirty = true;
Bind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
}
}
}
void PANEL_SETUP_BUSES::ImportSettingsFrom( const std::map<wxString, std::vector<wxString>>& aAliases )
{
m_aliases.clear();
std::vector<std::pair<wxString, std::vector<wxString>>> aliasList( aAliases.begin(),
aAliases.end() );
std::sort( aliasList.begin(), aliasList.end(),
[]( const std::pair<wxString, std::vector<wxString>>& a,
const std::pair<wxString, std::vector<wxString>>& b )
{
return a.first.CmpNoCase( b.first ) < 0;
} );
for( const auto& alias : aliasList )
{
std::shared_ptr<BUS_ALIAS> entry = std::make_shared<BUS_ALIAS>();
entry->SetName( alias.first );
entry->SetMembers( alias.second );
m_aliases.push_back( entry );
}
int ii = 0;
m_aliasesGrid->ClearRows();
m_aliasesGrid->AppendRows( m_aliases.size() );
for( const std::shared_ptr<BUS_ALIAS>& alias : m_aliases )
m_aliasesGrid->SetCellValue( ii++, 0, alias->GetName() );
m_membersBook->SetSelection( 1 );
}
void PANEL_SETUP_BUSES::updateAliasMembers( int aAliasIndex )
{
if( !m_aliases.empty() && m_membersGrid->GetNumberRows() > 0 && aAliasIndex >= 0
&& aAliasIndex < (int)m_aliases.size() )
{
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[aAliasIndex];
alias->ClearMembers();
for( int ii = 0; ii < m_membersGrid->GetNumberRows(); ++ii )
alias->AddMember( m_membersGrid->GetCellValue( ii, 0 ) );
}
}