Files
kicad-source-mirror/eeschema/tools/symbol_editor_edit_tool.cpp
Seth Hillbrand cd65579faf Demote hidden power input pins to passive when exploding stacked pins
Hidden power input pins act as global labels in KiCad's ERC, creating
unintended power nets. When ExplodeStackedPin creates hidden copies of
a PT_POWER_IN pin, the copies are now set to PT_PASSIVE instead.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/23021
2026-02-13 15:20:14 -08:00

1861 lines
60 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <tool/picker_tool.h>
#include <tools/sch_selection_tool.h>
#include <tools/sch_tool_utils.h>
#include <tools/symbol_editor_pin_tool.h>
#include <tools/symbol_editor_drawing_tools.h>
#include <tools/symbol_editor_move_tool.h>
#include <clipboard.h>
#include <sch_actions.h>
#include <increment.h>
#include <pin_layout_cache.h>
#include <string_utils.h>
#include <symbol_edit_frame.h>
#include <sch_commit.h>
#include <dialogs/dialog_shape_properties.h>
#include <dialogs/dialog_text_properties.h>
#include <dialogs/dialog_field_properties.h>
#include <dialogs/dialog_lib_symbol_properties.h>
#include <dialogs/dialog_lib_edit_pin_table.h>
#include <dialogs/dialog_update_symbol_fields.h>
#include <view/view_controls.h>
#include <view/view.h>
#include <richio.h>
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
#include <sch_textbox.h>
#include <lib_symbol_library_manager.h>
#include <wx/textdlg.h> // for wxTextEntryDialog
#include <math/util.h> // for KiROUND
#include <io/kicad/kicad_io_utils.h>
#include <trace_helpers.h>
#include <plotters/plotters_pslike.h>
#include <sch_painter.h>
#include <sch_plotter.h>
#include <locale_io.h>
#include <gal/gal_print.h>
#include <gal/graphics_abstraction_layer.h>
#include <zoom_defines.h>
#include <wx/ffile.h>
#include <wx/mstream.h>
#include <wx/dcmemory.h>
namespace
{
constexpr int clipboardMaxBitmapSize = 4096;
constexpr double clipboardBboxInflation = 0.02;
void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& aMimeData, const wxString& aMimeType,
const wxMemoryBuffer& aBuffer )
{
if( aBuffer.GetDataLen() == 0 )
return;
CLIPBOARD_MIME_DATA entry;
entry.m_mimeType = aMimeType;
entry.m_data = aBuffer;
aMimeData.push_back( entry );
}
void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& aMimeData, const wxString& aMimeType,
wxImage&& aImage )
{
if( !aImage.IsOk() )
return;
CLIPBOARD_MIME_DATA entry;
entry.m_mimeType = aMimeType;
entry.m_image = std::move( aImage );
aMimeData.push_back( std::move( entry ) );
}
bool loadFileToBuffer( const wxString& aFileName, wxMemoryBuffer& aBuffer )
{
wxFFile file( aFileName, wxS( "rb" ) );
if( !file.IsOpened() )
return false;
wxFileOffset size = file.Length();
if( size <= 0 )
return false;
void* data = aBuffer.GetWriteBuf( size );
if( file.Read( data, size ) != static_cast<size_t>( size ) )
{
aBuffer.UngetWriteBuf( 0 );
return false;
}
aBuffer.UngetWriteBuf( size );
return true;
}
bool plotSymbolToSvg( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer )
{
if( !aSymbol )
return false;
SCH_RENDER_SETTINGS renderSettings;
renderSettings.LoadColors( aFrame->GetColorSettings() );
renderSettings.SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );
std::unique_ptr<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
plotter->SetRenderSettings( &renderSettings );
PAGE_INFO pageInfo = aFrame->GetScreen()->GetPageSettings();
pageInfo.SetWidthMils( schIUScale.IUToMils( aBBox.GetWidth() ) );
pageInfo.SetHeightMils( schIUScale.IUToMils( aBBox.GetHeight() ) );
plotter->SetPageSettings( pageInfo );
plotter->SetColorMode( true );
VECTOR2I plot_offset = aBBox.GetOrigin();
plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false );
plotter->SetCreator( wxT( "Eeschema-SVG" ) );
wxFileName tempFile( wxFileName::CreateTempFileName( wxS( "kicad_symbol_svg" ) ) );
if( !plotter->OpenFile( tempFile.GetFullPath() ) )
{
wxRemoveFile( tempFile.GetFullPath() );
return false;
}
LOCALE_IO toggle;
SCH_PLOT_OPTS plotOpts;
plotter->StartPlot( wxT( "1" ) );
constexpr bool background = true;
aSymbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
aSymbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
aSymbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
plotter->EndPlot();
plotter.reset();
bool ok = loadFileToBuffer( tempFile.GetFullPath(), aBuffer );
wxRemoveFile( tempFile.GetFullPath() );
return ok;
}
wxImage renderSymbolToBitmap( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
int aUnit, int aBodyStyle, int aWidth, int aHeight,
double aViewScale, const wxColour& aBgColor )
{
if( !aSymbol )
return wxImage();
wxBitmap bitmap( aWidth, aHeight, 24 );
wxMemoryDC dc;
dc.SelectObject( bitmap );
dc.SetBackground( wxBrush( aBgColor ) );
dc.Clear();
KIGFX::GAL_DISPLAY_OPTIONS options;
options.antialiasing_mode = KIGFX::GAL_ANTIALIASING_MODE::AA_HIGHQUALITY;
std::unique_ptr<KIGFX::GAL_PRINT> galPrint = KIGFX::GAL_PRINT::Create( options, &dc );
if( !galPrint )
return wxImage();
KIGFX::GAL* gal = galPrint->GetGAL();
KIGFX::PRINT_CONTEXT* printCtx = galPrint->GetPrintCtx();
std::unique_ptr<KIGFX::SCH_PAINTER> painter = std::make_unique<KIGFX::SCH_PAINTER>( gal );
std::unique_ptr<KIGFX::VIEW> view = std::make_unique<KIGFX::VIEW>();
// For symbol editor, we don't have a full schematic context
// but SCH_PAINTER can still work for rendering individual items
view->SetGAL( gal );
view->SetPainter( painter.get() );
view->SetScaleLimits( ZOOM_MAX_LIMIT_EESCHEMA, ZOOM_MIN_LIMIT_EESCHEMA );
view->SetScale( 1.0 );
gal->SetWorldUnitLength( SCH_WORLD_UNIT );
// Clone items and add to view
std::vector<std::unique_ptr<SCH_ITEM>> clonedItems;
for( SCH_ITEM& item : aSymbol->GetDrawItems() )
{
if( aUnit && item.GetUnit() && item.GetUnit() != aUnit )
continue;
if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle )
continue;
SCH_ITEM* clone = static_cast<SCH_ITEM*>( item.Clone() );
clonedItems.emplace_back( clone );
view->Add( clone );
}
SCH_RENDER_SETTINGS* dstSettings = painter->GetSettings();
dstSettings->LoadColors( aFrame->GetColorSettings() );
dstSettings->SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );
dstSettings->SetIsPrinting( true );
COLOR4D bgColor4D( aBgColor.Red() / 255.0, aBgColor.Green() / 255.0,
aBgColor.Blue() / 255.0, 1.0 );
dstSettings->SetBackgroundColor( bgColor4D );
for( int i = 0; i < KIGFX::VIEW::VIEW_MAX_LAYERS; ++i )
{
view->SetLayerVisible( i, true );
view->SetLayerTarget( i, KIGFX::TARGET_NONCACHED );
}
view->SetLayerVisible( LAYER_DRAWINGSHEET, false );
// Calculate effective output DPI for the print context.
// On GTK, Cairo uses device scale 72/4800 and SetSheetSize doubles internal resolution.
// On Windows/macOS, there's no device scale, so effective DPI = native DPI * 2.
#ifdef __WXGTK__
double ppi = 144.0;
#else
double ppi = printCtx->GetNativeDPI() * 2.0;
#endif
double inch2Iu = 1000.0 * schIUScale.IU_PER_MILS;
VECTOR2D pageSizeIn( (double) aWidth / ppi, (double) aHeight / ppi );
galPrint->SetSheetSize( pageSizeIn );
galPrint->SetNativePaperSize( pageSizeIn, printCtx->HasNativeLandscapeRotation() );
// SetSheetSize creates an internal canvas at 2× the nominal page size for quality.
// The × 2 multiplier ensures content fills this internal canvas.
double zoomFactor = 2.0 * aViewScale * inch2Iu / ppi;
// Set up both the GAL and VIEW to center on the bbox.
view->SetCenter( aBBox.Centre() );
view->SetScale( aViewScale * zoomFactor );
gal->SetLookAtPoint( aBBox.Centre() );
gal->SetZoomFactor( zoomFactor );
gal->SetClearColor( bgColor4D );
gal->ClearScreen();
view->UseDrawPriority( true );
{
KIGFX::GAL_DRAWING_CONTEXT ctx( gal );
view->Redraw();
}
dc.SelectObject( wxNullBitmap );
return bitmap.ConvertToImage();
}
wxImage renderSymbolToImageWithAlpha( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol,
const BOX2I& aBBox, int aUnit, int aBodyStyle )
{
if( !aSymbol )
return wxImage();
VECTOR2I size = aBBox.GetSize();
if( size.x <= 0 || size.y <= 0 )
return wxImage();
// Use the current view scale to match what the user sees on screen
double viewScale = aFrame->GetCanvas()->GetView()->GetScale();
int bitmapWidth = KiROUND( size.x * viewScale );
int bitmapHeight = KiROUND( size.y * viewScale );
// Clamp to maximum size while preserving aspect ratio
if( bitmapWidth > clipboardMaxBitmapSize || bitmapHeight > clipboardMaxBitmapSize )
{
double scaleDown = (double) clipboardMaxBitmapSize / std::max( bitmapWidth, bitmapHeight );
bitmapWidth = KiROUND( bitmapWidth * scaleDown );
bitmapHeight = KiROUND( bitmapHeight * scaleDown );
viewScale *= scaleDown;
}
if( bitmapWidth <= 0 || bitmapHeight <= 0 )
return wxImage();
// Render twice with different backgrounds for alpha computation
wxImage imageOnWhite = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
bitmapWidth, bitmapHeight, viewScale, *wxWHITE );
wxImage imageOnBlack = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
bitmapWidth, bitmapHeight, viewScale, *wxBLACK );
if( !imageOnWhite.IsOk() || !imageOnBlack.IsOk() )
return wxImage();
// Create output image with alpha channel
wxImage result( bitmapWidth, bitmapHeight );
result.InitAlpha();
unsigned char* rgbWhite = imageOnWhite.GetData();
unsigned char* rgbBlack = imageOnBlack.GetData();
unsigned char* rgbResult = result.GetData();
unsigned char* alphaResult = result.GetAlpha();
int pixelCount = bitmapWidth * bitmapHeight;
for( int i = 0; i < pixelCount; ++i )
{
int idx = i * 3;
int rW = rgbWhite[idx], gW = rgbWhite[idx + 1], bW = rgbWhite[idx + 2];
int rB = rgbBlack[idx], gB = rgbBlack[idx + 1], bB = rgbBlack[idx + 2];
// Alpha computation: α = 1 - (white - black) / 255
int diffR = rW - rB;
int diffG = gW - gB;
int diffB = bW - bB;
int avgDiff = ( diffR + diffG + diffB ) / 3;
int alpha = 255 - avgDiff;
alpha = std::max( 0, std::min( 255, alpha ) );
alphaResult[i] = static_cast<unsigned char>( alpha );
if( alpha > 0 )
{
rgbResult[idx] = static_cast<unsigned char>( std::min( 255, rB * 255 / alpha ) );
rgbResult[idx + 1] = static_cast<unsigned char>( std::min( 255, gB * 255 / alpha ) );
rgbResult[idx + 2] = static_cast<unsigned char>( std::min( 255, bB * 255 / alpha ) );
}
else
{
rgbResult[idx] = 0;
rgbResult[idx + 1] = 0;
rgbResult[idx + 2] = 0;
}
}
return result;
}
} // namespace
SYMBOL_EDITOR_EDIT_TOOL::SYMBOL_EDITOR_EDIT_TOOL() :
SCH_TOOL_BASE( "eeschema.SymbolEditTool" )
{
}
const std::vector<KICAD_T> SYMBOL_EDITOR_EDIT_TOOL::SwappableItems = {
LIB_SYMBOL_T, // Allows swapping the anchor
SCH_PIN_T,
SCH_SHAPE_T,
SCH_TEXT_T,
SCH_TEXTBOX_T,
SCH_FIELD_T,
};
bool SYMBOL_EDITOR_EDIT_TOOL::Init()
{
SCH_TOOL_BASE::Init();
SYMBOL_EDITOR_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool<SYMBOL_EDITOR_DRAWING_TOOLS>();
SYMBOL_EDITOR_MOVE_TOOL* moveTool = m_toolMgr->GetTool<SYMBOL_EDITOR_MOVE_TOOL>();
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;
};
auto swapSelectionCondition =
canEdit && SCH_CONDITIONS::OnlyTypes( SwappableItems ) && SELECTION_CONDITIONS::MoreThan( 1 );
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<SCH_PIN*> pins;
for( EDA_ITEM* item : sel )
{
if( item->Type() != SCH_PIN_T )
return false;
pins.push_back( static_cast<SCH_PIN*>( 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<SCH_PIN*>( 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<SCH_PIN*>( sel.Front() );
bool isValid;
std::vector<wxString> 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, swapSelectionCondition, 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, swapSelectionCondition, 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<SCH_ITEM*>( selection.Front() );
SCH_COMMIT localCommit( m_toolMgr );
SCH_COMMIT* commit = dynamic_cast<SCH_COMMIT*>( 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<SCH_ITEM*>( 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<SCH_ITEM*>( 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<SCH_FIELD*>( 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<SCH_ITEM*>( 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;
}
int SYMBOL_EDITOR_EDIT_TOOL::Swap( const TOOL_EVENT& aEvent )
{
SCH_SELECTION& selection = m_selectionTool->RequestSelection( SwappableItems );
std::vector<EDA_ITEM*> 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<SCH_ITEM*>( sorted[i] );
SCH_ITEM* b = static_cast<SCH_ITEM*>( 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<SCH_PIN*>( a );
SCH_PIN* bBpin = static_cast<SCH_PIN*>( 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<KICAD_T> 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<EDA_ITEM*> 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<SCH_ITEM*> toDelete;
int fieldsHidden = 0;
int fieldsAlreadyHidden = 0;
for( EDA_ITEM* item : items )
{
if( item->Type() == SCH_PIN_T )
{
SCH_PIN* curr_pin = static_cast<SCH_PIN*>( 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<bool> 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<SCH_FIELD*>( item );
// Hide "deleted" fields
if( field->IsVisible() )
{
field->SetVisible( false );
fieldsHidden++;
}
else
{
fieldsAlreadyHidden++;
}
}
else if( SCH_ITEM* schItem = dynamic_cast<SCH_ITEM*>( 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 called from tree context menu, edit properties without loading into canvas
if( aEvent.IsAction( &SCH_ACTIONS::symbolProperties ) )
{
LIB_ID treeLibId = m_frame->GetTreeLIBID();
// Check if the selected symbol in tree is different from the currently loaded one
if( treeLibId.IsValid() &&
( !m_frame->GetCurSymbol() || m_frame->GetCurSymbol()->GetLibId() != treeLibId ) )
{
// Edit properties directly from library buffer without loading to canvas
editSymbolPropertiesFromLibrary( treeLibId );
return 0;
}
}
if( m_frame->GetCurSymbol() )
editSymbolProperties();
}
else if( selection.Size() == 1 )
{
SCH_ITEM* item = static_cast<SCH_ITEM*>( 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<SCH_PIN&>( *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<SYMBOL_EDITOR_PIN_TOOL>() )
pinTool->EditPinProperties( &pin, mouseOverNumber );
break;
}
case SCH_SHAPE_T:
editShapeProperties( static_cast<SCH_SHAPE*>( item ) );
break;
case SCH_TEXT_T:
editTextProperties( item );
break;
case SCH_TEXTBOX_T:
editTextBoxProperties( item );
break;
case SCH_FIELD_T:
editFieldProperties( static_cast<SCH_FIELD*>( 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<SYMBOL_EDITOR_DRAWING_TOOLS>();
drawingTools->SetDrawSpecificBodyStyle( !dlg.GetApplyToAllConversions() );
drawingTools->SetDrawSpecificUnit( !dlg.GetApplyToAllUnits() );
std::vector<MSG_PANEL_ITEM> 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<SCH_TEXT*>( 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<SCH_TEXTBOX*>( 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::editSymbolPropertiesFromLibrary( const LIB_ID& aLibId )
{
LIB_SYMBOL_LIBRARY_MANAGER& libMgr = m_frame->GetLibManager();
wxString libName = aLibId.GetLibNickname();
wxString symbolName = aLibId.GetLibItemName();
// Get the symbol from the library buffer (without loading it into the editor)
LIB_SYMBOL* bufferedSymbol = libMgr.GetBufferedSymbol( symbolName, libName );
if( !bufferedSymbol )
return;
// Create a copy to work with
LIB_SYMBOL tempSymbol( *bufferedSymbol );
m_toolMgr->RunAction( ACTIONS::cancelInteractive );
m_toolMgr->RunAction( ACTIONS::selectionClear );
DIALOG_LIB_SYMBOL_PROPERTIES dlg( m_frame, &tempSymbol );
// 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;
// Update the buffered symbol with the changes
libMgr.UpdateSymbol( &tempSymbol, libName );
// Mark the library as modified
libMgr.SetSymbolModified( symbolName, libName );
// Update the tree view
wxDataViewItem treeItem = libMgr.GetAdapter()->FindItem( aLibId );
m_frame->UpdateLibraryTree( treeItem, &tempSymbol );
}
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<SYMBOL_EDITOR_DRAWING_TOOLS>();
// 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<SCH_SELECTION_TOOL>();
wxCHECK( selTool, -1 );
std::vector<SCH_PIN*> selectedPins;
SCH_SELECTION& selection = selTool->GetSelection();
for( EDA_ITEM* item : selection )
{
if( item->Type() == SCH_PIN_T )
{
SCH_PIN* pinItem = static_cast<SCH_PIN*>( 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<SCH_SELECTION_TOOL>();
wxCHECK( selTool, -1 );
SCH_SELECTION& selection = selTool->GetSelection();
// Collect pins to convert - accept pins with any number format
std::vector<SCH_PIN*> 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<SCH_PIN*>( 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<SCH_PIN*>( 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<wxString, std::vector<long>> prefixGroups;
std::vector<wxString> 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( traceStackedPins,
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<SCH_PIN*> pinsToRemove;
for( size_t i = 1; i < pinsToConvert.size(); ++i )
{
SCH_PIN* pinToRemove = pinsToConvert[i];
// Log the pin before removing it
wxLogTrace( traceStackedPins,
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<SCH_SELECTION_TOOL>();
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<SCH_PIN*>( selection.Front() );
// Check if the pin has stacked notation
bool isValid;
std::vector<wxString> 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() );
// Hidden power input pins act as global labels, so demote them to passive
if( pin->GetType() == ELECTRICAL_PINTYPE::PT_POWER_IN )
newPin->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
else
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 );
// 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<SCH_SELECTION_TOOL>();
// 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<SCH_SELECTION_TOOL>();
// 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, KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES );
// Generate SVG and PNG for multi-format clipboard
std::vector<CLIPBOARD_MIME_DATA> mimeData;
// Get the bounding box for just the selected items
BOX2I bbox;
for( EDA_ITEM* item : selection )
{
SCH_ITEM* schItem = static_cast<SCH_ITEM*>( item );
if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 )
bbox = schItem->GetBoundingBox();
else
bbox.Merge( schItem->GetBoundingBox() );
}
if( bbox.GetWidth() > 0 && bbox.GetHeight() > 0 )
{
bbox.Inflate( bbox.GetWidth() * clipboardBboxInflation,
bbox.GetHeight() * clipboardBboxInflation );
// Create a temporary symbol with just the selected items for plotting
LIB_SYMBOL* plotSymbol = new LIB_SYMBOL( *symbol );
// Mark unselected items as deleted in the plot copy
for( SCH_ITEM& item : plotSymbol->GetDrawItems() )
{
if( item.Type() == SCH_FIELD_T )
continue;
// Find matching item in selection by position/type
bool found = false;
for( EDA_ITEM* selItem : selection )
{
SCH_ITEM* selSchItem = static_cast<SCH_ITEM*>( selItem );
if( selSchItem->Type() == item.Type()
&& selSchItem->GetPosition() == item.GetPosition() )
{
found = true;
break;
}
}
if( !found )
item.SetFlags( STRUCT_DELETED );
}
// Now copy only the non-deleted items to a clean symbol for plotting
LIB_SYMBOL* cleanSymbol = new LIB_SYMBOL( *plotSymbol );
delete plotSymbol;
int unit = m_frame->GetUnit();
int bodyStyle = m_frame->GetBodyStyle();
wxMemoryBuffer svgBuffer;
if( plotSymbolToSvg( m_frame, cleanSymbol, bbox, unit, bodyStyle, svgBuffer ) )
appendMimeData( mimeData, wxS( "image/svg+xml" ), svgBuffer );
wxImage pngImage = renderSymbolToImageWithAlpha( m_frame, cleanSymbol, bbox, unit, bodyStyle );
if( pngImage.IsOk() )
appendMimeData( mimeData, wxS( "image/png" ), std::move( pngImage ) );
delete cleanSymbol;
}
if( SaveClipboard( prettyData, mimeData ) )
return 0;
else
return -1;
}
int SYMBOL_EDITOR_EDIT_TOOL::CopyAsText( const TOOL_EVENT& aEvent )
{
SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
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<LIB_SYMBOL*> 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<EDA_ITEM*> oldItems;
std::vector<EDA_ITEM*> 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<SCH_PIN*>( a )->GetNumber();
const wxString& bNum = static_cast<SCH_PIN*>( 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<SCH_ITEM*>( item );
SCH_ITEM* newItem = oldItem->Duplicate( true, &commit );
if( newItem->Type() == SCH_PIN_T )
{
SCH_PIN* newPin = static_cast<SCH_PIN*>( 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<EDA_ITEMS*>( 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
}