/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2017-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 "pcbnew_action_plugins.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../scripting/python_scripting.h" PYTHON_ACTION_PLUGIN::PYTHON_ACTION_PLUGIN( PyObject* aAction ) { PyLOCK lock; m_PyAction = aAction; Py_XINCREF( aAction ); } PYTHON_ACTION_PLUGIN::~PYTHON_ACTION_PLUGIN() { PyLOCK lock; Py_XDECREF( m_PyAction ); } PyObject* PYTHON_ACTION_PLUGIN::CallMethod( const char* aMethod, PyObject* aArglist ) { PyLOCK lock; PyErr_Clear(); // pFunc is a new reference to the desired method PyObject* pFunc = PyObject_GetAttrString( m_PyAction, aMethod ); if( pFunc && PyCallable_Check( pFunc ) ) { PyObject* result = PyObject_CallObject( pFunc, aArglist ); if( !wxTheApp ) KIPLATFORM::APP::AttachConsole( true ); if( PyErr_Occurred() ) { wxString message = _HKI( "Exception on python action plugin code" ); wxString traceback = PyErrStringWithTraceback(); std::cerr << message << std::endl << std::endl; std::cerr << traceback << std::endl; if( wxTheApp ) wxSafeShowMessage( wxGetTranslation( message ), traceback ); } if( !wxTheApp ) { wxArrayString messages; messages.Add( "Fatal error"); messages.Add( wxString::Format( "The application handle was destroyed after running Python plugin '%s'.", m_cachedName ) ); // Poor man's ASCII message box { int maxLen = 0; for( const wxString& msg : messages ) if( (int)msg.length() > maxLen ) maxLen = msg.length(); wxChar ch = '*'; wxString border( ch, 3 ); std::cerr << wxString( ch, maxLen + 2 + border.length() * 2 ) << std::endl; std::cerr << border << ' ' << wxString( ' ', maxLen ) << ' ' << border << std::endl; for( wxString msg : messages ) std::cerr << border << ' ' << msg.Pad( maxLen - msg.length() ) << ' ' << border << std::endl; std::cerr << border << ' ' << wxString( ' ', maxLen ) << ' ' << border << std::endl; std::cerr << wxString( ch, maxLen + 2 + border.length() * 2 ) << std::endl; } #ifdef _WIN32 std::cerr << std::endl << "Press any key to abort..." << std::endl; (void) std::getchar(); #endif abort(); } if( result ) { Py_XDECREF( pFunc ); return result; } } else { wxString msg = wxString::Format( _( "Method '%s' not found, or not callable" ), aMethod ); wxMessageBox( msg, _( "Unknown Method" ), wxICON_ERROR | wxOK ); } if( pFunc ) { Py_XDECREF( pFunc ); } return nullptr; } wxString PYTHON_ACTION_PLUGIN::CallRetStrMethod( const char* aMethod, PyObject* aArglist ) { wxString ret; PyLOCK lock; PyObject* result = CallMethod( aMethod, aArglist ); ret = PyStringToWx( result ); Py_XDECREF( result ); return ret; } wxString PYTHON_ACTION_PLUGIN::GetCategoryName() { PyLOCK lock; return CallRetStrMethod( "GetCategoryName" ); } wxString PYTHON_ACTION_PLUGIN::GetName() { PyLOCK lock; wxString name = CallRetStrMethod( "GetName" ); m_cachedName = name; return name; } wxString PYTHON_ACTION_PLUGIN::GetDescription() { PyLOCK lock; return CallRetStrMethod( "GetDescription" ); } bool PYTHON_ACTION_PLUGIN::GetShowToolbarButton() { PyLOCK lock; PyObject* result = CallMethod( "GetShowToolbarButton"); return PyObject_IsTrue(result); } wxString PYTHON_ACTION_PLUGIN::GetIconFileName( bool aDark ) { PyLOCK lock; PyObject* arglist = Py_BuildValue( "(i)", static_cast( aDark ) ); wxString result = CallRetStrMethod( "GetIconFileName", arglist ); Py_DECREF( arglist ); return result; } wxString PYTHON_ACTION_PLUGIN::GetPluginPath() { PyLOCK lock; return CallRetStrMethod( "GetPluginPath" ); } void PYTHON_ACTION_PLUGIN::Run() { PyLOCK lock; CallMethod( "Run" ); } void* PYTHON_ACTION_PLUGIN::GetObject() { return (void*) m_PyAction; } void PYTHON_ACTION_PLUGINS::register_action( PyObject* aPyAction ) { PYTHON_ACTION_PLUGIN* fw = new PYTHON_ACTION_PLUGIN( aPyAction ); fw->register_action(); } void PYTHON_ACTION_PLUGINS::deregister_action( PyObject* aPyAction ) { // deregister also destroys the previously created "PYTHON_ACTION_PLUGIN object" ACTION_PLUGINS::deregister_object( (void*) aPyAction ); } void PCB_EDIT_FRAME::OnActionPluginMenu( wxCommandEvent& aEvent ) { ACTION_PLUGIN* actionPlugin = ACTION_PLUGINS::GetActionByMenu( aEvent.GetId() ); if( actionPlugin ) RunActionPlugin( actionPlugin ); } void PCB_EDIT_FRAME::OnActionPluginButton( wxCommandEvent& aEvent ) { ACTION_PLUGIN* actionPlugin = ACTION_PLUGINS::GetActionByButton( aEvent.GetId() ); if( actionPlugin ) RunActionPlugin( actionPlugin ); } void PCB_EDIT_FRAME::RunActionPlugin( ACTION_PLUGIN* aActionPlugin ) { PICKED_ITEMS_LIST itemsList; BOARD* currentPcb = GetBoard(); bool fromEmpty = false; // Append tracks: for( PCB_TRACK* item : currentPcb->Tracks() ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::CHANGED ); itemsList.PushItem( picker ); } // Append footprints: for( FOOTPRINT* item : currentPcb->Footprints() ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::CHANGED ); itemsList.PushItem( picker ); } // Append drawings for( BOARD_ITEM* item : currentPcb->Drawings() ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::CHANGED ); itemsList.PushItem( picker ); } // Append zones outlines for( ZONE* zone : currentPcb->Zones() ) { ITEM_PICKER picker( nullptr, zone, UNDO_REDO::CHANGED ); itemsList.PushItem( picker ); } if( itemsList.GetCount() > 0 ) SaveCopyInUndoList( itemsList, UNDO_REDO::CHANGED ); else fromEmpty = true; itemsList.ClearItemsList(); BOARD_COMMIT commit( this ); // Execute plugin itself... ACTION_PLUGINS::SetActionRunning( true ); aActionPlugin->Run(); ACTION_PLUGINS::SetActionRunning( false ); // Get back the undo buffer to fix some modifications PICKED_ITEMS_LIST* oldBuffer = nullptr; if( fromEmpty ) { oldBuffer = new PICKED_ITEMS_LIST(); } else { oldBuffer = PopCommandFromUndoList(); wxASSERT( oldBuffer ); } // Try do discover what was modified PICKED_ITEMS_LIST deletedItemsList; // The list of existing items after running the action script const std::set currItemList = currentPcb->GetItemSet(); // Found deleted items for( unsigned int i = 0; i < oldBuffer->GetCount(); i++ ) { BOARD_ITEM* item = (BOARD_ITEM*) oldBuffer->GetPickedItem( i ); ITEM_PICKER picker( nullptr, item, UNDO_REDO::DELETED ); wxASSERT( item ); if( currItemList.find( item ) == currItemList.end() ) { deletedItemsList.PushItem( picker ); commit.Removed( item ); } } // Mark deleted elements in undolist for( unsigned int i = 0; i < deletedItemsList.GetCount(); i++ ) { oldBuffer->PushItem( deletedItemsList.GetItemWrapper( i ) ); } // Find new footprints for( FOOTPRINT* item : currentPcb->Footprints() ) { if( !oldBuffer->ContainsItem( item ) ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::NEWITEM ); oldBuffer->PushItem( picker ); commit.Added( item ); } } for( PCB_TRACK* item : currentPcb->Tracks() ) { if( !oldBuffer->ContainsItem( item ) ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::NEWITEM ); oldBuffer->PushItem( picker ); commit.Added( item ); } } for( BOARD_ITEM* item : currentPcb->Drawings() ) { if( !oldBuffer->ContainsItem( item ) ) { ITEM_PICKER picker( nullptr, item, UNDO_REDO::NEWITEM ); oldBuffer->PushItem( picker ); commit.Added( item ); } } for( ZONE* zone : currentPcb->Zones() ) { if( !oldBuffer->ContainsItem( zone ) ) { ITEM_PICKER picker( nullptr, zone, UNDO_REDO::NEWITEM ); oldBuffer->PushItem( picker ); commit.Added( zone ); } } if( oldBuffer->GetCount() ) { OnModify(); PushCommandToUndoList( oldBuffer ); } else { delete oldBuffer; } // Apply changes, UndoList already handled commit.Push( _( "Apply action script" ), SKIP_UNDO | SKIP_SET_DIRTY ); RebuildAndRefresh(); } void PCB_EDIT_FRAME::RebuildAndRefresh() { // The list of existing items after running the action script const std::set items = GetBoard()->GetItemSet(); // Sync selection with items selection state SELECTION& selection = GetCurrentSelection(); PCB_SELECTION_TOOL* selTool = m_toolManager->GetTool(); EDA_ITEMS to_add; EDA_ITEMS to_remove; for( BOARD_ITEM* item : items ) { if( item->IsSelected() && !selection.Contains( item ) ) { item->ClearSelected(); // temporarily to_add.push_back( item ); } } for( EDA_ITEM* item : selection.GetItems() ) { if( !item->IsSelected() ) to_remove.push_back( static_cast( item ) ); } if( !to_add.empty() ) selTool->AddItemsToSel( &to_add ); if( !to_remove.empty() ) selTool->RemoveItemsFromSel( &to_remove ); m_pcb->BuildConnectivity(); PCB_DRAW_PANEL_GAL* canvas = GetCanvas(); canvas->GetView()->Clear(); canvas->GetView()->InitPreview(); canvas->GetGAL()->SetGridOrigin( VECTOR2D( m_pcb->GetDesignSettings().GetGridOrigin() ) ); canvas->DisplayBoard( m_pcb ); // allow tools to re-add their view items (selection previews, grids, etc.) if( m_toolManager ) m_toolManager->ResetTools( TOOL_BASE::REDRAW ); // reload the drawing-sheet SetPageSettings( m_pcb->GetPageSettings() ); canvas->SyncLayersVisibility( m_pcb ); canvas->Refresh(); } void PCB_EDIT_FRAME::buildActionPluginMenus( ACTION_MENU* actionMenu ) { if( !actionMenu ) // Should not occur. return; for( int ii = 0; ii < ACTION_PLUGINS::GetActionsCount(); ii++ ) { wxMenuItem* item; ACTION_PLUGIN* ap = ACTION_PLUGINS::GetAction( ii ); const wxBitmap& bitmap = ap->iconBitmap.IsOk() ? ap->iconBitmap : KiBitmap( BITMAPS::puzzle_piece ); item = AddMenuItem( actionMenu, wxID_ANY, ap->GetName(), ap->GetDescription(), bitmap ); Connect( item->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( PCB_EDIT_FRAME::OnActionPluginMenu ) ); ACTION_PLUGINS::SetActionMenu( ii, item->GetId() ); } } void PCB_EDIT_FRAME::AddActionPluginTools() { bool need_separator = true; const std::vector& orderedPlugins = GetOrderedActionPlugins(); for( ACTION_PLUGIN* ap : orderedPlugins ) { if( GetActionPluginButtonVisible( ap->GetPluginPath(), ap->GetShowToolbarButton() ) ) { if( need_separator ) { m_mainToolBar->AddScaledSeparator( this ); need_separator = false; } // Add button wxBitmap bitmap; if ( ap->iconBitmap.IsOk() ) bitmap = KiScaledBitmap( ap->iconBitmap, this ); else bitmap = KiScaledBitmap( BITMAPS::puzzle_piece, this ); wxAuiToolBarItem* button = m_mainToolBar->AddTool( wxID_ANY, wxEmptyString, bitmap, ap->GetName() ); Connect( button->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( PCB_EDIT_FRAME::OnActionPluginButton ) ); // Link action plugin to button ACTION_PLUGINS::SetActionButton( ap, button->GetId() ); } } } std::vector PCB_EDIT_FRAME::GetOrderedActionPlugins() { PCBNEW_SETTINGS* cfg = Pgm().GetSettingsManager().GetAppSettings(); std::vector plugins; std::vector orderedPlugins; for( int i = 0; i < ACTION_PLUGINS::GetActionsCount(); i++ ) plugins.push_back( ACTION_PLUGINS::GetAction( i ) ); // First add plugins that have entries in settings for( const auto& pair : cfg->m_VisibleActionPlugins ) { auto loc = std::find_if( plugins.begin(), plugins.end(), [pair] ( ACTION_PLUGIN* plugin ) { return plugin->GetPluginPath() == pair.first; } ); if( loc != plugins.end() ) { orderedPlugins.push_back( *loc ); plugins.erase( loc ); } } // Now append new plugins that have not been configured yet for( auto remaining_plugin : plugins ) orderedPlugins.push_back( remaining_plugin ); return orderedPlugins; } bool PCB_EDIT_FRAME::GetActionPluginButtonVisible( const wxString& aPluginPath, bool aPluginDefault ) { PCBNEW_SETTINGS* cfg = Pgm().GetSettingsManager().GetAppSettings(); for( const auto& entry : cfg->m_VisibleActionPlugins ) { if( entry.first == aPluginPath ) return entry.second; } // Plugin is not in settings, return default. return aPluginDefault; }