kicad/eeschema/tools/sch_selection_tool.cpp

806 lines
23 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2019 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <sch_actions.h>
#include <core/typeinfo.h>
#include <sch_item_struct.h>
#include <sch_selection_tool.h>
#include <sch_base_frame.h>
#include <sch_edit_frame.h>
#include <sch_component.h>
#include <sch_sheet.h>
#include <view/view.h>
#include <view/view_controls.h>
#include <view/view_group.h>
#include <tool/tool_event.h>
#include <tool/tool_manager.h>
#include <sch_actions.h>
#include <sch_collectors.h>
#include <painter.h>
#include <eeschema_id.h>
#include <menus_helpers.h>
// Selection tool actions
TOOL_ACTION SCH_ACTIONS::selectionActivate( "eeschema.InteractiveSelection",
AS_GLOBAL, 0, "", "", NULL, AF_ACTIVATE ); // No description, not shown anywhere
TOOL_ACTION SCH_ACTIONS::selectionCursor( "eeschema.InteractiveSelection.Cursor",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::selectionMenu( "eeschema.InteractiveSelection.SelectionMenu",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::selectItem( "eeschema.InteractiveSelection.SelectItem",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::selectItems( "eeschema.InteractiveSelection.SelectItems",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::unselectItem( "eeschema.InteractiveSelection.UnselectItem",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::unselectItems( "eeschema.InteractiveSelection.UnselectItems",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
TOOL_ACTION SCH_ACTIONS::selectionClear( "eeschema.InteractiveSelection.Clear",
AS_GLOBAL, 0, "", "" ); // No description, it is not supposed to be shown anywhere
SCH_SELECTION_TOOL::SCH_SELECTION_TOOL() :
TOOL_INTERACTIVE( "eeschema.InteractiveSelection" ),
m_frame( NULL ),
m_additive( false ),
m_subtractive( false ),
m_multiple( false ),
m_skip_heuristics( false ),
m_locked( true ),
m_menu( *this )
{
}
SCH_SELECTION_TOOL::~SCH_SELECTION_TOOL()
{
getView()->Remove( &m_selection );
}
bool SCH_SELECTION_TOOL::Init()
{
auto frame = getEditFrame<SCH_BASE_FRAME>();
if( frame )
m_menu.AddStandardSubMenus( *frame );
return true;
}
void SCH_SELECTION_TOOL::Reset( RESET_REASON aReason )
{
m_frame = getEditFrame<SCH_BASE_FRAME>();
m_locked = true;
if( aReason == TOOL_BASE::MODEL_RELOAD )
{
// Remove pointers to the selected items from containers without changing their
// properties (as they are already deleted while a new sheet is loaded)
m_selection.Clear();
getView()->GetPainter()->GetSettings()->SetHighlight( false );
}
else
// Restore previous properties of selected items and remove them from containers
clearSelection();
// Reinsert the VIEW_GROUP, in case it was removed from the VIEW
getView()->Remove( &m_selection );
getView()->Add( &m_selection );
}
int SCH_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent )
{
// Main loop: keep receiving events
while( OPT_TOOL_EVENT evt = Wait() )
{
// Should selected items be added to the current selection or
// become the new selection (discarding previously selected items)
m_additive = evt->Modifier( MD_SHIFT );
// Should selected items be REMOVED from the current selection?
// This will be ignored if the SHIFT modifier is pressed
m_subtractive = !m_additive && evt->Modifier( MD_CTRL );
// Is the user requesting that the selection list include all possible
// items without removing less likely selection candidates
m_skip_heuristics = !!evt->Modifier( MD_ALT );
// Single click? Select single object
if( evt->IsClick( BUT_LEFT ) )
{
if( evt->Modifier( MD_CTRL ) && dynamic_cast<SCH_EDIT_FRAME*>( m_frame ) )
{
m_toolMgr->RunAction( SCH_ACTIONS::highlightNet, true );
}
else
{
// If no modifier keys are pressed, clear the selection
if( !m_additive )
clearSelection();
SelectPoint( evt->Position());
}
}
// right click? if there is any object - show the context menu
else if( evt->IsClick( BUT_RIGHT ) )
{
bool selectionCancelled = false;
if( m_selection.Empty() )
{
SelectPoint( evt->Position(), SCH_COLLECTOR::AllItems, &selectionCancelled );
m_selection.SetIsHover( true );
}
if( !selectionCancelled )
m_menu.ShowContextMenu( m_selection );
}
// double click? Display the properties window
else if( evt->IsDblClick( BUT_LEFT ) )
{
if( m_selection.Empty() )
SelectPoint( evt->Position());
m_toolMgr->RunAction( SCH_ACTIONS::properties );
}
// drag with LMB? Select multiple objects (or at least draw a selection box) or drag them
else if( evt->IsDrag( BUT_LEFT ) )
{
if( m_additive || m_subtractive || m_selection.Empty() )
{
// JEY TODO: move block selection to SCH_SELECTION_TOOL
//selectMultiple();
}
else
{
// Check if dragging has started within any of selected items bounding box
if( selectionContains( evt->Position() ) )
{
// Yes -> run the move tool and wait till it finishes
m_toolMgr->InvokeTool( "eeschema.InteractiveEdit" );
}
else
{
// No -> clear the selection list
clearSelection();
}
}
}
else if( evt->IsCancel() || evt->Action() == TA_UNDO_REDO_PRE )
{
clearSelection();
if( evt->IsCancel() && dynamic_cast<SCH_EDIT_FRAME*>( m_frame ) )
m_toolMgr->RunAction( SCH_ACTIONS::clearHighlight, true );
}
else if( evt->Action() == TA_CONTEXT_MENU_CLOSED )
{
m_menu.CloseContextMenu( evt );
}
}
// This tool is supposed to be active forever
assert( false );
return 0;
}
SELECTION& SCH_SELECTION_TOOL::GetSelection()
{
return m_selection;
}
SCH_ITEM* SCH_SELECTION_TOOL::SelectPoint( const VECTOR2I& aWhere, const KICAD_T* aFilterList,
bool* aSelectionCancelledFlag, bool aCheckLocked )
{
SCH_COLLECTOR collector;
collector.Collect( m_frame->GetScreen()->GetDrawItems(), aFilterList, (wxPoint) aWhere );
bool anyCollected = collector.GetCount() != 0;
// Remove unselectable items
for( int i = collector.GetCount() - 1; i >= 0; --i )
{
if( !selectable( collector[ i ] ) )
collector.Remove( i );
if( aCheckLocked && collector[ i ]->IsLocked() )
collector.Remove( i );
}
m_selection.ClearReferencePoint();
// Apply some ugly heuristics to avoid disambiguation menus whenever possible
if( collector.GetCount() > 1 && !m_skip_heuristics )
{
guessSelectionCandidates( collector, aWhere );
}
// If still more than one item we're going to have to ask the user.
if( collector.GetCount() > 1 )
{
collector.m_MenuTitle = _( "Clarify Selection" );
// Must call selectionMenu via RunAction() to avoid event-loop contention
m_toolMgr->RunAction( SCH_ACTIONS::selectionMenu, true, &collector );
if( collector.m_MenuCancelled )
{
if( aSelectionCancelledFlag )
*aSelectionCancelledFlag = true;
m_frame->GetScreen()->SetCurItem( nullptr );
return nullptr;
}
}
if( collector.GetCount() == 1 )
{
SCH_ITEM* item = collector[ 0 ];
toggleSelection( item );
MSG_PANEL_ITEMS msgItems;
item->GetMsgPanelInfo( m_frame->GetUserUnits(), msgItems );
m_frame->SetMsgPanel( msgItems );
return item;
}
if( !m_additive && anyCollected )
clearSelection();
m_frame->ClearMsgPanel();
return nullptr;
}
void SCH_SELECTION_TOOL::guessSelectionCandidates( SCH_COLLECTOR& collector,
const VECTOR2I& aWhere )
{
// There are certain parent/child and enclosure combinations that can be handled
// automatically. Since schematics are meant to be human-readable we don't have
// all the various overlap and coverage issues that we do in Pcbnew.
if( collector.GetCount() == 2 )
{
SCH_ITEM* a = collector[ 0 ];
SCH_ITEM* b = collector[ 1 ];
if( a->GetParent() == b )
collector.Remove( b );
else if( a == b->GetParent() )
collector.Remove( a );
else if( a->Type() == SCH_SHEET_T && b->Type() != SCH_SHEET_T )
collector.Remove( a );
else if( b->Type() == SCH_SHEET_T && a->Type() != SCH_SHEET_T )
collector.Remove( b );
}
}
SELECTION& SCH_SELECTION_TOOL::RequestSelection( const KICAD_T aFilterList[] )
{
if( m_selection.Empty() )
{
VECTOR2D cursorPos = getViewControls()->GetCursorPosition( false );
clearSelection();
SelectPoint( cursorPos, aFilterList );
return m_selection;
}
else
{
// Trim an existing selection by aFilterList
SELECTION originalSelection( m_selection );
for( EDA_ITEM* item : originalSelection )
{
KICAD_T matchType;
bool match = false;
for( const KICAD_T* p = aFilterList; (matchType = *p) != EOT && !match; ++p )
match = ( item->Type() == matchType );
if( !match )
toggleSelection( static_cast<SCH_ITEM*>( item ) );
}
return m_selection;
}
}
bool SCH_SELECTION_TOOL::selectCursor( const KICAD_T aFilterList[], bool aForceSelect )
{
if( aForceSelect || m_selection.Empty() )
{
VECTOR2D cursorPos = getViewControls()->GetCursorPosition( false );
clearSelection();
SelectPoint( cursorPos, aFilterList );
}
return !m_selection.Empty();
}
int SCH_SELECTION_TOOL::SelectItems( const TOOL_EVENT& aEvent )
{
std::vector<SCH_ITEM*>* items = aEvent.Parameter<std::vector<SCH_ITEM*>*>();
if( items )
{
// Perform individual selection of each item before processing the event.
for( auto item : *items )
select( item );
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
return 0;
}
int SCH_SELECTION_TOOL::SelectItem( const TOOL_EVENT& aEvent )
{
// Check if there is an item to be selected
SCH_ITEM* item = aEvent.Parameter<SCH_ITEM*>();
if( item )
{
select( item );
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
return 0;
}
int SCH_SELECTION_TOOL::UnselectItems( const TOOL_EVENT& aEvent )
{
std::vector<SCH_ITEM*>* items = aEvent.Parameter<std::vector<SCH_ITEM*>*>();
if( items )
{
// Perform individual unselection of each item before processing the event
for( auto item : *items )
unselect( item );
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
return 0;
}
int SCH_SELECTION_TOOL::UnselectItem( const TOOL_EVENT& aEvent )
{
// Check if there is an item to be selected
SCH_ITEM* item = aEvent.Parameter<SCH_ITEM*>();
if( item )
{
unselect( item );
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
return 0;
}
int SCH_SELECTION_TOOL::ClearSelection( const TOOL_EVENT& aEvent )
{
clearSelection();
return 0;
}
int SCH_SELECTION_TOOL::SelectionMenu( const TOOL_EVENT& aEvent )
{
SCH_COLLECTOR* collector = aEvent.Parameter<SCH_COLLECTOR*>();
if( !doSelectionMenu( collector ) )
collector->m_MenuCancelled = true;
return 0;
}
bool SCH_SELECTION_TOOL::doSelectionMenu( SCH_COLLECTOR* aCollector )
{
SCH_ITEM* current = nullptr;
#if 1
// ====================================================================================
// JEY TODO: use wxWidgets event loop for showing menu until we move to modern toolset event loop
wxMenu selectMenu;
AddMenuItem( &selectMenu, wxID_NONE, _( "Clarify Selection" ), KiBitmap( info_xpm ) );
selectMenu.AppendSeparator();
for( int i = 0; i < aCollector->GetCount() && i < MAX_SELECT_ITEM_IDS; i++ )
{
SCH_ITEM* item = ( *aCollector )[i];
wxString text = item->GetSelectMenuText( m_frame->GetUserUnits() );
BITMAP_DEF xpm = item->GetMenuImage();
AddMenuItem( &selectMenu, i, text, KiBitmap( xpm ) );
}
// Set to NULL in case the user aborts the clarification context menu.
m_frame->GetScreen()->SetCurItem( nullptr );
int idx = m_frame->GetPopupMenuSelectionFromUser( selectMenu );
if( idx == wxID_NONE )
{
m_frame->GetScreen()->SetCurItem( nullptr );
return false;
}
m_frame->GetCanvas()->MoveCursorToCrossHair();
current = ( *aCollector )[ idx ];
m_frame->GetScreen()->SetCurItem( current );
aCollector->Empty();
aCollector->Append( current );
return true;
// ====================================================================================
#endif
CONTEXT_MENU menu;
int limit = std::min( MAX_SELECT_ITEM_IDS, aCollector->GetCount() );
for( int i = 0; i < limit; ++i )
{
wxString text;
SCH_ITEM* item = ( *aCollector )[i];
text = item->GetSelectMenuText( m_frame->GetUserUnits() );
wxString menuText = wxString::Format("&%d. %s", i + 1, text );
menu.Add( menuText, i + 1, item->GetMenuImage() );
}
if( aCollector->m_MenuTitle.Length() )
menu.SetTitle( aCollector->m_MenuTitle );
menu.SetIcon( info_xpm );
menu.DisplayTitle( true );
SetContextMenu( &menu, CMENU_NOW );
while( OPT_TOOL_EVENT evt = Wait() )
{
if( evt->Action() == TA_CONTEXT_MENU_UPDATE )
{
if( current )
unhighlight( current, BRIGHTENED );
int id = *evt->GetCommandId();
// User has pointed an item, so show it in a different way
if( id > 0 && id <= limit )
{
current = ( *aCollector )[id - 1];
highlight( current, BRIGHTENED );
}
else
{
current = NULL;
}
}
else if( evt->Action() == TA_CONTEXT_MENU_CHOICE )
{
if( current )
unhighlight( current, BRIGHTENED );
OPT<int> id = evt->GetCommandId();
// User has selected an item, so this one will be returned
if( id && ( *id > 0 ) )
current = ( *aCollector )[*id - 1];
else
current = NULL;
break;
}
}
if( current )
{
unhighlight( current, BRIGHTENED );
aCollector->Empty();
aCollector->Append( current );
return true;
}
return false;
}
bool SCH_SELECTION_TOOL::selectable( const SCH_ITEM* aItem, bool checkVisibilityOnly ) const
{
// NOTE: in the future this is where eeschema layer/itemtype visibility will be handled
switch( aItem->Type() )
{
case SCH_PIN_T:
if( !static_cast<const SCH_PIN*>( aItem )->IsVisible() && !m_frame->GetShowAllPins() )
return false;
break;
case LIB_PART_T: // In libedit we do not want to select the symbol itself.
return false;
case SCH_MARKER_T: // Always selectable
return true;
default: // Suppress warnings
break;
}
return true;
}
void SCH_SELECTION_TOOL::clearSelection()
{
if( m_selection.Empty() )
return;
while( m_selection.GetSize() )
unhighlight( static_cast<SCH_ITEM*>( m_selection.Front() ), SELECTED, &m_selection );
getView()->Update( &m_selection );
m_selection.SetIsHover( false );
m_selection.ClearReferencePoint();
if( m_frame )
m_frame->GetScreen()->SetCurItem( nullptr );
m_locked = true;
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::ClearedEvent );
}
void SCH_SELECTION_TOOL::toggleSelection( SCH_ITEM* aItem, bool aForce )
{
if( aItem->IsSelected() )
{
unselect( aItem );
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
else
{
if( !m_additive )
clearSelection();
// Prevent selection of invisible or inactive items
if( aForce || selectable( aItem ) )
{
select( aItem );
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
}
if( m_frame )
m_frame->GetGalCanvas()->ForceRefresh();
}
void SCH_SELECTION_TOOL::select( SCH_ITEM* aItem )
{
if( aItem->IsSelected() )
return;
highlight( aItem, SELECTED, &m_selection );
getView()->Update( &m_selection );
if( m_frame )
{
if( m_selection.Size() == 1 )
{
// Set as the current item, so the information about selection is displayed
m_frame->GetScreen()->SetCurItem( aItem );
}
else if( m_selection.Size() == 2 ) // Check only for 2, so it will not be
{ // called for every next selected item
// If multiple items are selected, do not show the information about the selected item
m_frame->GetScreen()->SetCurItem( nullptr );
}
}
}
void SCH_SELECTION_TOOL::unselect( SCH_ITEM* aItem )
{
unhighlight( aItem, SELECTED, &m_selection );
getView()->Update( &m_selection );
if( m_frame && m_frame->GetScreen()->GetCurItem() == aItem )
m_frame->GetScreen()->SetCurItem( nullptr );
if( m_selection.Empty() )
m_locked = true;
}
void SCH_SELECTION_TOOL::highlight( SCH_ITEM* aItem, int aMode, SELECTION* aGroup )
{
if( aMode == SELECTED )
aItem->SetSelected();
else if( aMode == BRIGHTENED )
aItem->SetBrightened();
if( aGroup )
aGroup->Add( aItem );
// Highlight pins and fields. (All the other component children are currently only
// represented in the LIB_PART.)
if( aItem->Type() == SCH_COMPONENT_T )
{
SCH_PINS pins = static_cast<SCH_COMPONENT*>( aItem )->GetPins();
for( SCH_PIN& pin : pins )
{
if( aMode == SELECTED )
pin.SetSelected();
else if( aMode == BRIGHTENED )
pin.SetBrightened();
}
std::vector<SCH_FIELD*> fields;
static_cast<SCH_COMPONENT*>( aItem )->GetFields( fields, false );
for( auto field : fields )
{
if( aMode == SELECTED )
field->SetSelected();
else if( aMode == BRIGHTENED )
field->SetBrightened();
// JEY TODO: do these need hiding from view and adding to aGroup?
}
}
// JEY TODO: Sheets and sheet pins?
// Many selections are very temporal and updating the display each time just
// creates noise.
if( aMode == BRIGHTENED )
getView()->MarkTargetDirty( KIGFX::TARGET_OVERLAY );
}
void SCH_SELECTION_TOOL::unhighlight( SCH_ITEM* aItem, int aMode, SELECTION* aGroup )
{
if( aMode == SELECTED )
aItem->ClearSelected();
else if( aMode == BRIGHTENED )
aItem->ClearBrightened();
if( aGroup )
aGroup->Remove( aItem );
// Unhighlight pins and fields. (All the other component children are currently only
// represented in the LIB_PART.)
if( aItem->Type() == SCH_COMPONENT_T )
{
SCH_PINS pins = static_cast<SCH_COMPONENT*>( aItem )->GetPins();
for( SCH_PIN& pin : pins )
{
if( aMode == SELECTED )
pin.ClearSelected();
else if( aMode == BRIGHTENED )
pin.ClearBrightened();
}
std::vector<SCH_FIELD*> fields;
static_cast<SCH_COMPONENT*>( aItem )->GetFields( fields, false );
for( auto field : fields )
{
if( aMode == SELECTED )
field->ClearSelected();
else if( aMode == BRIGHTENED )
field->ClearBrightened();
// JEY TODO: do these need showing and updating?
}
}
// JEY TODO: Sheets and sheet pins?
// Many selections are very temporal and updating the display each time just
// creates noise.
if( aMode == BRIGHTENED )
getView()->MarkTargetDirty( KIGFX::TARGET_OVERLAY );
}
bool SCH_SELECTION_TOOL::selectionContains( const VECTOR2I& aPoint ) const
{
const unsigned GRIP_MARGIN = 20;
VECTOR2I margin = getView()->ToWorld( VECTOR2I( GRIP_MARGIN, GRIP_MARGIN ), false );
// Check if the point is located within any of the currently selected items bounding boxes
for( auto item : m_selection )
{
BOX2I itemBox = item->ViewBBox();
itemBox.Inflate( margin.x, margin.y ); // Give some margin for gripping an item
if( itemBox.Contains( aPoint ) )
return true;
}
return false;
}
void SCH_SELECTION_TOOL::setTransitions()
{
Go( &SCH_SELECTION_TOOL::Main, SCH_ACTIONS::selectionActivate.MakeEvent() );
Go( &SCH_SELECTION_TOOL::ClearSelection, SCH_ACTIONS::selectionClear.MakeEvent() );
Go( &SCH_SELECTION_TOOL::SelectItem, SCH_ACTIONS::selectItem.MakeEvent() );
Go( &SCH_SELECTION_TOOL::SelectItems, SCH_ACTIONS::selectItems.MakeEvent() );
Go( &SCH_SELECTION_TOOL::UnselectItem, SCH_ACTIONS::unselectItem.MakeEvent() );
Go( &SCH_SELECTION_TOOL::UnselectItems, SCH_ACTIONS::unselectItems.MakeEvent() );
Go( &SCH_SELECTION_TOOL::SelectionMenu, SCH_ACTIONS::selectionMenu.MakeEvent() );
}