Files
kicad-source-mirror/common/lib_table_grid_tricks.cpp
Jeff Young 4d37d50f57 Handle visibility flag where it is supported.
(This got lost when merging separate routines into
shared code.)

Fixes https://gitlab.com/kicad/code/kicad/-/issues/23078
2026-02-16 17:34:40 +00:00

534 lines
18 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.
*
* 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 "lib_table_grid_tricks.h"
#include "confirm.h"
#include "lib_table_grid_data_model.h"
#include <libraries/library_manager.h>
#include <wx/clipbrd.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <lib_id.h>
LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid ) :
GRID_TRICKS( aGrid )
{
m_grid->Disconnect( wxEVT_CHAR_HOOK );
m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}
LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid,
std::function<void( wxCommandEvent& )> aAddHandler ) :
GRID_TRICKS( aGrid, aAddHandler )
{
m_grid->Disconnect( wxEVT_CHAR_HOOK );
m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}
void LIB_TABLE_GRID_TRICKS::onGridCellLeftClick( wxGridEvent& aEvent )
{
if( aEvent.GetCol() == COL_STATUS )
{
// Status column button action depends on row:
// Normal rows should have no button, so they are a no-op
// Errored rows should have the warning button, so we show their error
// Configurable libraries will have the options button, so we launch the config
// Chained tables will have the open button, so we request the table be opened
LIB_TABLE_GRID_DATA_MODEL* table = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );
const LIBRARY_TABLE_ROW& row = table->at( aEvent.GetRow() );
LIBRARY_MANAGER_ADAPTER* adapter = table->Adapter();
wxString title = row.Type() == "Table"
? wxString::Format( _( "Error loading library table '%s'" ), row.Nickname() )
: wxString::Format( _( "Error loading library '%s'" ), row.Nickname() );
if( !row.IsOk() )
{
DisplayErrorMessage( m_grid, title, row.ErrorDescription() );
}
else if( std::optional<LIBRARY_ERROR> e = adapter->LibraryError( row.Nickname() ) )
{
DisplayErrorMessage( m_grid, title, e->message );
}
else if( adapter->SupportsConfigurationDialog( row.Nickname() ) )
{
adapter->ShowConfigurationDialog( row.Nickname(), wxGetTopLevelParent( m_grid ) );
}
else if( row.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
{
openTable( row );
}
aEvent.Skip();
}
else
{
GRID_TRICKS::onGridCellLeftClick( aEvent );
}
}
void LIB_TABLE_GRID_TRICKS::showPopupMenu( wxMenu& menu, wxGridEvent& aEvent )
{
// Ensure selection parameters are up to date
getSelectedArea();
LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );
const LIBRARY_TABLE_ROW& firstRow = tbl->at( m_sel_row_start );
bool showSettings = false;
bool showOpen = false;
if( LIBRARY_MANAGER_ADAPTER* adapter = tbl->Adapter() )
{
wxString nickname = tbl->GetValue( m_sel_row_start, COL_NICKNAME );
if( m_sel_row_count == 1 && adapter->SupportsConfigurationDialog( nickname ) )
{
showSettings = true;
menu.Append( LIB_TABLE_GRID_TRICKS_LIBRARY_SETTINGS, _( "Edit Settings..." ) );
}
}
if( firstRow.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
{
showOpen = true;
menu.Append( LIB_TABLE_GRID_TRICKS_OPEN_TABLE, _( "Open Library Table" ) );
}
if( showSettings || showOpen )
menu.AppendSeparator();
bool showActivate = false;
bool showDeactivate = false;
bool showSetVisible = false;
bool showUnsetVisible = false;
bool showOptions = false;
for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
{
if( tbl->GetValueAsBool( row, 0 ) )
showDeactivate = true;
else
showActivate = true;
if( tbl->GetValueAsBool( row, 1 ) )
showUnsetVisible = true;
else
showSetVisible = true;
if( showActivate && showDeactivate && showSetVisible && showUnsetVisible )
break;
}
if( showActivate )
menu.Append( LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED, _( "Activate Selected" ) );
if( showDeactivate )
menu.Append( LIB_TABLE_GRID_TRICKS_DEACTIVATE_SELECTED, _( "Deactivate Selected" ) );
if( supportsVisibilityColumn() )
{
if( showSetVisible )
menu.Append( LIB_TABLE_GRID_TRICKS_SET_VISIBLE, _( "Set Visible Flag" ) );
if( showUnsetVisible )
menu.Append( LIB_TABLE_GRID_TRICKS_UNSET_VISIBLE, _( "Unset Visible Flag" ) );
}
if( m_sel_row_count == 1 && firstRow.Type() != LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
{
showOptions = true;
menu.Append( LIB_TABLE_GRID_TRICKS_OPTIONS_EDITOR, _( "Edit Options..." ),
_( "Edit options for this library entry" ) );
}
if( showActivate || showDeactivate || showSetVisible || showUnsetVisible || showOptions )
menu.AppendSeparator();
GRID_TRICKS::showPopupMenu( menu, aEvent );
}
void LIB_TABLE_GRID_TRICKS::doPopupSelection( wxCommandEvent& event )
{
int menu_id = event.GetId();
LIB_TABLE_GRID_DATA_MODEL* tbl = (LIB_TABLE_GRID_DATA_MODEL*) m_grid->GetTable();
if( menu_id == LIB_TABLE_GRID_TRICKS_OPTIONS_EDITOR )
{
optionsEditor( m_grid->GetGridCursorRow() );
}
else if( menu_id == LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED
|| menu_id == LIB_TABLE_GRID_TRICKS_DEACTIVATE_SELECTED )
{
bool selected_state = menu_id == LIB_TABLE_GRID_TRICKS_ACTIVATE_SELECTED;
for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
tbl->SetValueAsBool( row, 0, selected_state );
// Ensure the new state (on/off) of the widgets is immediately shown:
m_grid->Refresh();
}
else if( menu_id == LIB_TABLE_GRID_TRICKS_SET_VISIBLE
|| menu_id == LIB_TABLE_GRID_TRICKS_UNSET_VISIBLE )
{
bool selected_state = menu_id == LIB_TABLE_GRID_TRICKS_SET_VISIBLE;
for( int row = m_sel_row_start; row < m_sel_row_start + m_sel_row_count; ++row )
tbl->SetValueAsBool( row, 1, selected_state );
// Ensure the new state (on/off) of the widgets is immediately shown:
m_grid->Refresh();
}
else if( menu_id == LIB_TABLE_GRID_TRICKS_LIBRARY_SETTINGS )
{
LIBRARY_MANAGER_ADAPTER* adapter = tbl->Adapter();
LIBRARY_TABLE_ROW& row = tbl->At( m_sel_row_start );
adapter->ShowConfigurationDialog( row.Nickname(), wxGetTopLevelParent( m_grid ) );
}
else if( menu_id == LIB_TABLE_GRID_TRICKS_OPEN_TABLE )
{
openTable( tbl->At( m_sel_row_start ) );
}
else
{
GRID_TRICKS::doPopupSelection( event );
}
}
void LIB_TABLE_GRID_TRICKS::onCharHook( wxKeyEvent& ev )
{
if( ev.GetModifiers() == wxMOD_CONTROL && ev.GetKeyCode() == 'V' && m_grid->IsCellEditControlShown() )
{
wxLogNull doNotLog;
if( wxTheClipboard->Open() )
{
if( wxTheClipboard->IsSupported( wxDF_TEXT ) || wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
{
wxTextDataObject data;
wxTheClipboard->GetData( data );
wxString text = data.GetText();
if( !text.Contains( '\t' ) && text.Contains( ',' ) )
text.Replace( ',', '\t' );
if( text.Contains( '\t' ) || text.Contains( '\n' ) || text.Contains( '\r' ) )
{
m_grid->CancelPendingChanges();
int row = m_grid->GetGridCursorRow();
// Check if the current row already has data (has a nickname)
wxGridTableBase* table = m_grid->GetTable();
if( table && row >= 0 && row < table->GetNumberRows() )
{
// Check if the row has a nickname (indicating it has existing data)
wxString nickname = table->GetValue( row, COL_NICKNAME );
if( !nickname.IsEmpty() )
{
// Row already has data, don't allow pasting over it
wxTheClipboard->Close();
wxBell(); // Provide audio feedback
return;
}
}
m_grid->ClearSelection();
m_grid->SelectRow( row );
m_grid->SetGridCursor( row, 0 );
getSelectedArea();
paste_text( text );
wxTheClipboard->Close();
m_grid->ForceRefresh();
return;
}
}
wxTheClipboard->Close();
}
}
GRID_TRICKS::onCharHook( ev );
}
/*
* Handle specialized clipboard text, either s-expr syntax starting with a lib table preamble
* (such as "(fp_lib_table"), or spreadsheet formatted text.
*/
void LIB_TABLE_GRID_TRICKS::paste_text( const wxString& cb_text )
{
LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );
if( size_t ndx = cb_text.find( getTablePreamble() ); ndx != std::string::npos )
{
// paste the LIB_TABLE_ROWs of s-expr, starting at column 0 regardless of current cursor column.
if( LIBRARY_TABLE tempTable( true, cb_text, tbl->Table().Scope() ); tempTable.IsOk() )
{
std::ranges::copy( tempTable.Rows(),
std::inserter( tbl->Table().Rows(), tbl->Table().Rows().begin() ) );
if( tbl->GetView() )
{
wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, 0, 0 );
tbl->GetView()->ProcessTableMessage( msg );
}
}
else
{
DisplayError( wxGetTopLevelParent( m_grid ), tempTable.ErrorDescription() );
}
}
else
{
wxString text = cb_text;
if( !text.Contains( '\t' ) && text.Contains( ',' ) )
text.Replace( ',', '\t' );
if( text.Contains( '\t' ) )
{
int row = m_grid->GetGridCursorRow();
m_grid->ClearSelection();
m_grid->SelectRow( row );
m_grid->SetGridCursor( row, 0 );
getSelectedArea();
}
GRID_TRICKS::paste_text( text );
m_grid->AutoSizeColumns( false );
}
m_grid->AutoSizeColumns( false );
}
bool LIB_TABLE_GRID_TRICKS::handleDoubleClick( wxGridEvent& aEvent )
{
if( aEvent.GetCol() == COL_OPTIONS )
{
optionsEditor( aEvent.GetRow() );
return true;
}
return false;
}
void LIB_TABLE_GRID_TRICKS::AppendRowHandler( WX_GRID* aGrid )
{
aGrid->OnAddRow(
[&]() -> std::pair<int, int>
{
aGrid->AppendRows( 1 );
return { aGrid->GetNumberRows() - 1, COL_NICKNAME };
} );
}
void LIB_TABLE_GRID_TRICKS::DeleteRowHandler( WX_GRID* aGrid )
{
if( !aGrid->CommitPendingChanges() )
return;
wxGridUpdateLocker noUpdates( aGrid );
int curRow = aGrid->GetGridCursorRow();
int curCol = aGrid->GetGridCursorCol();
// In a wxGrid, collect rows that have a selected cell, or are selected
// It is not so easy: it depends on the way the selection was made.
// Here, we collect rows selected by clicking on a row label, and rows that contain
// previously-selected cells.
// If no candidate, just delete the row with the grid cursor.
wxArrayInt selectedRows = aGrid->GetSelectedRows();
wxGridCellCoordsArray cells = aGrid->GetSelectedCells();
wxGridCellCoordsArray blockTopLeft = aGrid->GetSelectionBlockTopLeft();
wxGridCellCoordsArray blockBotRight = aGrid->GetSelectionBlockBottomRight();
// Add all row having cell selected to list:
for( unsigned ii = 0; ii < cells.GetCount(); ii++ )
selectedRows.Add( cells[ii].GetRow() );
// Handle block selection
if( !blockTopLeft.IsEmpty() && !blockBotRight.IsEmpty() )
{
for( int i = blockTopLeft[0].GetRow(); i <= blockBotRight[0].GetRow(); ++i )
selectedRows.Add( i );
}
// Use the row having the grid cursor only if we have no candidate:
if( selectedRows.size() == 0 && aGrid->GetGridCursorRow() >= 0 )
selectedRows.Add( aGrid->GetGridCursorRow() );
if( selectedRows.size() == 0 )
{
wxBell();
return;
}
std::sort( selectedRows.begin(), selectedRows.end() );
// Remove selected rows (note: a row can be stored more than once in list)
int last_row = -1;
// Needed to avoid a wxWidgets alert if the row to delete is the last row
// at least on wxMSW 3.2
aGrid->ClearSelection();
for( int ii = selectedRows.GetCount()-1; ii >= 0; ii-- )
{
int row = selectedRows[ii];
if( row != last_row )
{
last_row = row;
aGrid->DeleteRows( row, 1 );
}
}
if( aGrid->GetNumberRows() > 0 && curRow >= 0 )
aGrid->SetGridCursor( std::min( curRow, aGrid->GetNumberRows() - 1 ), curCol );
}
void LIB_TABLE_GRID_TRICKS::MoveUpHandler( WX_GRID* aGrid )
{
aGrid->OnMoveRowUp(
[&]( int row )
{
LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
int curRow = aGrid->GetGridCursorRow();
std::vector<LIBRARY_TABLE_ROW>& rows = tbl->Table().Rows();
auto current = rows.begin() + curRow;
auto prev = rows.begin() + curRow - 1;
std::iter_swap( current, prev );
// Update the wxGrid
wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, row - 1, 0 );
tbl->GetView()->ProcessTableMessage( msg );
} );
}
void LIB_TABLE_GRID_TRICKS::MoveDownHandler( WX_GRID* aGrid )
{
aGrid->OnMoveRowDown(
[&]( int row )
{
LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
int curRow = aGrid->GetGridCursorRow();
std::vector<LIBRARY_TABLE_ROW>& rows = tbl->Table().Rows();
auto current = rows.begin() + curRow;
auto next = rows.begin() + curRow + 1;
std::iter_swap( current, next );
// Update the wxGrid
wxGridTableMessage msg( tbl, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, row, 0 );
tbl->GetView()->ProcessTableMessage( msg );
} );
}
bool LIB_TABLE_GRID_TRICKS::VerifyTable( WX_GRID* aGrid, bool aSupportsVisibilityColumn,
std::function<void( int aRow, int aCol )> aErrorHandler )
{
wxWindow* topLevelParent = wxGetTopLevelParent( aGrid );
LIB_TABLE_GRID_DATA_MODEL* model = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
wxString msg;
for( int r = 0; r < model->GetNumberRows(); )
{
wxString nick = model->GetValue( r, COL_NICKNAME ).Trim( false ).Trim();
wxString uri = model->GetValue( r, COL_URI ).Trim( false ).Trim();
unsigned illegalCh = 0;
if( !uri )
{
// Silently nuke rows that have no libraray URI
model->DeleteRows( r, 1 );
}
else if( !nick || ( illegalCh = LIB_ID::FindIllegalLibraryNameChar( nick ) ) )
{
if( !nick )
msg = _( "Library must have a nickname." );
else
msg = wxString::Format( _( "Illegal character '%c' in nickname '%s'." ), illegalCh, nick );
aErrorHandler( r, COL_NICKNAME );
KICAD_MESSAGE_DIALOG errdlg( topLevelParent, msg, _( "Library Nickname Error" ) );
errdlg.ShowModal();
return false;
}
else
{
// set the trimmed values back into the table so they get saved to disk.
model->SetValue( r, COL_NICKNAME, nick );
model->SetValue( r, COL_URI, uri );
if( !aSupportsVisibilityColumn )
{
// Make sure to not save an inappropriate hidden flag
model->SetValue( r, COL_VISIBLE, wxS( "1" ) );
}
++r; // this row was OK.
}
}
// check for duplicate nickNames
for( int r1 = 0; r1 < model->GetNumberRows() - 1; ++r1 )
{
wxString nick1 = model->GetValue( r1, COL_NICKNAME );
for( int r2 = r1 + 1; r2 < model->GetNumberRows(); ++r2 )
{
wxString nick2 = model->GetValue( r2, COL_NICKNAME );
if( nick1 == nick2 )
{
msg = wxString::Format( _( "Multiple libraries cannot share the same nickname ('%s')." ), nick1 );
// go to the lower of the two rows, it is technically the duplicate:
aErrorHandler( r2, 1 );
KICAD_MESSAGE_DIALOG errdlg( topLevelParent, msg, _( "Library Nickname Error" ) );
errdlg.ShowModal();
return false;
}
}
}
return true;
}