/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2019 CERN * 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 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 "symbol_editor_edit_tool.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for wxTextEntryDialog #include // for KiROUND #include SYMBOL_EDITOR_EDIT_TOOL::SYMBOL_EDITOR_EDIT_TOOL() : SCH_TOOL_BASE( "eeschema.SymbolEditTool" ) { } bool SYMBOL_EDITOR_EDIT_TOOL::Init() { SCH_TOOL_BASE::Init(); SYMBOL_EDITOR_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool(); SYMBOL_EDITOR_MOVE_TOOL* moveTool = m_toolMgr->GetTool(); wxASSERT_MSG( drawingTools, "eeschema.SymbolDrawing tool is not available" ); auto haveSymbolCondition = [&]( const SELECTION& sel ) { return m_isSymbolEditor && m_frame->GetCurSymbol(); }; auto canEdit = [&]( const SELECTION& sel ) { if( !m_frame->IsSymbolEditable() ) return false; if( m_frame->IsSymbolAlias() ) { for( EDA_ITEM* item : sel ) { if( item->Type() != SCH_FIELD_T ) return false; } } return true; }; const auto canCopyText = SCH_CONDITIONS::OnlyTypes( { SCH_TEXT_T, SCH_TEXTBOX_T, SCH_FIELD_T, SCH_PIN_T, SCH_TABLE_T, SCH_TABLECELL_T, } ); const auto canConvertStackedPins = [&]( const SELECTION& sel ) { // If multiple pins are selected, check they are all at same location if( sel.Size() >= 2 ) { std::vector pins; for( EDA_ITEM* item : sel ) { if( item->Type() != SCH_PIN_T ) return false; pins.push_back( static_cast( item ) ); } // Check that all pins are at the same location VECTOR2I pos = pins[0]->GetPosition(); for( size_t i = 1; i < pins.size(); ++i ) { if( pins[i]->GetPosition() != pos ) return false; } return true; } // If single pin is selected, check if there are other pins at same location if( sel.Size() == 1 && sel.Front()->Type() == SCH_PIN_T ) { SCH_PIN* selectedPin = static_cast( sel.Front() ); VECTOR2I pos = selectedPin->GetPosition(); // Get the symbol and check for other pins at same location LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); if( !symbol ) return false; int coLocatedCount = 0; for( SCH_PIN* pin : symbol->GetPins() ) { if( pin->GetPosition() == pos ) { coLocatedCount++; if( coLocatedCount >= 2 ) return true; } } } return false; }; const auto canExplodeStackedPin = [&]( const SELECTION& sel ) { if( sel.Size() != 1 || sel.Front()->Type() != SCH_PIN_T ) return false; SCH_PIN* pin = static_cast( sel.Front() ); bool isValid; std::vector stackedNumbers = pin->GetStackedPinNumbers( &isValid ); return isValid && stackedNumbers.size() > 1; }; // clang-format off // Add edit actions to the move tool menu if( moveTool ) { CONDITIONAL_MENU& moveMenu = moveTool->GetToolMenu().GetMenu(); moveMenu.AddSeparator( 200 ); moveMenu.AddItem( SCH_ACTIONS::rotateCCW, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( SCH_ACTIONS::rotateCW, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( SCH_ACTIONS::mirrorV, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( SCH_ACTIONS::mirrorH, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( SCH_ACTIONS::swap, canEdit && SELECTION_CONDITIONS::MoreThan( 1 ), 200); moveMenu.AddItem( SCH_ACTIONS::properties, canEdit && SCH_CONDITIONS::Count( 1 ), 200 ); moveMenu.AddSeparator( 300 ); moveMenu.AddItem( ACTIONS::cut, SCH_CONDITIONS::IdleSelection, 300 ); moveMenu.AddItem( ACTIONS::copy, SCH_CONDITIONS::IdleSelection, 300 ); moveMenu.AddItem( ACTIONS::copyAsText, canCopyText && SCH_CONDITIONS::IdleSelection, 300 ); moveMenu.AddItem( ACTIONS::duplicate, canEdit && SCH_CONDITIONS::NotEmpty, 300 ); moveMenu.AddItem( ACTIONS::doDelete, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); moveMenu.AddSeparator( 400 ); moveMenu.AddItem( ACTIONS::selectAll, haveSymbolCondition, 400 ); moveMenu.AddItem( ACTIONS::unselectAll, haveSymbolCondition, 400 ); } // Add editing actions to the drawing tool menu CONDITIONAL_MENU& drawMenu = drawingTools->GetToolMenu().GetMenu(); drawMenu.AddSeparator( 200 ); drawMenu.AddItem( SCH_ACTIONS::rotateCCW, canEdit && SCH_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( SCH_ACTIONS::rotateCW, canEdit && SCH_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( SCH_ACTIONS::mirrorV, canEdit && SCH_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( SCH_ACTIONS::mirrorH, canEdit && SCH_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( SCH_ACTIONS::properties, canEdit && SCH_CONDITIONS::Count( 1 ), 200 ); // Add editing actions to the selection tool menu CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu(); selToolMenu.AddItem( SCH_ACTIONS::rotateCCW, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( SCH_ACTIONS::rotateCW, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( SCH_ACTIONS::mirrorV, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( SCH_ACTIONS::mirrorH, canEdit && SCH_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( SCH_ACTIONS::swap, canEdit && SELECTION_CONDITIONS::MoreThan( 1 ), 200 ); selToolMenu.AddItem( SCH_ACTIONS::properties, canEdit && SCH_CONDITIONS::Count( 1 ), 200 ); selToolMenu.AddSeparator( 250 ); selToolMenu.AddItem( SCH_ACTIONS::convertStackedPins, canEdit && canConvertStackedPins, 250 ); selToolMenu.AddItem( SCH_ACTIONS::explodeStackedPin, canEdit && canExplodeStackedPin, 250 ); selToolMenu.AddSeparator( 300 ); selToolMenu.AddItem( ACTIONS::cut, SCH_CONDITIONS::IdleSelection, 300 ); selToolMenu.AddItem( ACTIONS::copy, SCH_CONDITIONS::IdleSelection, 300 ); selToolMenu.AddItem( ACTIONS::copyAsText, canCopyText && SCH_CONDITIONS::IdleSelection, 300 ); selToolMenu.AddItem( ACTIONS::paste, canEdit && SCH_CONDITIONS::Idle, 300 ); selToolMenu.AddItem( ACTIONS::duplicate, canEdit && SCH_CONDITIONS::NotEmpty, 300 ); selToolMenu.AddItem( ACTIONS::doDelete, canEdit && SCH_CONDITIONS::NotEmpty, 300 ); selToolMenu.AddSeparator( 400 ); selToolMenu.AddItem( ACTIONS::selectAll, haveSymbolCondition, 400 ); selToolMenu.AddItem( ACTIONS::unselectAll, haveSymbolCondition, 400 ); // clang-format on return true; } int SYMBOL_EDITOR_EDIT_TOOL::Rotate( const TOOL_EVENT& aEvent ) { SCH_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.GetSize() == 0 ) return 0; VECTOR2I rotPoint; bool ccw = ( aEvent.Matches( SCH_ACTIONS::rotateCCW.MakeEvent() ) ); SCH_ITEM* item = static_cast( selection.Front() ); SCH_COMMIT localCommit( m_toolMgr ); SCH_COMMIT* commit = dynamic_cast( aEvent.Commit() ); if( !commit ) commit = &localCommit; if( !item->IsMoving() ) commit->Modify( m_frame->GetCurSymbol(), m_frame->GetScreen(), RECURSE_MODE::RECURSE ); if( selection.GetSize() == 1 ) rotPoint = item->GetPosition(); else rotPoint = m_frame->GetNearestHalfGridPosition( selection.GetCenter() ); for( unsigned ii = 0; ii < selection.GetSize(); ii++ ) { item = static_cast( selection.GetItem( ii ) ); item->Rotate( rotPoint, ccw ); m_frame->UpdateItem( item, false, true ); } if( item->IsMoving() ) { m_toolMgr->RunAction( ACTIONS::refreshPreview ); } else { if( selection.IsHover() ) m_toolMgr->RunAction( ACTIONS::selectionClear ); if( !localCommit.Empty() ) localCommit.Push( _( "Rotate" ) ); } return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Mirror( const TOOL_EVENT& aEvent ) { SCH_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.GetSize() == 0 ) return 0; VECTOR2I mirrorPoint; bool xAxis = ( aEvent.Matches( SCH_ACTIONS::mirrorV.MakeEvent() ) ); SCH_ITEM* item = static_cast( selection.Front() ); if( !item->IsMoving() ) saveCopyInUndoList( m_frame->GetCurSymbol(), UNDO_REDO::LIBEDIT ); if( selection.GetSize() == 1 ) { mirrorPoint = item->GetPosition(); switch( item->Type() ) { case SCH_FIELD_T: { SCH_FIELD* field = static_cast( item ); if( xAxis ) field->SetVertJustify( GetFlippedAlignment( field->GetVertJustify() ) ); else field->SetHorizJustify( GetFlippedAlignment( field->GetHorizJustify() ) ); break; } default: if( xAxis ) item->MirrorVertically( mirrorPoint.y ); else item->MirrorHorizontally( mirrorPoint.x ); break; } m_frame->UpdateItem( item, false, true ); } else { mirrorPoint = m_frame->GetNearestHalfGridPosition( selection.GetCenter() ); for( unsigned ii = 0; ii < selection.GetSize(); ii++ ) { item = static_cast( selection.GetItem( ii ) ); if( xAxis ) item->MirrorVertically( mirrorPoint.y ); else item->MirrorHorizontally( mirrorPoint.x ); m_frame->UpdateItem( item, false, true ); } } if( item->IsMoving() ) { m_toolMgr->RunAction( ACTIONS::refreshPreview ); } else { if( selection.IsHover() ) m_toolMgr->RunAction( ACTIONS::selectionClear ); m_frame->OnModify(); } return 0; } const std::vector swappableItems = { LIB_SYMBOL_T, // Allows swapping the anchor SCH_PIN_T, SCH_SHAPE_T, SCH_TEXT_T, SCH_TEXTBOX_T, SCH_FIELD_T, }; int SYMBOL_EDITOR_EDIT_TOOL::Swap( const TOOL_EVENT& aEvent ) { SCH_SELECTION& selection = m_selectionTool->RequestSelection( swappableItems ); std::vector sorted = selection.GetItemsSortedBySelectionOrder(); if( selection.Size() < 2 ) return 0; EDA_ITEM* front = selection.Front(); bool isMoving = front->IsMoving(); // Save copy for undo if not in edit (edit command already handle the save copy) if( front->GetEditFlags() == 0 ) saveCopyInUndoList( front->GetParent(), UNDO_REDO::LIBEDIT ); for( size_t i = 0; i < sorted.size() - 1; i++ ) { SCH_ITEM* a = static_cast( sorted[i] ); SCH_ITEM* b = static_cast( sorted[( i + 1 ) % sorted.size()] ); VECTOR2I aPos = a->GetPosition(), bPos = b->GetPosition(); std::swap( aPos, bPos ); a->SetPosition( aPos ); b->SetPosition( bPos ); // Special case some common swaps if( a->Type() == b->Type() ) { switch( a->Type() ) { case SCH_PIN_T: { SCH_PIN* aPin = static_cast( a ); SCH_PIN* bBpin = static_cast( b ); PIN_ORIENTATION aOrient = aPin->GetOrientation(); PIN_ORIENTATION bOrient = bBpin->GetOrientation(); aPin->SetOrientation( bOrient ); bBpin->SetOrientation( aOrient ); break; } default: break; } } m_frame->UpdateItem( a, false, true ); m_frame->UpdateItem( b, false, true ); } // Update R-Tree for modified items for( EDA_ITEM* selected : selection ) updateItem( selected, true ); if( isMoving ) { m_toolMgr->PostAction( ACTIONS::refreshPreview ); } else { if( selection.IsHover() ) m_toolMgr->RunAction( ACTIONS::selectionClear ); m_frame->OnModify(); } return 0; } static std::vector nonFields = { LIB_SYMBOL_T, SCH_SHAPE_T, SCH_TEXT_T, SCH_TEXTBOX_T, SCH_PIN_T }; int SYMBOL_EDITOR_EDIT_TOOL::DoDelete( const TOOL_EVENT& aEvent ) { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); std::deque items = m_selectionTool->RequestSelection().GetItems(); SCH_COMMIT commit( m_frame ); if( items.empty() ) return 0; // Don't leave a freed pointer in the selection m_toolMgr->RunAction( ACTIONS::selectionClear ); commit.Modify( symbol, m_frame->GetScreen() ); std::set toDelete; int fieldsHidden = 0; int fieldsAlreadyHidden = 0; for( EDA_ITEM* item : items ) { if( item->Type() == SCH_PIN_T ) { SCH_PIN* curr_pin = static_cast( item ); VECTOR2I pos = curr_pin->GetPosition(); toDelete.insert( curr_pin ); // when pin editing is synchronized, pins in the same position, with the same name // in different units are also removed. But only one pin per unit (matching) if( m_frame->SynchronizePins() ) { std::vector got_unit( symbol->GetUnitCount() + 1 ); got_unit[curr_pin->GetUnit()] = true; for( SCH_PIN* pin : symbol->GetPins() ) { if( got_unit[pin->GetUnit()] ) continue; if( pin->GetPosition() != pos ) continue; if( pin->GetBodyStyle() != curr_pin->GetBodyStyle() ) continue; if( pin->GetType() != curr_pin->GetType() ) continue; if( pin->GetName() != curr_pin->GetName() ) continue; toDelete.insert( pin ); got_unit[pin->GetUnit()] = true; } } } else if( item->Type() == SCH_FIELD_T ) { SCH_FIELD* field = static_cast( item ); // Hide "deleted" fields if( field->IsVisible() ) { field->SetVisible( false ); fieldsHidden++; } else { fieldsAlreadyHidden++; } } else if( SCH_ITEM* schItem = dynamic_cast( item ) ) { toDelete.insert( schItem ); } } for( SCH_ITEM* item : toDelete ) symbol->RemoveDrawItem( item ); if( toDelete.size() == 0 ) { if( fieldsHidden == 1 ) commit.Push( _( "Hide Field" ) ); else if( fieldsHidden > 1 ) commit.Push( _( "Hide Fields" ) ); else if( fieldsAlreadyHidden > 0 ) m_frame->ShowInfoBarError( _( "Use the Symbol Properties dialog to remove fields." ) ); } else { commit.Push( _( "Delete" ) ); } m_frame->RebuildView(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Properties( const TOOL_EVENT& aEvent ) { SCH_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.Empty() || aEvent.IsAction( &SCH_ACTIONS::symbolProperties ) ) { if( m_frame->GetCurSymbol() ) editSymbolProperties(); } else if( selection.Size() == 1 ) { SCH_ITEM* item = static_cast( selection.Front() ); // Save copy for undo if not in edit (edit command already handle the save copy) if( item->GetEditFlags() == 0 ) saveCopyInUndoList( item->GetParent(), UNDO_REDO::LIBEDIT ); switch( item->Type() ) { case SCH_PIN_T: { SCH_PIN& pin = static_cast( *item ); // Mouse, not cursor, as grid points may well not be under any text const VECTOR2I& mousePos = m_toolMgr->GetMousePosition(); PIN_LAYOUT_CACHE& layout = pin.GetLayoutCache(); bool mouseOverNumber = false; if( OPT_BOX2I numberBox = layout.GetPinNumberBBox() ) { mouseOverNumber = numberBox->Contains( mousePos ); } if( SYMBOL_EDITOR_PIN_TOOL* pinTool = m_toolMgr->GetTool() ) pinTool->EditPinProperties( &pin, mouseOverNumber ); break; } case SCH_SHAPE_T: editShapeProperties( static_cast( item ) ); break; case SCH_TEXT_T: editTextProperties( item ); break; case SCH_TEXTBOX_T: editTextBoxProperties( item ); break; case SCH_FIELD_T: editFieldProperties( static_cast( item ) ); break; default: wxFAIL_MSG( wxT( "Unhandled item <" ) + item->GetClass() + wxT( ">" ) ); break; } } if( selection.IsHover() ) m_toolMgr->RunAction( ACTIONS::selectionClear ); return 0; } void SYMBOL_EDITOR_EDIT_TOOL::editShapeProperties( SCH_SHAPE* aShape ) { DIALOG_SHAPE_PROPERTIES dlg( m_frame, aShape ); if( dlg.ShowModal() != wxID_OK ) return; updateItem( aShape, true ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify(); SYMBOL_EDITOR_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool(); drawingTools->SetDrawSpecificBodyStyle( !dlg.GetApplyToAllConversions() ); drawingTools->SetDrawSpecificUnit( !dlg.GetApplyToAllUnits() ); std::vector items; aShape->GetMsgPanelInfo( m_frame, items ); m_frame->SetMsgPanel( items ); } void SYMBOL_EDITOR_EDIT_TOOL::editTextProperties( SCH_ITEM* aItem ) { if ( aItem->Type() != SCH_TEXT_T ) return; DIALOG_TEXT_PROPERTIES dlg( m_frame, static_cast( aItem ) ); if( dlg.ShowModal() != wxID_OK ) return; updateItem( aItem, true ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify( ); } void SYMBOL_EDITOR_EDIT_TOOL::editTextBoxProperties( SCH_ITEM* aItem ) { if ( aItem->Type() != SCH_TEXTBOX_T ) return; DIALOG_TEXT_PROPERTIES dlg( m_frame, static_cast( aItem ) ); if( dlg.ShowModal() != wxID_OK ) return; updateItem( aItem, true ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify( ); } void SYMBOL_EDITOR_EDIT_TOOL::editFieldProperties( SCH_FIELD* aField ) { if( aField == nullptr ) return; wxString caption; if( aField->IsMandatory() ) caption.Printf( _( "Edit %s Field" ), TitleCaps( aField->GetName() ) ); else caption.Printf( _( "Edit '%s' Field" ), aField->GetName() ); DIALOG_FIELD_PROPERTIES dlg( m_frame, caption, aField ); // The dialog may invoke a kiway player for footprint fields // so we must use a quasimodal dialog. if( dlg.ShowQuasiModal() != wxID_OK ) return; SCH_COMMIT commit( m_toolMgr ); commit.Modify( aField, m_frame->GetScreen() ); dlg.UpdateField( aField ); commit.Push( caption ); m_frame->GetCanvas()->Refresh(); m_frame->UpdateSymbolMsgPanelInfo(); } void SYMBOL_EDITOR_EDIT_TOOL::editSymbolProperties() { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); bool partLocked = symbol->UnitsLocked(); m_toolMgr->RunAction( ACTIONS::cancelInteractive ); m_toolMgr->RunAction( ACTIONS::selectionClear ); DIALOG_LIB_SYMBOL_PROPERTIES dlg( m_frame, symbol ); // This dialog itself subsequently can invoke a KIWAY_PLAYER as a quasimodal // frame. Therefore this dialog as a modal frame parent, MUST be run under // quasimodal mode for the quasimodal frame support to work. So don't use // the QUASIMODAL macros here. if( dlg.ShowQuasiModal() != wxID_OK ) return; m_frame->RebuildSymbolUnitAndBodyStyleLists(); m_frame->OnModify(); // if m_UnitSelectionLocked has changed, set some edit options or defaults // to the best value if( partLocked != symbol->UnitsLocked() ) { SYMBOL_EDITOR_DRAWING_TOOLS* tools = m_toolMgr->GetTool(); // Enable synchronized pin edit mode for symbols with interchangeable units m_frame->m_SyncPinEdit = !symbol->UnitsLocked(); // also set default edit options to the better value // Usually if units are locked, graphic items are specific to each unit // and if units are interchangeable, graphic items are common to units tools->SetDrawSpecificUnit( symbol->UnitsLocked() ); } } int SYMBOL_EDITOR_EDIT_TOOL::PinTable( const TOOL_EVENT& aEvent ) { SCH_COMMIT commit( m_frame ); LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); if( !symbol ) return 0; commit.Modify( symbol, m_frame->GetScreen() ); SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); wxCHECK( selTool, -1 ); std::vector selectedPins; SCH_SELECTION& selection = selTool->GetSelection(); for( EDA_ITEM* item : selection ) { if( item->Type() == SCH_PIN_T ) { SCH_PIN* pinItem = static_cast( item ); selectedPins.push_back( pinItem ); } } // And now clear the selection so if we change the pins we don't have dangling pointers // in the selection. m_toolMgr->RunAction( ACTIONS::selectionClear ); DIALOG_LIB_EDIT_PIN_TABLE dlg( m_frame, symbol, selectedPins ); if( dlg.ShowModal() == wxID_CANCEL ) return -1; commit.Push( _( "Edit Pins" ) ); m_frame->RebuildView(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins( const TOOL_EVENT& aEvent ) { SCH_COMMIT commit( m_frame ); LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); if( !symbol ) return 0; SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); wxCHECK( selTool, -1 ); SCH_SELECTION& selection = selTool->GetSelection(); // Collect pins to convert - accept pins with any number format std::vector pinsToConvert; if( selection.Size() == 1 && selection.Front()->Type() == SCH_PIN_T ) { // Single pin selected - find all pins at the same location SCH_PIN* selectedPin = static_cast( selection.Front() ); VECTOR2I pos = selectedPin->GetPosition(); for( SCH_PIN* pin : symbol->GetPins() ) { if( pin->GetPosition() == pos ) pinsToConvert.push_back( pin ); } } else { // Multiple pins selected - use them directly, accepting any pin numbers for( EDA_ITEM* item : selection ) { if( item->Type() == SCH_PIN_T ) pinsToConvert.push_back( static_cast( item ) ); } } if( pinsToConvert.size() < 2 ) { m_frame->ShowInfoBarError( _( "At least two pins are needed to convert to stacked pins" ) ); return 0; } // Check that all pins are at the same location VECTOR2I pos = pinsToConvert[0]->GetPosition(); for( size_t i = 1; i < pinsToConvert.size(); ++i ) { if( pinsToConvert[i]->GetPosition() != pos ) { m_frame->ShowInfoBarError( _( "All pins must be at the same location" ) ); return 0; } } commit.Modify( symbol, m_frame->GetScreen() ); // Clear selection before modifying pins, like the Delete command does m_toolMgr->RunAction( ACTIONS::selectionClear ); // Sort pins for consistent ordering - handle arbitrary pin number formats std::sort( pinsToConvert.begin(), pinsToConvert.end(), []( SCH_PIN* a, SCH_PIN* b ) { wxString numA = a->GetNumber(); wxString numB = b->GetNumber(); // Try to convert to integers for proper numeric sorting long longA, longB; bool aIsNumeric = numA.ToLong( &longA ); bool bIsNumeric = numB.ToLong( &longB ); // Both are purely numeric - sort numerically if( aIsNumeric && bIsNumeric ) return longA < longB; // Mixed numeric/non-numeric - numeric pins come first if( aIsNumeric && !bIsNumeric ) return true; if( !aIsNumeric && bIsNumeric ) return false; // Both non-numeric or mixed alphanumeric - use lexicographic sorting return numA < numB; }); // Build the stacked notation string with range collapsing wxString stackedNotation = wxT("["); // Helper function to collapse consecutive numbers into ranges - handles arbitrary pin formats auto collapseRanges = [&]() -> wxString { if( pinsToConvert.empty() ) return wxT(""); wxString result; // Group pins by their alphanumeric prefix for range collapsing std::map> prefixGroups; std::vector nonNumericPins; // Parse each pin number to separate prefix from numeric suffix for( SCH_PIN* pin : pinsToConvert ) { wxString pinNumber = pin->GetNumber(); // Skip empty pin numbers (shouldn't happen, but be defensive) if( pinNumber.IsEmpty() ) { nonNumericPins.push_back( wxT("(empty)") ); continue; } wxString prefix; wxString numericPart; // Find where numeric part starts (scan from end) size_t numStart = pinNumber.length(); for( int i = pinNumber.length() - 1; i >= 0; i-- ) { if( !wxIsdigit( pinNumber[i] ) ) { numStart = i + 1; break; } if( i == 0 ) // All digits numStart = 0; } if( numStart < pinNumber.length() ) // Has numeric suffix { prefix = pinNumber.Left( numStart ); numericPart = pinNumber.Mid( numStart ); long numValue; if( numericPart.ToLong( &numValue ) && numValue >= 0 ) // Valid non-negative number { prefixGroups[prefix].push_back( numValue ); } else { // Numeric part couldn't be parsed or is negative - treat as non-numeric nonNumericPins.push_back( pinNumber ); } } else // No numeric suffix - consolidate as individual value { nonNumericPins.push_back( pinNumber ); } } // Process each prefix group for( auto& [prefix, numbers] : prefixGroups ) { if( !result.IsEmpty() ) result += wxT(","); // Sort numeric values for this prefix std::sort( numbers.begin(), numbers.end() ); // Collapse consecutive ranges within this prefix size_t i = 0; while( i < numbers.size() ) { if( i > 0 ) // Not first number in this prefix group result += wxT(","); long start = numbers[i]; long end = start; // Find the end of consecutive sequence while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 ) { i++; end = numbers[i]; } // Add range or single number with prefix if( end > start + 1 ) // Range of 3+ numbers result += wxString::Format( wxT("%s%ld-%s%ld"), prefix, start, prefix, end ); else if( end == start + 1 ) // Two consecutive numbers result += wxString::Format( wxT("%s%ld,%s%ld"), prefix, start, prefix, end ); else // Single number result += wxString::Format( wxT("%s%ld"), prefix, start ); i++; } } // Add non-numeric pin numbers as individual comma-separated values for( const wxString& nonNum : nonNumericPins ) { if( !result.IsEmpty() ) result += wxT(","); result += nonNum; } return result; }; stackedNotation += collapseRanges(); stackedNotation += wxT("]"); // Keep the first pin and give it the stacked notation SCH_PIN* masterPin = pinsToConvert[0]; masterPin->SetNumber( stackedNotation ); // Log information about pins being removed before we remove them wxLogTrace( "KICAD_STACKED_PINS", wxString::Format( "Converting %zu pins to stacked notation '%s'", pinsToConvert.size(), stackedNotation ) ); // Remove all other pins from the symbol that were consolidated into the stacked notation // Collect pins to remove first, then remove them all at once like the Delete command std::vector pinsToRemove; for( size_t i = 1; i < pinsToConvert.size(); ++i ) { SCH_PIN* pinToRemove = pinsToConvert[i]; // Log the pin before removing it wxLogTrace( "KICAD_STACKED_PINS", wxString::Format( "Will remove pin '%s' at position (%d, %d)", pinToRemove->GetNumber(), pinToRemove->GetPosition().x, pinToRemove->GetPosition().y ) ); pinsToRemove.push_back( pinToRemove ); } // Remove all pins at once, like the Delete command does for( SCH_PIN* pin : pinsToRemove ) { symbol->RemoveDrawItem( pin ); } commit.Push( wxString::Format( _( "Convert %zu Stacked Pins to '%s'" ), pinsToConvert.size(), stackedNotation ) ); m_frame->RebuildView(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin( const TOOL_EVENT& aEvent ) { SCH_COMMIT commit( m_frame ); LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); if( !symbol ) return 0; SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); wxCHECK( selTool, -1 ); SCH_SELECTION& selection = selTool->GetSelection(); if( selection.GetSize() != 1 || selection.Front()->Type() != SCH_PIN_T ) { m_frame->ShowInfoBarError( _( "Select a single pin with stacked notation to explode" ) ); return 0; } SCH_PIN* pin = static_cast( selection.Front() ); // Check if the pin has stacked notation bool isValid; std::vector stackedNumbers = pin->GetStackedPinNumbers( &isValid ); if( !isValid || stackedNumbers.size() <= 1 ) { m_frame->ShowInfoBarError( _( "Selected pin does not have valid stacked notation" ) ); return 0; } commit.Modify( symbol, m_frame->GetScreen() ); // Clear selection before modifying pins m_toolMgr->RunAction( ACTIONS::selectionClear ); // Sort the stacked numbers to find the smallest one std::sort( stackedNumbers.begin(), stackedNumbers.end(), []( const wxString& a, const wxString& b ) { // Try to convert to integers for proper numeric sorting long numA, numB; if( a.ToLong( &numA ) && b.ToLong( &numB ) ) return numA < numB; // Fall back to string comparison if not numeric return a < b; }); // Change the original pin to use the first (smallest) number and make it visible pin->SetNumber( stackedNumbers[0] ); pin->SetVisible( true ); // Create additional pins for the remaining numbers and make them invisible for( size_t i = 1; i < stackedNumbers.size(); ++i ) { SCH_PIN* newPin = new SCH_PIN( symbol ); // Copy all properties from the original pin newPin->SetPosition( pin->GetPosition() ); newPin->SetOrientation( pin->GetOrientation() ); newPin->SetShape( pin->GetShape() ); newPin->SetLength( pin->GetLength() ); newPin->SetType( pin->GetType() ); newPin->SetName( pin->GetName() ); newPin->SetNumber( stackedNumbers[i] ); newPin->SetNameTextSize( pin->GetNameTextSize() ); newPin->SetNumberTextSize( pin->GetNumberTextSize() ); newPin->SetUnit( pin->GetUnit() ); newPin->SetBodyStyle( pin->GetBodyStyle() ); newPin->SetVisible( false ); // Make all other pins invisible // Add the new pin to the symbol symbol->AddDrawItem( newPin ); } commit.Push( _( "Explode Stacked Pin" ) ); m_frame->RebuildView(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields( const TOOL_EVENT& aEvent ) { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); if( !symbol ) return 0; if( !symbol->IsDerived() ) { m_frame->ShowInfoBarError( _( "Symbol is not derived from another symbol." ) ); } else { DIALOG_UPDATE_SYMBOL_FIELDS dlg( m_frame, symbol ); if( dlg.ShowModal() == wxID_CANCEL ) return -1; } return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Undo( const TOOL_EVENT& aEvent ) { SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); // Nuke the selection for later rebuilding. This does *not* clear the flags on any items; // it just clears the SELECTION's reference to them. selTool->GetSelection().Clear(); { m_frame->GetSymbolFromUndoList(); } selTool->RebuildSelection(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Redo( const TOOL_EVENT& aEvent ) { SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); // Nuke the selection for later rebuilding. This does *not* clear the flags on any items; // it just clears the SELECTION's reference to them. selTool->GetSelection().Clear(); { m_frame->GetSymbolFromRedoList(); } selTool->RebuildSelection(); return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Cut( const TOOL_EVENT& aEvent ) { int retVal = Copy( aEvent ); if( retVal == 0 ) retVal = DoDelete( aEvent ); return retVal; } int SYMBOL_EDITOR_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent ) { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); SCH_SELECTION& selection = m_selectionTool->RequestSelection( nonFields ); if( !symbol || !selection.GetSize() ) return 0; for( SCH_ITEM& item : symbol->GetDrawItems() ) { if( item.Type() == SCH_FIELD_T ) continue; wxASSERT( !item.HasFlag( STRUCT_DELETED ) ); if( !item.IsSelected() ) item.SetFlags( STRUCT_DELETED ); } LIB_SYMBOL* partCopy = new LIB_SYMBOL( *symbol ); STRING_FORMATTER formatter; SCH_IO_KICAD_SEXPR::FormatLibSymbol( partCopy, formatter ); delete partCopy; for( SCH_ITEM& item : symbol->GetDrawItems() ) item.ClearFlags( STRUCT_DELETED ); std::string prettyData = formatter.GetString(); KICAD_FORMAT::Prettify( prettyData, true ); if( SaveClipboard( prettyData ) ) return 0; else return -1; } int SYMBOL_EDITOR_EDIT_TOOL::CopyAsText( const TOOL_EVENT& aEvent ) { SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); SCH_SELECTION& selection = selTool->RequestSelection(); if( selection.Empty() ) return 0; wxString itemsAsText = GetSelectedItemsAsText( selection ); if( selection.IsHover() ) m_toolMgr->RunAction( ACTIONS::selectionClear ); return SaveClipboard( itemsAsText.ToStdString() ); } int SYMBOL_EDITOR_EDIT_TOOL::Paste( const TOOL_EVENT& aEvent ) { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); LIB_SYMBOL* newPart = nullptr; if( !symbol || symbol->IsDerived() ) return 0; std::string clipboardData = GetClipboardUTF8(); try { std::vector newParts = SCH_IO_KICAD_SEXPR::ParseLibSymbols( clipboardData, "Clipboard" ); if( newParts.empty() || !newParts[0] ) return -1; newPart = newParts[0]; } catch( IO_ERROR& ) { // If it's not a symbol then paste as text newPart = new LIB_SYMBOL( "dummy_part" ); wxString pasteText( clipboardData ); // Limit of 5000 is totally arbitrary. Without a limit, pasting a bitmap image from // eeschema makes KiCad appear to hang. if( pasteText.Length() > 5000 ) pasteText = pasteText.Left( 5000 ) + wxT( "..." ); SCH_TEXT* newText = new SCH_TEXT( { 0, 0 }, pasteText, LAYER_DEVICE ); newPart->AddDrawItem( newText ); } SCH_COMMIT commit( m_toolMgr ); commit.Modify( symbol, m_frame->GetScreen() ); m_selectionTool->ClearSelection(); for( SCH_ITEM& item : symbol->GetDrawItems() ) item.ClearFlags( IS_NEW | IS_PASTED | SELECTED ); for( SCH_ITEM& item : newPart->GetDrawItems() ) { if( item.Type() == SCH_FIELD_T ) continue; SCH_ITEM* newItem = item.Duplicate( true, &commit ); newItem->SetParent( symbol ); newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetUnit( newItem->GetUnit() ? m_frame->GetUnit() : 0 ); newItem->SetBodyStyle( newItem->GetBodyStyle() ? m_frame->GetBodyStyle() : 0 ); symbol->AddDrawItem( newItem ); getView()->Add( newItem ); } delete newPart; m_selectionTool->RebuildSelection(); SCH_SELECTION& selection = m_selectionTool->GetSelection(); if( !selection.Empty() ) { selection.SetReferencePoint( getViewControls()->GetCursorPosition( true ) ); if( m_toolMgr->RunSynchronousAction( SCH_ACTIONS::move, &commit ) ) commit.Push( _( "Paste" ) ); else commit.Revert(); } return 0; } int SYMBOL_EDITOR_EDIT_TOOL::Duplicate( const TOOL_EVENT& aEvent ) { LIB_SYMBOL* symbol = m_frame->GetCurSymbol(); SCH_SELECTION& selection = m_selectionTool->RequestSelection( nonFields ); SCH_COMMIT commit( m_toolMgr ); if( selection.GetSize() == 0 ) return 0; commit.Modify( symbol, m_frame->GetScreen() ); std::vector oldItems; std::vector newItems; std::copy( selection.begin(), selection.end(), std::back_inserter( oldItems ) ); std::sort( oldItems.begin(), oldItems.end(), []( EDA_ITEM* a, EDA_ITEM* b ) { int cmp; if( a->Type() != b->Type() ) return a->Type() < b->Type(); // Create the new pins in the same order as the old pins if( a->Type() == SCH_PIN_T ) { const wxString& aNum = static_cast( a )->GetNumber(); const wxString& bNum = static_cast( b )->GetNumber(); cmp = StrNumCmp( aNum, bNum ); // If the pin numbers are not numeric, then just number them by their position // on the screen. if( aNum.IsNumber() && bNum.IsNumber() && cmp != 0 ) return cmp < 0; } cmp = LexicographicalCompare( a->GetPosition(), b->GetPosition() ); if( cmp != 0 ) return cmp < 0; return a->m_Uuid < b->m_Uuid; } ); for( EDA_ITEM* item : oldItems ) { SCH_ITEM* oldItem = static_cast( item ); SCH_ITEM* newItem = oldItem->Duplicate( true, &commit ); if( newItem->Type() == SCH_PIN_T ) { SCH_PIN* newPin = static_cast( newItem ); if( !newPin->GetNumber().IsEmpty() ) newPin->SetNumber( wxString::Format( wxT( "%i" ), symbol->GetMaxPinNumber() + 1 ) ); } oldItem->ClearFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetParent( symbol ); newItems.push_back( newItem ); symbol->AddDrawItem( newItem ); getView()->Add( newItem ); } m_toolMgr->RunAction( ACTIONS::selectionClear ); m_toolMgr->RunAction( ACTIONS::selectItems, &newItems ); selection.SetReferencePoint( getViewControls()->GetCursorPosition( true ) ); if( m_toolMgr->RunSynchronousAction( SCH_ACTIONS::move, &commit ) ) commit.Push( _( "Duplicate" ) ); else commit.Revert(); return 0; } void SYMBOL_EDITOR_EDIT_TOOL::setTransitions() { // clang-format off Go( &SYMBOL_EDITOR_EDIT_TOOL::Undo, ACTIONS::undo.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Redo, ACTIONS::redo.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Cut, ACTIONS::cut.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Copy, ACTIONS::copy.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::CopyAsText, ACTIONS::copyAsText.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Paste, ACTIONS::paste.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Duplicate, ACTIONS::duplicate.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Rotate, SCH_ACTIONS::rotateCW.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Rotate, SCH_ACTIONS::rotateCCW.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Mirror, SCH_ACTIONS::mirrorV.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Mirror, SCH_ACTIONS::mirrorH.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Swap, SCH_ACTIONS::swap.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::DoDelete, ACTIONS::doDelete.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::InteractiveDelete, ACTIONS::deleteTool.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment, ACTIONS::increment.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment, ACTIONS::incrementPrimary.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment, ACTIONS::decrementPrimary.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment, ACTIONS::incrementSecondary.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment, ACTIONS::decrementSecondary.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties, SCH_ACTIONS::properties.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties, SCH_ACTIONS::symbolProperties.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::PinTable, SCH_ACTIONS::pinTable.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins, SCH_ACTIONS::convertStackedPins.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin, SCH_ACTIONS::explodeStackedPin.MakeEvent() ); Go( &SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields, SCH_ACTIONS::updateSymbolFields.MakeEvent() ); // clang-format on }