kicad-source/eeschema/dialogs/dialog_lib_edit_pin_table.cpp
Seth Hillbrand 13249b723b Handle excessive resize requests
For unknown reasons, GTK3 may send resize events when editing grid
fields.  This can cause the grid editor to exit, losing the focus and
overwritting the characters at the next input.

We avoid this by filtering the size events when the size doesn't change.

Fixes: lp:1817810
* https://bugs.launchpad.net/kicad/+bug/1817810
2019-02-27 11:32:30 -08:00

689 lines
21 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 1992-2019 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "dialog_lib_edit_pin_table.h"
#include "lib_pin.h"
#include "pin_number.h"
#include "grid_tricks.h"
#include <widgets/grid_icon_text_helpers.h>
#include <widgets/wx_grid.h>
#include <queue>
#include <base_units.h>
#include <bitmaps.h>
#include <wx/bmpcbox.h>
#include <kiface_i.h>
#include <kicad_string.h>
#include <confirm.h>
#define PinTableShownColumnsKey wxT( "PinTableShownColumns" )
static std::vector<BITMAP_DEF> g_typeIcons;
static wxArrayString g_typeNames;
static std::vector<BITMAP_DEF> g_shapeIcons;
static wxArrayString g_shapeNames;
static std::vector<BITMAP_DEF> g_orientationIcons;
static wxArrayString g_orientationNames;
class PIN_TABLE_DATA_MODEL : public wxGridTableBase
{
private:
// Because the rows of the grid can either be a single pin or a group of pins, the
// data model is a 2D vector. If we're in the single pin case, each row's LIB_PINS
// contains only a single pin.
std::vector<LIB_PINS> m_rows;
EDA_UNITS_T m_userUnits;
bool m_edited;
public:
PIN_TABLE_DATA_MODEL( EDA_UNITS_T aUserUnits ) :
m_userUnits( aUserUnits ), m_edited( false )
{}
int GetNumberRows() override { return (int) m_rows.size(); }
int GetNumberCols() override { return COL_COUNT; }
wxString GetColLabelValue( int aCol ) override
{
switch( aCol )
{
case COL_NUMBER: return _( "Number" );
case COL_NAME: return _( "Name" );
case COL_TYPE: return _( "Electrical Type" );
case COL_SHAPE: return _( "Graphic Style" );
case COL_ORIENTATION: return _( "Orientation" );
case COL_NUMBER_SIZE: return _( "Number Text Size" );
case COL_NAME_SIZE: return _( "Name Text Size" );
case COL_LENGTH: return _( "Length" );
case COL_POSX: return _( "X Position" );
case COL_POSY: return _( "Y Position" );
default: wxFAIL; return wxEmptyString;
}
}
bool IsEmptyCell( int row, int col ) override
{
return false; // don't allow adjacent cell overflow, even if we are actually empty
}
wxString GetValue( int aRow, int aCol ) override
{
return GetValue( m_rows[ aRow ], aCol, m_userUnits );
}
static wxString GetValue( const LIB_PINS& pins, int aCol, EDA_UNITS_T aUserUnits )
{
wxString fieldValue;
if( pins.empty())
return fieldValue;
for( LIB_PIN* pin : pins )
{
wxString val;
switch( aCol )
{
case COL_NUMBER:
val = pin->GetNumber();
break;
case COL_NAME:
val = pin->GetName();
break;
case COL_TYPE:
val = g_typeNames[ pin->GetType() ];
break;
case COL_SHAPE:
val = g_shapeNames[ pin->GetShape() ];
break;
case COL_ORIENTATION:
val = g_orientationNames[ LIB_PIN::GetOrientationIndex( pin->GetOrientation() ) ];
break;
case COL_NUMBER_SIZE:
val = StringFromValue( aUserUnits, pin->GetNumberTextSize(), true, true );
break;
case COL_NAME_SIZE:
val = StringFromValue( aUserUnits, pin->GetNameTextSize(), true, true );
break;
case COL_LENGTH:
val = StringFromValue( aUserUnits, pin->GetLength(), true );
break;
case COL_POSX:
val = StringFromValue( aUserUnits, pin->GetPosition().x, true );
break;
case COL_POSY:
val = StringFromValue( aUserUnits, pin->GetPosition().y, true );
break;
default:
wxFAIL;
break;
}
if( aCol == COL_NUMBER )
{
if( fieldValue.length() )
fieldValue += wxT( ", " );
fieldValue += val;
}
else
{
if( !fieldValue.Length() )
fieldValue = val;
else if( val != fieldValue )
fieldValue = INDETERMINATE;
}
}
return fieldValue;
}
void SetValue( int aRow, int aCol, const wxString &aValue ) override
{
if( aValue == INDETERMINATE )
return;
LIB_PINS pins = m_rows[ aRow ];
for( LIB_PIN* pin : pins )
{
switch( aCol )
{
case COL_NUMBER:
pin->SetNumber( aValue );
break;
case COL_NAME:
pin->SetName( aValue );
break;
case COL_TYPE:
if( g_typeNames.Index( aValue ) != wxNOT_FOUND )
pin->SetType( (ELECTRICAL_PINTYPE) g_typeNames.Index( aValue ), false );
break;
case COL_SHAPE:
if( g_shapeNames.Index( aValue ) != wxNOT_FOUND )
pin->SetShape( (GRAPHIC_PINSHAPE) g_shapeNames.Index( aValue ) );
break;
case COL_ORIENTATION:
if( g_orientationNames.Index( aValue ) != wxNOT_FOUND )
pin->SetOrientation( LIB_PIN::GetOrientationCode(
g_orientationNames.Index( aValue ) ), false );
break;
case COL_NUMBER_SIZE:
pin->SetNumberTextSize( ValueFromString( m_userUnits, aValue, true ) );
break;
case COL_NAME_SIZE:
pin->SetNameTextSize( ValueFromString( m_userUnits, aValue, true ) );
break;
case COL_LENGTH:
pin->SetLength( ValueFromString( m_userUnits, aValue ) );
break;
case COL_POSX:
pin->SetPinPosition( wxPoint( ValueFromString( m_userUnits, aValue ),
pin->GetPosition().y ) );
break;
case COL_POSY:
pin->SetPinPosition( wxPoint( pin->GetPosition().x,
ValueFromString( m_userUnits, aValue ) ) );
break;
default:
wxFAIL;
break;
}
}
m_edited = true;
}
static int findRow( const std::vector<LIB_PINS>& aRowSet, const wxString& aName )
{
for( size_t i = 0; i < aRowSet.size(); ++i )
{
if( aRowSet[ i ][ 0 ] && aRowSet[ i ][ 0 ]->GetName() == aName )
return i;
}
return -1;
}
static bool compare( const LIB_PINS& lhs, const LIB_PINS& rhs,
int sortCol, bool ascending, EDA_UNITS_T units )
{
wxString lhStr = GetValue( lhs, sortCol, units );
wxString rhStr = GetValue( rhs, sortCol, units );
if( lhStr == rhStr )
{
// Secondary sort key is always COL_NUMBER
sortCol = COL_NUMBER;
lhStr = GetValue( lhs, sortCol, units );
rhStr = GetValue( rhs, sortCol, units );
}
bool cmp;
switch( sortCol )
{
case COL_NUMBER:
case COL_NAME:
cmp = PinNumbers::Compare( lhStr, rhStr ) < 0;
break;
case COL_NUMBER_SIZE:
case COL_NAME_SIZE:
cmp = ValueFromString( units, lhStr, true ) < ValueFromString( units, rhStr, true );
break;
case COL_LENGTH:
case COL_POSX:
case COL_POSY:
cmp = ValueFromString( units, lhStr ) < ValueFromString( units, rhStr );
break;
default:
cmp = StrNumCmp( lhStr, rhStr ) < 0;
break;
}
return ascending == cmp;
}
void RebuildRows( LIB_PINS& aPins, bool groupByName )
{
if ( GetView() )
{
// Commit any pending in-place edits before the row gets moved out from under
// the editor.
dynamic_cast<WX_GRID*>( GetView() )->CommitPendingChanges( true );
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, 0, m_rows.size() );
GetView()->ProcessTableMessage( msg );
}
m_rows.clear();
for( LIB_PIN* pin : aPins )
{
int rowIndex = -1;
if( groupByName )
rowIndex = findRow( m_rows, pin->GetName() );
if( rowIndex < 0 )
{
m_rows.emplace_back( LIB_PINS() );
rowIndex = m_rows.size() - 1;
}
m_rows[ rowIndex ].push_back( pin );
}
int sortCol = 0;
bool ascending = true;
if( GetView() && GetView()->GetSortingColumn() != wxNOT_FOUND )
{
sortCol = GetView()->GetSortingColumn();
ascending = GetView()->IsSortOrderAscending();
}
SortRows( sortCol, ascending );
if ( GetView() )
{
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, m_rows.size() );
GetView()->ProcessTableMessage( msg );
}
}
void SortRows( int aSortCol, bool ascending )
{
std::sort( m_rows.begin(), m_rows.end(),
[ aSortCol, ascending, this ]( LIB_PINS lhs, LIB_PINS rhs ) -> bool
{ return compare( lhs, rhs, aSortCol, ascending, m_userUnits ); } );
}
void AppendRow( LIB_PIN* aPin )
{
LIB_PINS row;
row.push_back( aPin );
m_rows.push_back( row );
if ( GetView() )
{
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, 1 );
GetView()->ProcessTableMessage( msg );
}
}
LIB_PINS RemoveRow( int aRow )
{
LIB_PINS removedRow = m_rows[ aRow ];
m_rows.erase( m_rows.begin() + aRow );
if ( GetView() )
{
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, aRow, 1 );
GetView()->ProcessTableMessage( msg );
}
return removedRow;
}
bool IsEdited()
{
return m_edited;
}
};
DIALOG_LIB_EDIT_PIN_TABLE::DIALOG_LIB_EDIT_PIN_TABLE( wxWindow* parent, LIB_PART* aPart ) :
DIALOG_LIB_EDIT_PIN_TABLE_BASE( parent ),
m_part( aPart )
{
m_config = Kiface().KifaceSettings();
if( g_typeNames.empty())
{
for( unsigned i = 0; i < PINTYPE_COUNT; ++i )
g_typeIcons.push_back( GetBitmap( static_cast<ELECTRICAL_PINTYPE>( i ) ) );
for( unsigned i = 0; i < PINTYPE_COUNT; ++i )
g_typeNames.push_back( GetText( static_cast<ELECTRICAL_PINTYPE>( i ) ) );
g_typeNames.push_back( INDETERMINATE );
for( unsigned i = 0; i < PINSHAPE_COUNT; ++i )
g_shapeIcons.push_back( GetBitmap( static_cast<GRAPHIC_PINSHAPE>( i ) ) );
for( unsigned i = 0; i < PINSHAPE_COUNT; ++i )
g_shapeNames.push_back( GetText( static_cast<GRAPHIC_PINSHAPE>( i ) ) );
g_shapeNames.push_back( INDETERMINATE );
for( unsigned i = 0; i < LIB_PIN::GetOrientationNames().size(); ++i )
g_orientationIcons.push_back( LIB_PIN::GetOrientationSymbols()[ i ] );
g_orientationNames = LIB_PIN::GetOrientationNames();
g_orientationNames.push_back( INDETERMINATE );
}
m_dataModel = new PIN_TABLE_DATA_MODEL( GetUserUnits() );
// Save original columns widths so we can do proportional sizing.
for( int i = 0; i < COL_COUNT; ++i )
m_originalColWidths[ i ] = m_grid->GetColSize( i );
// Give a bit more room for combobox editors
m_grid->SetDefaultRowSize( m_grid->GetDefaultRowSize() + 4 );
m_grid->SetTable( m_dataModel );
m_grid->PushEventHandler( new GRID_TRICKS( m_grid ) );
// Show/hide columns according to the user's preference
m_config->Read( PinTableShownColumnsKey, &m_columnsShown, wxT( "0 1 2 3 4 8 9" ) );
m_grid->ShowHideColumns( m_columnsShown );
// Set special attributes
wxGridCellAttr* attr;
attr = new wxGridCellAttr;
attr->SetRenderer( new GRID_CELL_ICON_TEXT_RENDERER( g_typeIcons, g_typeNames ) );
attr->SetEditor( new GRID_CELL_ICON_TEXT_POPUP( g_typeIcons, g_typeNames ) );
m_grid->SetColAttr( COL_TYPE, attr );
attr = new wxGridCellAttr;
attr->SetRenderer( new GRID_CELL_ICON_TEXT_RENDERER( g_shapeIcons, g_shapeNames ) );
attr->SetEditor( new GRID_CELL_ICON_TEXT_POPUP( g_shapeIcons, g_shapeNames ) );
m_grid->SetColAttr( COL_SHAPE, attr );
attr = new wxGridCellAttr;
attr->SetRenderer( new GRID_CELL_ICON_TEXT_RENDERER( g_orientationIcons, g_orientationNames ) );
attr->SetEditor( new GRID_CELL_ICON_TEXT_POPUP( g_orientationIcons, g_orientationNames ) );
m_grid->SetColAttr( COL_ORIENTATION, attr );
/* Right-aligned position values look much better, but only MSW and GTK2+
* currently support righ-aligned textEditCtrls, so the text jumps on all
* the other platforms when you edit it.
attr = new wxGridCellAttr;
attr->SetAlignment( wxALIGN_RIGHT, wxALIGN_TOP );
m_grid->SetColAttr( COL_POSX, attr );
attr = new wxGridCellAttr;
attr->SetAlignment( wxALIGN_RIGHT, wxALIGN_TOP );
m_grid->SetColAttr( COL_POSY, attr );
*/
m_addButton->SetBitmap( KiBitmap( small_plus_xpm ) );
m_deleteButton->SetBitmap( KiBitmap( trash_xpm ) );
m_refreshButton->SetBitmap( KiBitmap( refresh_xpm ) );
GetSizer()->SetSizeHints(this);
Centre();
m_ButtonsOK->SetDefault();
m_initialized = true;
m_modified = false;
m_width = 0;
// Connect Events
m_grid->Connect( wxEVT_GRID_COL_SORT, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE::OnColSort ), nullptr, this );
}
DIALOG_LIB_EDIT_PIN_TABLE::~DIALOG_LIB_EDIT_PIN_TABLE()
{
m_config->Write( PinTableShownColumnsKey, m_grid->GetShownColumns() );
// Disconnect Events
m_grid->Disconnect( wxEVT_GRID_COL_SORT, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE::OnColSort ), nullptr, this );
// Prevents crash bug in wxGrid's d'tor
m_grid->DestroyTable( m_dataModel );
// Delete the GRID_TRICKS.
m_grid->PopEventHandler( true );
// This is our copy of the pins. If they were transfered to the part on an OK, then
// m_pins will already be empty.
for( auto pin : m_pins )
delete pin;
}
bool DIALOG_LIB_EDIT_PIN_TABLE::TransferDataToWindow()
{
// Make a copy of the pins for editing
for( LIB_PIN* pin = m_part->GetNextPin( nullptr ); pin; pin = m_part->GetNextPin( pin ) )
m_pins.push_back( new LIB_PIN( *pin ) );
m_dataModel->RebuildRows( m_pins, m_cbGroup->GetValue() );
updateSummary();
return true;
}
bool DIALOG_LIB_EDIT_PIN_TABLE::TransferDataFromWindow()
{
if( !m_grid->CommitPendingChanges() )
return false;
// Delete the part's pins
while( LIB_PIN* pin = m_part->GetNextPin( nullptr ) )
m_part->RemoveDrawItem( pin );
// Transfer our pins to the part
for( LIB_PIN* pin : m_pins )
{
pin->SetParent( m_part );
m_part->AddDrawItem( pin );
}
m_pins.clear();
return true;
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnColSort( wxGridEvent& aEvent )
{
int sortCol = aEvent.GetCol();
bool ascending;
// This is bonkers, but wxWidgets doesn't tell us ascending/descending in the
// event, and if we ask it will give us pre-event info.
if( m_grid->IsSortingBy( sortCol ) )
// same column; invert ascending
ascending = !m_grid->IsSortOrderAscending();
else
// different column; start with ascending
ascending = true;
m_dataModel->SortRows( sortCol, ascending );
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnAddRow( wxCommandEvent& event )
{
if( !m_grid->CommitPendingChanges() )
return;
m_pins.push_back( new LIB_PIN( nullptr ) );
m_dataModel->AppendRow( m_pins[ m_pins.size() - 1 ] );
m_grid->MakeCellVisible( m_grid->GetNumberRows() - 1, 0 );
m_grid->SetGridCursor( m_grid->GetNumberRows() - 1, 0 );
m_grid->EnableCellEditControl( true );
m_grid->ShowCellEditControl();
updateSummary();
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnDeleteRow( wxCommandEvent& event )
{
if( !m_grid->CommitPendingChanges() )
return;
int curRow = m_grid->GetGridCursorRow();
if( curRow < 0 )
return;
LIB_PINS removedRow = m_dataModel->RemoveRow( curRow );
for( auto pin : removedRow )
m_pins.erase( std::find( m_pins.begin(), m_pins.end(), pin ) );
curRow = std::max( 0, curRow - 1 );
m_grid->MakeCellVisible( curRow, m_grid->GetGridCursorCol() );
m_grid->SetGridCursor( curRow, m_grid->GetGridCursorCol() );
updateSummary();
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnCellEdited( wxGridEvent& event )
{
updateSummary();
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnRebuildRows( wxCommandEvent& )
{
if( !m_grid->CommitPendingChanges() )
return;
m_dataModel->RebuildRows( m_pins, m_cbGroup->GetValue() );
adjustGridColumns( m_grid->GetRect().GetWidth() );
}
void DIALOG_LIB_EDIT_PIN_TABLE::adjustGridColumns( int aWidth )
{
m_width = aWidth;
// Account for scroll bars
aWidth -= ( m_grid->GetSize().x - m_grid->GetClientSize().x );
wxGridUpdateLocker deferRepaintsTillLeavingScope;
// The Number and Name columns must be at least wide enough to hold their contents, but
// no less wide than their original widths.
m_grid->AutoSizeColumn( COL_NUMBER );
if( m_grid->GetColSize( COL_NUMBER ) < m_originalColWidths[ COL_NUMBER ] )
m_grid->SetColSize( COL_NUMBER, m_originalColWidths[ COL_NUMBER ] );
m_grid->AutoSizeColumn( COL_NAME );
if( m_grid->GetColSize( COL_NAME ) < m_originalColWidths[ COL_NAME ] )
m_grid->SetColSize( COL_NAME, m_originalColWidths[ COL_NAME ] );
// If the grid is still wider than the columns, then stretch the Number and Name columns
// to fit.
for( int i = 0; i < COL_COUNT; ++i )
aWidth -= m_grid->GetColSize( i );
if( aWidth > 0 )
{
m_grid->SetColSize( COL_NUMBER, m_grid->GetColSize( COL_NUMBER ) + aWidth / 2 );
m_grid->SetColSize( COL_NAME, m_grid->GetColSize( COL_NAME ) + aWidth / 2 );
}
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnSize( wxSizeEvent& event )
{
auto new_size = event.GetSize().GetX();
if( m_initialized && m_width != new_size )
{
adjustGridColumns( new_size );
event.Skip();
}
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnUpdateUI( wxUpdateUIEvent& event )
{
wxString columnsShown = m_grid->GetShownColumns();
if( columnsShown != m_columnsShown )
{
m_columnsShown = columnsShown;
if( !m_grid->IsCellEditControlShown() )
adjustGridColumns( m_grid->GetRect().GetWidth() );
}
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnCancel( wxCommandEvent& event )
{
Close();
}
void DIALOG_LIB_EDIT_PIN_TABLE::OnClose( wxCloseEvent& event )
{
// This is a cancel, so commit quietly as we're going to throw the results away anyway.
m_grid->CommitPendingChanges( true );
if( m_dataModel->IsEdited() )
{
if( !HandleUnsavedChanges( this, wxEmptyString,
[&]()->bool { return TransferDataFromWindow(); } ) )
{
event.Veto();
return;
}
}
if( IsQuasiModal() )
EndQuasiModal( wxID_CANCEL );
else if( IsModal() )
EndModal( wxID_CANCEL );
else
event.Skip();
}
void DIALOG_LIB_EDIT_PIN_TABLE::updateSummary()
{
PinNumbers pinNumbers;
for( LIB_PIN* pin : m_pins )
{
if( pin->GetNumber().Length() )
pinNumbers.insert( pin->GetNumber() );
}
m_summary->SetLabel( pinNumbers.GetSummary() );
}