mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-02-18 22:39:45 +00:00
Recommendation is to avoid using the year nomenclature as this information is already encoded in the git repo. Avoids needing to repeatly update. Also updates AUTHORS.txt from current repo with contributor names
609 lines
18 KiB
C++
609 lines
18 KiB
C++
/*
|
|
* 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 <wx/log.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <fmt/format.h>
|
|
|
|
#include <tool/tool_manager.h>
|
|
#include <tool/picker_tool.h>
|
|
#include <drawing_sheet/ds_data_item.h>
|
|
#include <drawing_sheet/ds_data_model.h>
|
|
#include <drawing_sheet/ds_draw_item.h>
|
|
#include <bitmaps.h>
|
|
#include <clipboard.h>
|
|
#include <confirm.h>
|
|
#include <eda_item.h>
|
|
#include <macros.h>
|
|
#include <string_utils.h>
|
|
#include <view/view.h>
|
|
#include <view/view_controls.h>
|
|
#include <math/util.h> // 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<PL_EDITOR_FRAME>();
|
|
m_selectionTool = m_toolMgr->GetTool<PL_SELECTION_TOOL>();
|
|
|
|
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.get() );
|
|
|
|
//
|
|
// 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<PL_EDITOR_FRAME>();
|
|
}
|
|
|
|
|
|
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<DS_DATA_ITEM*> unique_peers;
|
|
|
|
for( EDA_ITEM* item : selection )
|
|
{
|
|
DS_DRAW_ITEM_BASE* drawItem = static_cast<DS_DRAW_ITEM_BASE*>( 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::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 <ESC> 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 );
|
|
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->MoveToIU( aItem->GetStartPosIU() + 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 );
|
|
|
|
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<DS_DRAW_ITEM_BASE*>( selection.Front() );
|
|
|
|
if( currItem->GetFlags() & ( IS_NEW ) )
|
|
return 0;
|
|
|
|
m_frame->SaveCopyInUndoList();
|
|
|
|
while( selection.Front() )
|
|
{
|
|
DS_DRAW_ITEM_BASE* drawItem = static_cast<DS_DRAW_ITEM_BASE*>( 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::InteractiveDelete( const TOOL_EVENT& aEvent )
|
|
{
|
|
PICKER_TOOL* picker = m_toolMgr->GetTool<PICKER_TOOL>();
|
|
|
|
// 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<PL_SELECTION_TOOL>();
|
|
selectionTool->UnbrightenItem( m_pickerItem );
|
|
selectionTool->AddItemToSel( m_pickerItem, true /*quiet mode*/ );
|
|
m_toolMgr->RunAction( ACTIONS::doDelete );
|
|
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<PL_SELECTION_TOOL>();
|
|
|
|
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<PL_SELECTION_TOOL>()->UnbrightenItem( m_pickerItem );
|
|
|
|
// Wake the selection tool after exiting to ensure the cursor gets updated
|
|
m_toolMgr->PostAction( PL_ACTIONS::selectionActivate );
|
|
} );
|
|
|
|
m_toolMgr->RunAction( ACTIONS::pickerTool, &aEvent );
|
|
|
|
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<DS_DATA_ITEM*> 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<DS_DRAW_ITEM_BASE*>( item )->GetPeer() );
|
|
|
|
try
|
|
{
|
|
model.SaveInString( items, &sexpr );
|
|
}
|
|
catch( const IO_ERROR& ioe )
|
|
{
|
|
wxMessageBox( ioe.What(), _( "Error writing objects to clipboard" ) );
|
|
}
|
|
|
|
if( 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();
|
|
bool createdAnything = false;
|
|
|
|
if( std::unique_ptr<wxImage> clipImg = GetImageFromClipboard() )
|
|
{
|
|
auto image = std::make_unique<BITMAP_BASE>();
|
|
image->SetImage( *clipImg );
|
|
auto dataItem = std::make_unique<DS_DATA_ITEM_BITMAP>( image.release() );
|
|
model.Append( dataItem.release() );
|
|
|
|
createdAnything = true;
|
|
}
|
|
else
|
|
{
|
|
m_selectionTool->ClearSelection();
|
|
|
|
const std::string clipText = GetClipboardUTF8();
|
|
model.SetPageLayout( clipText.c_str(), true, wxT( "clipboard" ) );
|
|
createdAnything = true;
|
|
}
|
|
|
|
// Nothing pasteable
|
|
if( !createdAnything )
|
|
return 0;
|
|
|
|
// 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->PostAction( PL_ACTIONS::move );
|
|
}
|
|
|
|
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::InteractiveDelete, ACTIONS::deleteTool.MakeEvent() );
|
|
}
|