/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2019 CERN * Copyright (C) 2019-2023 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 // for KiROUND #include "tools/pl_selection_tool.h" #include "tools/pl_actions.h" #include "tools/pl_edit_tool.h" #include "pl_draw_panel_gal.h" #include "pl_editor_frame.h" #include "pl_editor_id.h" PL_EDIT_TOOL::PL_EDIT_TOOL() : TOOL_INTERACTIVE( "plEditor.InteractiveEdit" ), m_frame( nullptr ), m_selectionTool( nullptr ), m_moveInProgress( false ), m_moveOffset( 0, 0 ), m_cursor( 0, 0 ), m_pickerItem( nullptr ) { } bool PL_EDIT_TOOL::Init() { m_frame = getEditFrame(); m_selectionTool = m_toolMgr->GetTool(); wxASSERT_MSG( m_selectionTool, "plEditor.InteractiveSelection tool is not available" ); CONDITIONAL_MENU& ctxMenu = m_menu.GetMenu(); // cancel current tool goes in main context menu at the top if present ctxMenu.AddItem( ACTIONS::cancelInteractive, SELECTION_CONDITIONS::ShowAlways, 1 ); ctxMenu.AddSeparator( 200 ); ctxMenu.AddItem( ACTIONS::doDelete, SELECTION_CONDITIONS::NotEmpty, 200 ); // Finally, add the standard zoom/grid items m_frame->AddStandardSubMenus( m_menu ); // // Add editing actions to the selection tool menu // CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu(); selToolMenu.AddItem( PL_ACTIONS::move, SELECTION_CONDITIONS::NotEmpty, 250 ); selToolMenu.AddSeparator( 250 ); selToolMenu.AddItem( ACTIONS::cut, SELECTION_CONDITIONS::NotEmpty, 250 ); selToolMenu.AddItem( ACTIONS::copy, SELECTION_CONDITIONS::NotEmpty, 250 ); selToolMenu.AddItem( ACTIONS::paste, SELECTION_CONDITIONS::ShowAlways, 250 ); selToolMenu.AddItem( ACTIONS::doDelete, SELECTION_CONDITIONS::NotEmpty, 250 ); return true; } void PL_EDIT_TOOL::Reset( RESET_REASON aReason ) { if( aReason == MODEL_RELOAD ) m_frame = getEditFrame(); } int PL_EDIT_TOOL::Main( const TOOL_EVENT& aEvent ) { KIGFX::VIEW_CONTROLS* controls = getViewControls(); VECTOR2I originalCursorPos = controls->GetCursorPosition(); // Be sure that there is at least one item that we can move. If there's no selection try // looking for the stuff under mouse cursor (i.e. Kicad old-style hover selection). PL_SELECTION& selection = m_selectionTool->RequestSelection(); bool unselect = selection.IsHover(); if( selection.Empty() || m_moveInProgress ) return 0; std::set unique_peers; for( EDA_ITEM* item : selection ) { DS_DRAW_ITEM_BASE* drawItem = static_cast( item ); unique_peers.insert( drawItem->GetPeer() ); } m_frame->PushTool( aEvent ); Activate(); // Must be done after Activate() so that it gets set into the correct context controls->ShowCursor( true ); controls->SetAutoPan( true ); bool restore_state = false; bool chain_commands = false; TOOL_EVENT copy = aEvent; TOOL_EVENT* evt = © VECTOR2I prevPos; if( !selection.Front()->IsNew() ) { try { m_frame->SaveCopyInUndoList(); } catch( const fmt::v9::format_error& exc ) { wxLogWarning( wxS( "Exception \"%s\" serializing string ocurred." ), exc.what() ); return 1; } } // Main loop: keep receiving events do { m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::MOVING ); if( evt->IsAction( &PL_ACTIONS::move ) || evt->IsMotion() || evt->IsDrag( BUT_LEFT ) || evt->IsAction( &ACTIONS::refreshPreview ) ) { //------------------------------------------------------------------------ // Start a move operation // if( !m_moveInProgress ) { // Apply any initial offset in case we're coming from a previous command. // for( DS_DATA_ITEM* item : unique_peers ) moveItem( item, m_moveOffset ); // Set up the starting position and move/drag offset // m_cursor = controls->GetCursorPosition(); if( selection.HasReferencePoint() ) { VECTOR2I delta = m_cursor - selection.GetReferencePoint(); // Drag items to the current cursor position for( DS_DATA_ITEM* item : unique_peers ) moveItem( item, delta ); selection.SetReferencePoint( m_cursor ); } else if( selection.Size() == 1 ) { // Set the current cursor position to the first dragged item origin, // so the movement vector can be computed later updateModificationPoint( selection ); m_cursor = originalCursorPos; } else { updateModificationPoint( selection ); } controls->SetCursorPosition( m_cursor, false ); prevPos = m_cursor; controls->SetAutoPan( true ); m_moveInProgress = true; } //------------------------------------------------------------------------ // Follow the mouse // m_cursor = controls->GetCursorPosition(); VECTOR2I delta( m_cursor - prevPos ); selection.SetReferencePoint( m_cursor ); m_moveOffset += delta; prevPos = m_cursor; for( DS_DATA_ITEM* item : unique_peers ) moveItem( item, delta ); m_toolMgr->PostEvent( EVENTS::SelectedItemsMoved ); } //------------------------------------------------------------------------ // Handle cancel // else if( evt->IsCancelInteractive() || evt->IsActivate() ) { if( evt->IsCancelInteractive() ) m_frame->GetInfoBar()->Dismiss(); if( m_moveInProgress ) { if( evt->IsActivate() ) { // Allowing other tools to activate during a move runs the risk of race // conditions in which we try to spool up both event loops at once. m_frame->ShowInfoBarMsg( _( "Press to cancel move." ) ); evt->SetPassEvent( false ); continue; } evt->SetPassEvent( false ); restore_state = true; } break; } //------------------------------------------------------------------------ // Handle TOOL_ACTION special cases // else if( evt->Action() == TA_UNDO_REDO_PRE ) { unselect = true; break; } else if( evt->IsAction( &ACTIONS::doDelete ) ) { evt->SetPassEvent(); // Exit on a delete; there will no longer be anything to drag. break; } else if( evt->IsAction( &ACTIONS::duplicate ) ) { if( selection.Front()->IsNew() ) { // This doesn't really make sense; we'll just end up dragging a stack of // objects so we ignore the duplicate and just carry on. continue; } // Move original back and exit. The duplicate will run in its own loop. restore_state = true; unselect = false; chain_commands = true; break; } //------------------------------------------------------------------------ // Handle context menu // else if( evt->IsClick( BUT_RIGHT ) ) { m_menu.ShowContextMenu( m_selectionTool->GetSelection() ); } //------------------------------------------------------------------------ // Handle drop // else if( evt->IsMouseUp( BUT_LEFT ) || evt->IsClick( BUT_LEFT ) ) { break; // Finish } else { evt->SetPassEvent(); } controls->SetAutoPan( m_moveInProgress ); } while( ( evt = Wait() ) ); //Should be assignment not equality test controls->ForceCursorPosition( false ); controls->ShowCursor( false ); controls->SetAutoPan( false ); if( !chain_commands ) m_moveOffset = { 0, 0 }; selection.ClearReferencePoint(); for( EDA_ITEM* item : selection ) item->ClearEditFlags(); if( restore_state ) m_frame->RollbackFromUndo(); else m_frame->OnModify(); if( unselect ) m_toolMgr->RunAction( PL_ACTIONS::clearSelection, true ); else m_toolMgr->PostEvent( EVENTS::SelectedEvent ); m_moveInProgress = false; m_frame->PopTool( aEvent ); return 0; } void PL_EDIT_TOOL::moveItem( DS_DATA_ITEM* aItem, const VECTOR2I& aDelta ) { aItem->MoveToUi( aItem->GetStartPosUi() + aDelta ); for( DS_DRAW_ITEM_BASE* item : aItem->GetDrawItems() ) { getView()->Update( item ); item->SetFlags( IS_MOVING ); } } bool PL_EDIT_TOOL::updateModificationPoint( PL_SELECTION& aSelection ) { if( m_moveInProgress && aSelection.HasReferencePoint() ) return false; // When there is only one item selected, the reference point is its position... if( aSelection.Size() == 1 ) { aSelection.SetReferencePoint( aSelection.Front()->GetPosition() ); } // ...otherwise modify items with regard to the grid-snapped cursor position else { m_cursor = getViewControls()->GetCursorPosition( true ); aSelection.SetReferencePoint( m_cursor ); } return true; } int PL_EDIT_TOOL::ImportDrawingSheetContent( const TOOL_EVENT& aEvent ) { m_toolMgr->RunAction( ACTIONS::cancelInteractive, true ); wxCommandEvent evt( wxEVT_NULL, ID_APPEND_DESCR_FILE ); m_frame->Files_io( evt ); return 0; } int PL_EDIT_TOOL::DoDelete( const TOOL_EVENT& aEvent ) { PL_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.Size() == 0 ) return 0; // Do not delete an item if it is currently a new item being created to avoid a crash // In this case the selection contains only one item. DS_DRAW_ITEM_BASE* currItem = static_cast( selection.Front() ); if( currItem->GetFlags() & ( IS_NEW ) ) return 0; m_frame->SaveCopyInUndoList(); while( selection.Front() ) { DS_DRAW_ITEM_BASE* drawItem = static_cast( selection.Front() ); DS_DATA_ITEM* dataItem = drawItem->GetPeer(); DS_DATA_MODEL::GetTheInstance().Remove( dataItem ); for( DS_DRAW_ITEM_BASE* item : dataItem->GetDrawItems() ) { // Note: repeat items won't be selected but must be removed & deleted if( item->IsSelected() ) m_selectionTool->RemoveItemFromSel( item ); getView()->Remove( item ); } delete dataItem; } m_frame->OnModify(); return 0; } #define HITTEST_THRESHOLD_PIXELS 5 int PL_EDIT_TOOL::DeleteItemCursor( const TOOL_EVENT& aEvent ) { PICKER_TOOL* picker = m_toolMgr->GetTool(); // Deactivate other tools; particularly important if another PICKER is currently running Activate(); picker->SetCursor( KICURSOR::REMOVE ); m_pickerItem = nullptr; picker->SetClickHandler( [this] ( const VECTOR2D& aPosition ) -> bool { if( m_pickerItem ) { PL_SELECTION_TOOL* selectionTool = m_toolMgr->GetTool(); selectionTool->UnbrightenItem( m_pickerItem ); selectionTool->AddItemToSel( m_pickerItem, true /*quiet mode*/ ); m_toolMgr->RunAction( ACTIONS::doDelete, true ); m_pickerItem = nullptr; } return true; } ); picker->SetMotionHandler( [this] ( const VECTOR2D& aPos ) { int threshold = KiROUND( getView()->ToWorld( HITTEST_THRESHOLD_PIXELS ) ); EDA_ITEM* item = nullptr; for( DS_DATA_ITEM* dataItem : DS_DATA_MODEL::GetTheInstance().GetItems() ) { for( DS_DRAW_ITEM_BASE* drawItem : dataItem->GetDrawItems() ) { if( drawItem->HitTest( aPos, threshold ) ) { item = drawItem; break; } } } if( m_pickerItem != item ) { PL_SELECTION_TOOL* selectionTool = m_toolMgr->GetTool(); if( m_pickerItem ) selectionTool->UnbrightenItem( m_pickerItem ); m_pickerItem = item; if( m_pickerItem ) selectionTool->BrightenItem( m_pickerItem ); } } ); picker->SetFinalizeHandler( [this] ( const int& aFinalState ) { if( m_pickerItem ) m_toolMgr->GetTool()->UnbrightenItem( m_pickerItem ); // Wake the selection tool after exiting to ensure the cursor gets updated m_toolMgr->RunAction( PL_ACTIONS::selectionActivate, false ); } ); m_toolMgr->RunAction( ACTIONS::pickerTool, true ); return 0; } int PL_EDIT_TOOL::Undo( const TOOL_EVENT& aEvent ) { m_frame->GetLayoutFromUndoList(); return 0; } int PL_EDIT_TOOL::Redo( const TOOL_EVENT& aEvent ) { m_frame->GetLayoutFromRedoList(); return 0; } int PL_EDIT_TOOL::Cut( const TOOL_EVENT& aEvent ) { int retVal = Copy( aEvent ); if( retVal == 0 ) retVal = DoDelete( aEvent ); return retVal; } int PL_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent ) { PL_SELECTION& selection = m_selectionTool->RequestSelection(); std::vector items; DS_DATA_MODEL& model = DS_DATA_MODEL::GetTheInstance(); wxString sexpr; if( selection.GetSize() == 0 ) return 0; for( EDA_ITEM* item : selection.GetItems() ) items.push_back( static_cast( item )->GetPeer() ); try { model.SaveInString( items, &sexpr ); } catch( const IO_ERROR& ioe ) { wxMessageBox( ioe.What(), _( "Error writing objects to clipboard" ) ); } if( m_toolMgr->SaveClipboard( TO_UTF8( sexpr ) ) ) return 0; else return -1; } int PL_EDIT_TOOL::Paste( const TOOL_EVENT& aEvent ) { PL_SELECTION& selection = m_selectionTool->GetSelection(); DS_DATA_MODEL& model = DS_DATA_MODEL::GetTheInstance(); std::string sexpr = m_toolMgr->GetClipboardUTF8(); m_selectionTool->ClearSelection(); model.SetPageLayout( sexpr.c_str(), true, wxT( "clipboard" ) ); // Build out draw items and select the first of each data item for( DS_DATA_ITEM* dataItem : model.GetItems() ) { if( dataItem->GetDrawItems().empty() ) { dataItem->SyncDrawItems( nullptr, getView() ); dataItem->GetDrawItems().front()->SetSelected(); } } m_selectionTool->RebuildSelection(); if( !selection.Empty() ) { selection.SetReferencePoint( selection.GetTopLeftItem()->GetPosition() ); m_toolMgr->RunAction( PL_ACTIONS::move, false ); } return 0; } void PL_EDIT_TOOL::setTransitions() { Go( &PL_EDIT_TOOL::Main, PL_ACTIONS::move.MakeEvent() ); Go( &PL_EDIT_TOOL::ImportDrawingSheetContent, PL_ACTIONS::appendImportedDrawingSheet.MakeEvent() ); Go( &PL_EDIT_TOOL::Undo, ACTIONS::undo.MakeEvent() ); Go( &PL_EDIT_TOOL::Redo, ACTIONS::redo.MakeEvent() ); Go( &PL_EDIT_TOOL::Cut, ACTIONS::cut.MakeEvent() ); Go( &PL_EDIT_TOOL::Copy, ACTIONS::copy.MakeEvent() ); Go( &PL_EDIT_TOOL::Paste, ACTIONS::paste.MakeEvent() ); Go( &PL_EDIT_TOOL::DoDelete, ACTIONS::doDelete.MakeEvent() ); Go( &PL_EDIT_TOOL::DeleteItemCursor, ACTIONS::deleteTool.MakeEvent() ); }