Prevent single-element groups

Groups with fewer than two items serve no purpose. All
user-facing group creation paths now require at least two
items before creating a group. The Group menu action is
greyed out when fewer than two items are selected.

When items are removed from a group via RemoveFromGroup,
pruneExistingGroups, or the netlist updater, the group is
dissolved if it drops below two members. Importers are left
unchanged since they reflect external file structure.
This commit is contained in:
Seth Hillbrand
2026-02-19 09:43:01 -08:00
parent daa76770db
commit fb58cc4187
12 changed files with 111 additions and 56 deletions
+17 -3
View File
@@ -22,6 +22,8 @@
*/
#include "tool/group_tool.h"
#include <set>
#include <eda_draw_frame.h>
#include <kiplatform/ui.h>
#include <tool/actions.h>
@@ -61,7 +63,7 @@ public:
private:
void update() override
{
bool canGroup = false;
int selectionCount = 0;
bool hasGroup = false;
bool hasMember = false;
bool onlyOneGroup = false;
@@ -71,7 +73,7 @@ private:
{
for( EDA_ITEM* item : m_selectionTool->GetSelection() )
{
canGroup = true;
selectionCount++;
if( item->Type() == PCB_GROUP_T || item->Type() == SCH_GROUP_T )
{
@@ -92,7 +94,7 @@ private:
}
}
Enable( ACTIONS::group.GetUIId(), canGroup );
Enable( ACTIONS::group.GetUIId(), selectionCount >= 2 );
Enable( ACTIONS::ungroup.GetUIId(), hasGroup );
Enable( ACTIONS::addToGroup.GetUIId(), onlyOneGroup && hasUngroupedItems );
Enable( ACTIONS::removeFromGroup.GetUIId(), hasMember );
@@ -253,6 +255,8 @@ int GROUP_TOOL::RemoveFromGroup( const TOOL_EVENT& aEvent )
if( selection.Empty() )
m_toolMgr->RunAction( ACTIONS::selectionCursor );
std::set<EDA_GROUP*> affectedGroups;
for( EDA_ITEM* item : selection )
{
if( EDA_GROUP* group = item->GetParentGroup() )
@@ -260,6 +264,16 @@ int GROUP_TOOL::RemoveFromGroup( const TOOL_EVENT& aEvent )
m_commit->Modify( group->AsEdaItem(), m_frame->GetScreen(), RECURSE_MODE::NO_RECURSE );
m_commit->Modify( item, m_frame->GetScreen() );
group->RemoveItem( item );
affectedGroups.insert( group );
}
}
for( EDA_GROUP* group : affectedGroups )
{
if( group->GetItems().size() < 2 )
{
group->RemoveAll();
m_commit->Remove( group->AsEdaItem(), m_frame->GetScreen() );
}
}
+8 -6
View File
@@ -394,7 +394,7 @@ bool SCH_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
newGroup->SetName( blk.GetLibId().GetUniStringLibItemName() );
newGroup->SetDesignBlockLibId( blk.GetLibId() );
bool added = false;
int addedCount = 0;
for( EDA_ITEM* edaItem : selection )
{
@@ -414,10 +414,10 @@ bool SCH_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
commit.Modify( item, screen, RECURSE_MODE::NO_RECURSE );
newGroup->AddItem( item );
added = true;
addedCount++;
}
if( added )
if( addedCount >= 2 )
{
commit.Add( newGroup, screen );
commit.Push( _( "Group Items" ) );
@@ -427,6 +427,7 @@ bool SCH_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
}
else
{
newGroup->RemoveAll();
delete newGroup;
}
}
@@ -619,7 +620,7 @@ bool SCH_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
newGroup->SetName( aLibId.GetUniStringLibItemName() );
newGroup->SetDesignBlockLibId( aLibId );
bool added = false;
int addedCount = 0;
for( EDA_ITEM* edaItem : selection )
{
@@ -639,10 +640,10 @@ bool SCH_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
commit.Modify( item, screen, RECURSE_MODE::NO_RECURSE );
newGroup->AddItem( item );
added = true;
addedCount++;
}
if( added )
if( addedCount >= 2 )
{
commit.Add( newGroup, screen );
commit.Push( _( "Group Items" ) );
@@ -652,6 +653,7 @@ bool SCH_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
}
else
{
newGroup->RemoveAll();
delete newGroup;
}
}
+1 -1
View File
@@ -813,7 +813,7 @@ void SCH_EDIT_FRAME::setupUIConditions()
mgr->SetConditions( SCH_ACTIONS::rotateCCW, ENABLE( hasElements ) );
mgr->SetConditions( SCH_ACTIONS::mirrorH, ENABLE( hasElements ) );
mgr->SetConditions( SCH_ACTIONS::mirrorV, ENABLE( hasElements ) );
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::NotEmpty ) );
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::MoreThan( 1 ) ) );
mgr->SetConditions( ACTIONS::ungroup, ENABLE( SELECTION_CONDITIONS::HasType( SCH_GROUP_T ) ) );
mgr->SetConditions( SCH_ACTIONS::placeLinkedDesignBlock, ENABLE( groupWithDesignBlockLink ) );
+1 -1
View File
@@ -150,7 +150,7 @@ int SCH_GROUP_TOOL::Group( const TOOL_EVENT& aEvent )
selection.Remove( schItem );
}
if( selection.Empty() )
if( selection.GetSize() < 2 )
return 0;
SCH_GROUP* group = new SCH_GROUP;
+1 -1
View File
@@ -1368,7 +1368,7 @@ void FOOTPRINT_EDIT_FRAME::setupUIConditions()
mgr->SetConditions( PCB_ACTIONS::rotateCcw, ENABLE( cond.HasItems() ) );
mgr->SetConditions( PCB_ACTIONS::mirrorH, ENABLE( cond.HasItems() ) );
mgr->SetConditions( PCB_ACTIONS::mirrorV, ENABLE( cond.HasItems() ) );
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::NotEmpty ) );
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::MoreThan( 1 ) ) );
mgr->SetConditions( ACTIONS::ungroup, ENABLE( SELECTION_CONDITIONS::HasType( PCB_GROUP_T ) ) );
mgr->SetConditions( PCB_ACTIONS::padDisplayMode, CHECK( !cond.PadFillDisplay() ) );
@@ -947,6 +947,12 @@ bool BOARD_NETLIST_UPDATER::updateFootprintGroup( FOOTPRINT* aPcbFootprint,
changed = true;
m_commit.Modify( existingGroup, nullptr, RECURSE_MODE::NO_RECURSE );
existingGroup->RemoveItem( aPcbFootprint );
if( existingGroup->GetItems().size() < 2 )
{
existingGroup->RemoveAll();
m_commit.Remove( existingGroup );
}
}
m_reporter->Report( msg, RPT_SEVERITY_ACTION );
+8 -6
View File
@@ -423,7 +423,7 @@ bool PCB_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
newGroup->SetName( blk.GetLibId().GetUniStringLibItemName() );
newGroup->SetDesignBlockLibId( blk.GetLibId() );
bool added = false;
int addedCount = 0;
for( EDA_ITEM* edaItem : selection )
{
@@ -443,10 +443,10 @@ bool PCB_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
commit.Modify( item, nullptr, RECURSE_MODE::NO_RECURSE );
newGroup->AddItem( item );
added = true;
addedCount++;
}
if( added )
if( addedCount >= 2 )
{
commit.Add( newGroup );
commit.Push( _( "Group Items" ) );
@@ -456,6 +456,7 @@ bool PCB_EDIT_FRAME::SaveSelectionAsDesignBlock( const wxString& aLibraryName )
}
else
{
newGroup->RemoveAll();
delete newGroup;
}
}
@@ -560,7 +561,7 @@ bool PCB_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
newGroup->SetName( aLibId.GetUniStringLibItemName() );
newGroup->SetDesignBlockLibId( aLibId );
bool added = false;
int addedCount = 0;
for( EDA_ITEM* edaItem : selection )
{
@@ -580,10 +581,10 @@ bool PCB_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
commit.Modify( item, nullptr, RECURSE_MODE::NO_RECURSE );
newGroup->AddItem( item );
added = true;
addedCount++;
}
if( added )
if( addedCount >= 2 )
{
commit.Add( newGroup );
commit.Push( _( "Group Items" ) );
@@ -593,6 +594,7 @@ bool PCB_EDIT_FRAME::UpdateDesignBlockFromSelection( const LIB_ID& aLibId )
}
else
{
newGroup->RemoveAll();
delete newGroup;
}
}
+1 -1
View File
@@ -1014,7 +1014,7 @@ void PCB_EDIT_FRAME::setupUIConditions()
static const std::vector<KICAD_T> groupTypes = { PCB_GROUP_T, PCB_GENERATOR_T };
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::NotEmpty ) );
mgr->SetConditions( ACTIONS::group, ENABLE( SELECTION_CONDITIONS::MoreThan( 1 ) ) );
mgr->SetConditions( ACTIONS::ungroup, ENABLE( SELECTION_CONDITIONS::HasTypes( groupTypes ) ) );
mgr->SetConditions( PCB_ACTIONS::lock, ENABLE( PCB_SELECTION_CONDITIONS::HasUnlockedItems ) );
mgr->SetConditions( PCB_ACTIONS::unlock, ENABLE( PCB_SELECTION_CONDITIONS::HasLockedItems ) );
+13 -4
View File
@@ -2076,11 +2076,20 @@ int DRAWING_TOOL::PlaceImportedGraphics( const TOOL_EVENT& aEvent )
if( dlg.ShouldGroupItems() )
{
group = new PCB_GROUP( m_frame->GetModel() );
size_t boardItemCount = std::count_if( list.begin(), list.end(),
[]( const std::unique_ptr<EDA_ITEM>& ptr )
{
return ptr->IsBOARD_ITEM();
} );
newItems.push_back( group );
selectedItems.push_back( group );
preview.Add( group );
if( boardItemCount >= 2 )
{
group = new PCB_GROUP( m_frame->GetModel() );
newItems.push_back( group );
selectedItems.push_back( group );
preview.Add( group );
}
}
if( dlg.ShouldFixDiscontinuities() )
+8 -1
View File
@@ -885,6 +885,9 @@ int MULTICHANNEL_TOOL::RepeatLayout( const TOOL_EVENT& aEvent, ZONE* aRefZone )
{
for( const auto& [targetArea, compatData] : m_areas.m_compatMap )
{
if( compatData.m_groupableItems.size() < 2 )
continue;
pruneExistingGroups( commit, compatData.m_affectedItems );
PCB_GROUP* group = new PCB_GROUP( board() );
@@ -1533,7 +1536,7 @@ bool MULTICHANNEL_TOOL::pruneExistingGroups( COMMIT& aCommit,
for( EDA_ITEM* item : pruneList )
group->RemoveItem( item );
if( group->GetItems().empty() )
if( group->GetItems().size() < 2 )
aCommit.Remove( group );
}
}
@@ -1712,6 +1715,10 @@ int MULTICHANNEL_TOOL::AutogenerateRuleAreas( const TOOL_EVENT& aEvent )
if( ra.m_existsAlready && !m_areas.m_replaceExisting )
continue;
// A group needs at least 2 items (zone + at least 1 component)
if( ra.m_components.empty() )
continue;
std::unordered_set<BOARD_ITEM*> toPrune;
std::copy( ra.m_components.begin(), ra.m_components.end(), std::inserter( toPrune, toPrune.begin() ) );
+46 -31
View File
@@ -2064,48 +2064,63 @@ int PCB_CONTROL::AppendBoard( PCB_IO& pi, const wxString& fileName, DESIGN_BLOCK
{
if( placeAsGroup )
{
PCB_GROUP* group = new PCB_GROUP( brd );
if( aDesignBlock )
{
group->SetName( aDesignBlock->GetLibId().GetLibItemName() );
group->SetDesignBlockLibId( aDesignBlock->GetLibId() );
}
else
{
group->SetName( wxFileName( fileName ).GetName() );
}
// Get the selection tool selection
PCB_SELECTION_TOOL* selTool = m_toolMgr->GetTool<PCB_SELECTION_TOOL>();
PCB_SELECTION selection = selTool->GetSelection();
for( EDA_ITEM* eda_item : selection )
{
if( eda_item->IsBOARD_ITEM() )
{
if( static_cast<BOARD_ITEM*>( eda_item )->IsLocked() )
group->SetLocked( true );
}
}
commit->Add( group );
// Count items that would be added to the group
int groupableCount = 0;
for( EDA_ITEM* eda_item : selection )
{
if( eda_item->IsBOARD_ITEM() && !static_cast<BOARD_ITEM*>( eda_item )->GetParentFootprint() )
if( eda_item->IsBOARD_ITEM()
&& !static_cast<BOARD_ITEM*>( eda_item )->GetParentFootprint() )
{
commit->Modify( eda_item );
group->AddItem( eda_item );
groupableCount++;
}
}
selTool->ClearSelection();
selTool->select( group );
if( groupableCount >= 2 )
{
PCB_GROUP* group = new PCB_GROUP( brd );
m_toolMgr->PostEvent( EVENTS::SelectedItemsModified );
m_frame->OnModify();
m_frame->Refresh();
if( aDesignBlock )
{
group->SetName( aDesignBlock->GetLibId().GetLibItemName() );
group->SetDesignBlockLibId( aDesignBlock->GetLibId() );
}
else
{
group->SetName( wxFileName( fileName ).GetName() );
}
for( EDA_ITEM* eda_item : selection )
{
if( eda_item->IsBOARD_ITEM() )
{
if( static_cast<BOARD_ITEM*>( eda_item )->IsLocked() )
group->SetLocked( true );
}
}
commit->Add( group );
for( EDA_ITEM* eda_item : selection )
{
if( eda_item->IsBOARD_ITEM()
&& !static_cast<BOARD_ITEM*>( eda_item )->GetParentFootprint() )
{
commit->Modify( eda_item );
group->AddItem( eda_item );
}
}
selTool->ClearSelection();
selTool->select( group );
m_toolMgr->PostEvent( EVENTS::SelectedItemsModified );
m_frame->OnModify();
m_frame->Refresh();
}
}
// If we were provided a commit, let the caller control when to push it
+1 -1
View File
@@ -176,7 +176,7 @@ int PCB_GROUP_TOOL::Group( const TOOL_EVENT& aEvent )
} );
}
if( selection.Empty() )
if( selection.GetSize() < 2 )
return 0;
BOARD* board = getModel<BOARD>();