/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2019 CERN * Copyright (C) 2019-2022 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 #include #include #include #include #include #include #include #include #include #include #include #include #include // for KiROUND #include "pl_editor_frame.h" #define HITTEST_THRESHOLD_PIXELS 3 PL_SELECTION_TOOL::PL_SELECTION_TOOL() : SELECTION_TOOL( "plEditor.InteractiveSelection" ), m_frame( nullptr ) { } bool PL_SELECTION_TOOL::Init() { m_frame = getEditFrame(); auto& menu = m_menu.GetMenu(); menu.AddSeparator( 200 ); menu.AddItem( PL_ACTIONS::drawLine, SELECTION_CONDITIONS::Empty, 200 ); menu.AddItem( PL_ACTIONS::drawRectangle, SELECTION_CONDITIONS::Empty, 200 ); menu.AddItem( PL_ACTIONS::placeText, SELECTION_CONDITIONS::Empty, 200 ); menu.AddItem( PL_ACTIONS::placeImage, SELECTION_CONDITIONS::Empty, 200 ); menu.AddSeparator( 1000 ); m_frame->AddStandardSubMenus( m_menu ); m_disambiguateTimer.SetOwner( this ); Connect( wxEVT_TIMER, wxTimerEventHandler( PL_SELECTION_TOOL::onDisambiguationExpire ), nullptr, this ); return true; } void PL_SELECTION_TOOL::Reset( RESET_REASON aReason ) { if( aReason == MODEL_RELOAD ) m_frame = getEditFrame(); } int PL_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) { // Main loop: keep receiving events while( TOOL_EVENT* evt = Wait() ) { // on left click, a selection is made, depending on modifiers ALT, SHIFT, CTRL: setModifiersState( evt->Modifier( MD_SHIFT ), evt->Modifier( MD_CTRL ), evt->Modifier( MD_ALT ) ); if( evt->IsMouseDown( BUT_LEFT ) ) { // Avoid triggering when running under other tools PL_POINT_EDITOR *pt_tool = m_toolMgr->GetTool(); if( m_frame->ToolStackIsEmpty() && pt_tool && !pt_tool->HasPoint() ) { m_originalCursor = m_toolMgr->GetMousePosition(); m_disambiguateTimer.StartOnce( 500 ); } } // Single click? Select single object else if( evt->IsClick( BUT_LEFT ) ) { // If the timer has stopped, then we have already run the disambiguate routine // and we don't want to register an extra click here if( !m_disambiguateTimer.IsRunning() ) { evt->SetPassEvent(); continue; } m_disambiguateTimer.Stop(); SelectPoint( evt->Position() ); } // right click? if there is any object - show the context menu else if( evt->IsClick( BUT_RIGHT ) ) { m_disambiguateTimer.Stop(); bool selectionCancelled = false; if( m_selection.Empty() ) { SelectPoint( evt->Position(), &selectionCancelled ); m_selection.SetIsHover( true ); } if( !selectionCancelled ) m_menu.ShowContextMenu( m_selection ); } // double click? Display the properties window else if( evt->IsDblClick( BUT_LEFT ) ) { // No double-click actions currently defined } // drag with LMB? Select multiple objects (or at least draw a selection box) or drag them else if( evt->IsDrag( BUT_LEFT ) ) { m_disambiguateTimer.Stop(); if( hasModifier() || m_selection.Empty() ) { 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->RunAction( "plEditor.InteractiveMove.move", true ); } else { // No -> clear the selection list ClearSelection(); } } } // Middle double click? Do zoom to fit or zoom to objects else if( evt->IsDblClick( BUT_MIDDLE ) ) { m_toolMgr->RunAction( ACTIONS::zoomFitScreen, true ); } else if( evt->IsCancelInteractive() ) { m_disambiguateTimer.Stop(); ClearSelection(); } else if( evt->Action() == TA_UNDO_REDO_PRE ) { ClearSelection(); } else evt->SetPassEvent(); if( m_frame->ToolStackIsEmpty() ) { if( !hasModifier() && !m_selection.Empty() && m_frame->GetDragAction() == MOUSE_DRAG_ACTION::DRAG_SELECTED && evt->HasPosition() && selectionContains( evt->Position() ) ) { m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::MOVING ); } else { if( m_additive ) m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::ADD ); else if( m_subtractive ) m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SUBTRACT ); else if( m_exclusive_or ) m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::XOR ); else m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::ARROW ); } } } return 0; } int PL_SELECTION_TOOL::disambiguateCursor( const TOOL_EVENT& aEvent ) { wxMouseState keyboardState = wxGetMouseState(); setModifiersState( keyboardState.ShiftDown(), keyboardState.ControlDown(), keyboardState.AltDown() ); m_skip_heuristics = true; SelectPoint( m_originalCursor, &m_canceledMenu ); m_skip_heuristics = false; return 0; } PL_SELECTION& PL_SELECTION_TOOL::GetSelection() { return m_selection; } void PL_SELECTION_TOOL::SelectPoint( const VECTOR2I& aWhere, bool* aSelectionCancelledFlag ) { int threshold = KiROUND( getView()->ToWorld( HITTEST_THRESHOLD_PIXELS ) ); // locate items. COLLECTOR collector; for( DS_DATA_ITEM* dataItem : DS_DATA_MODEL::GetTheInstance().GetItems() ) { for( DS_DRAW_ITEM_BASE* drawItem : dataItem->GetDrawItems() ) { if( drawItem->HitTest( aWhere, threshold ) ) collector.Append( drawItem ); } } 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 ) { doSelectionMenu( &collector ); if( collector.m_MenuCancelled ) { if( aSelectionCancelledFlag ) *aSelectionCancelledFlag = true; return; } } bool anyAdded = false; bool anySubtracted = false; if( !m_additive && !m_subtractive && !m_exclusive_or ) { if( collector.GetCount() == 0 ) anySubtracted = true; ClearSelection(); } if( collector.GetCount() > 0 ) { for( int i = 0; i < collector.GetCount(); ++i ) { if( m_subtractive || ( m_exclusive_or && collector[i]->IsSelected() ) ) { unselect( collector[i] ); anySubtracted = true; } else { select( collector[i] ); anyAdded = true; } } } if( anyAdded ) m_toolMgr->ProcessEvent( EVENTS::SelectedEvent ); if( anySubtracted ) m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); } void PL_SELECTION_TOOL::guessSelectionCandidates( COLLECTOR& collector, const VECTOR2I& aPos ) { // There are certain conditions that can be handled automatically. // Prefer an exact hit to a sloppy one for( int i = 0; collector.GetCount() == 2 && i < 2; ++i ) { EDA_ITEM* item = collector[ i ]; EDA_ITEM* other = collector[ ( i + 1 ) % 2 ]; if( item->HitTest( aPos, 0 ) && !other->HitTest( aPos, 0 ) ) collector.Transfer( other ); } } PL_SELECTION& PL_SELECTION_TOOL::RequestSelection() { // If nothing is selected do a hover selection if( m_selection.Empty() ) { VECTOR2D cursorPos = getViewControls()->GetCursorPosition( true ); ClearSelection(); SelectPoint( cursorPos ); m_selection.SetIsHover( true ); } return m_selection; } bool PL_SELECTION_TOOL::selectMultiple() { bool cancelled = false; // Was the tool cancelled while it was running? m_multiple = true; // Multiple selection mode is active KIGFX::VIEW* view = getView(); KIGFX::PREVIEW::SELECTION_AREA area; view->Add( &area ); while( TOOL_EVENT* evt = Wait() ) { int width = area.GetEnd().x - area.GetOrigin().x; /* Selection mode depends on direction of drag-selection: * Left > Right : Select objects that are fully enclosed by selection * Right > Left : Select objects that are crossed by selection */ bool windowSelection = width >= 0 ? true : false; m_frame->GetCanvas()->SetCurrentCursor( windowSelection ? KICURSOR::SELECT_WINDOW : KICURSOR::SELECT_LASSO ); if( evt->IsCancelInteractive() || evt->IsActivate() ) { cancelled = true; break; } if( evt->IsDrag( BUT_LEFT ) ) { if( !m_drag_additive && !m_drag_subtractive ) ClearSelection(); // Start drawing a selection box area.SetOrigin( evt->DragOrigin() ); area.SetEnd( evt->Position() ); area.SetAdditive( m_drag_additive ); area.SetSubtractive( m_drag_subtractive ); area.SetExclusiveOr( false ); view->SetVisible( &area, true ); view->Update( &area ); getViewControls()->SetAutoPan( true ); } if( evt->IsMouseUp( BUT_LEFT ) ) { getViewControls()->SetAutoPan( false ); // End drawing the selection box view->SetVisible( &area, false ); int height = area.GetEnd().y - area.GetOrigin().y; bool anyAdded = false; bool anySubtracted = false; // Construct a BOX2I to determine EDA_ITEM selection BOX2I selectionRect( area.GetOrigin(), VECTOR2I( width, height ) ); selectionRect.Normalize(); for( DS_DATA_ITEM* dataItem : DS_DATA_MODEL::GetTheInstance().GetItems() ) { for( DS_DRAW_ITEM_BASE* item : dataItem->GetDrawItems() ) { if( item->HitTest( selectionRect, windowSelection ) ) { if( m_subtractive || ( m_exclusive_or && item->IsSelected() ) ) { unselect( item ); anySubtracted = true; } else { select( item ); anyAdded = true; } } } } // Inform other potentially interested tools if( anyAdded ) m_toolMgr->ProcessEvent( EVENTS::SelectedEvent ); if( anySubtracted ) m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); break; // Stop waiting for events } } getViewControls()->SetAutoPan( false ); // Stop drawing the selection box view->Remove( &area ); m_multiple = false; // Multiple selection mode is inactive if( !cancelled ) m_selection.ClearReferencePoint(); return cancelled; } int PL_SELECTION_TOOL::ClearSelection( const TOOL_EVENT& aEvent ) { ClearSelection(); return 0; } void PL_SELECTION_TOOL::RebuildSelection() { m_selection.Clear(); for( DS_DATA_ITEM* dataItem : DS_DATA_MODEL::GetTheInstance().GetItems() ) { for( DS_DRAW_ITEM_BASE* item : dataItem->GetDrawItems() ) { if( item->IsSelected() ) select( item ); } } } void PL_SELECTION_TOOL::ClearSelection() { if( m_selection.Empty() ) return; while( m_selection.GetSize() ) unhighlight( m_selection.Front(), SELECTED, &m_selection ); getView()->Update( &m_selection ); m_selection.SetIsHover( false ); m_selection.ClearReferencePoint(); // Inform other potentially interested tools m_toolMgr->ProcessEvent( EVENTS::ClearedEvent ); } void PL_SELECTION_TOOL::select( EDA_ITEM* aItem ) { highlight( aItem, SELECTED, &m_selection ); } void PL_SELECTION_TOOL::unselect( EDA_ITEM* aItem ) { unhighlight( aItem, SELECTED, &m_selection ); } void PL_SELECTION_TOOL::highlight( EDA_ITEM* aItem, int aMode, SELECTION* aGroup ) { if( aMode == SELECTED ) aItem->SetSelected(); else if( aMode == BRIGHTENED ) aItem->SetBrightened(); if( aGroup ) aGroup->Add( aItem ); getView()->Update( aItem ); } void PL_SELECTION_TOOL::unhighlight( EDA_ITEM* aItem, int aMode, SELECTION* aGroup ) { if( aMode == SELECTED ) aItem->ClearSelected(); else if( aMode == BRIGHTENED ) aItem->ClearBrightened(); if( aGroup ) aGroup->Remove( aItem ); getView()->Update( aItem ); } bool PL_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( EDA_ITEM* 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 PL_SELECTION_TOOL::setTransitions() { Go( &PL_SELECTION_TOOL::UpdateMenu, ACTIONS::updateMenu.MakeEvent() ); Go( &PL_SELECTION_TOOL::Main, PL_ACTIONS::selectionActivate.MakeEvent() ); Go( &PL_SELECTION_TOOL::ClearSelection, PL_ACTIONS::clearSelection.MakeEvent() ); Go( &PL_SELECTION_TOOL::AddItemToSel, PL_ACTIONS::addItemToSel.MakeEvent() ); Go( &PL_SELECTION_TOOL::AddItemsToSel, PL_ACTIONS::addItemsToSel.MakeEvent() ); Go( &PL_SELECTION_TOOL::RemoveItemFromSel, PL_ACTIONS::removeItemFromSel.MakeEvent() ); Go( &PL_SELECTION_TOOL::RemoveItemsFromSel, PL_ACTIONS::removeItemsFromSel.MakeEvent() ); Go( &PL_SELECTION_TOOL::SelectionMenu, PL_ACTIONS::selectionMenu.MakeEvent() ); Go( &PL_SELECTION_TOOL::disambiguateCursor, EVENTS::DisambiguatePoint ); }