/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Jon Evans * Copyright (C) 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 3 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, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace kiapi::common::commands; using types::CommandStatus; using types::DocumentType; using types::ItemRequestStatus; API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* aFrame ) : API_HANDLER_EDITOR( aFrame ) { registerHandler( &API_HANDLER_PCB::handleRunAction ); registerHandler( &API_HANDLER_PCB::handleGetOpenDocuments ); registerHandler( &API_HANDLER_PCB::handleGetItems ); registerHandler( &API_HANDLER_PCB::handleGetStackup ); registerHandler( &API_HANDLER_PCB::handleGetGraphicsDefaults ); registerHandler( &API_HANDLER_PCB::handleGetBoundingBox ); registerHandler( &API_HANDLER_PCB::handleGetPadShapeAsPolygon ); registerHandler( &API_HANDLER_PCB::handleGetTitleBlockInfo ); registerHandler( &API_HANDLER_PCB::handleExpandTextVariables ); registerHandler( &API_HANDLER_PCB::handleInteractiveMoveItems ); registerHandler( &API_HANDLER_PCB::handleGetNets ); registerHandler( &API_HANDLER_PCB::handleRefillZones ); registerHandler( &API_HANDLER_PCB::handleSaveDocumentToString ); registerHandler( &API_HANDLER_PCB::handleSaveSelectionToString ); registerHandler( &API_HANDLER_PCB::handleParseAndCreateItemsFromString ); } PCB_EDIT_FRAME* API_HANDLER_PCB::frame() const { return static_cast( m_frame ); } HANDLER_RESULT API_HANDLER_PCB::handleRunAction( RunAction& aRequest, const HANDLER_CONTEXT& ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); RunActionResponse response; if( frame()->GetToolManager()->RunAction( aRequest.action(), true ) ) response.set_status( RunActionStatus::RAS_OK ); else response.set_status( RunActionStatus::RAS_INVALID ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetOpenDocuments( GetOpenDocuments& aMsg, const HANDLER_CONTEXT& ) { if( aMsg.type() != DocumentType::DOCTYPE_PCB ) { ApiResponseStatus e; // No message needed for AS_UNHANDLED; this is an internal flag for the API server e.set_status( ApiStatusCode::AS_UNHANDLED ); return tl::unexpected( e ); } GetOpenDocumentsResponse response; common::types::DocumentSpecifier doc; wxFileName fn( frame()->GetCurrentFileName() ); doc.set_type( DocumentType::DOCTYPE_PCB ); doc.set_board_filename( fn.GetFullName() ); doc.mutable_project()->set_name( frame()->Prj().GetProjectName().ToStdString() ); doc.mutable_project()->set_path( frame()->Prj().GetProjectDirectory().ToStdString() ); response.mutable_documents()->Add( std::move( doc ) ); return response; } void API_HANDLER_PCB::pushCurrentCommit( const HANDLER_CONTEXT& aCtx, const wxString& aMessage ) { API_HANDLER_EDITOR::pushCurrentCommit( aCtx, aMessage ); frame()->Refresh(); } std::unique_ptr API_HANDLER_PCB::createCommit() { return std::make_unique( frame() ); } std::optional API_HANDLER_PCB::getItemById( const KIID& aId ) const { BOARD_ITEM* item = frame()->GetBoard()->GetItem( aId ); if( item == DELETED_BOARD_ITEM::GetInstance() ) return std::nullopt; return item; } bool API_HANDLER_PCB::validateDocumentInternal( const DocumentSpecifier& aDocument ) const { if( aDocument.type() != DocumentType::DOCTYPE_PCB ) return false; wxFileName fn( frame()->GetCurrentFileName() ); return 0 == aDocument.board_filename().compare( fn.GetFullName() ); } HANDLER_RESULT> API_HANDLER_PCB::createItemForType( KICAD_T aType, BOARD_ITEM_CONTAINER* aContainer ) { if( !aContainer ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "Tried to create an item in a null container" ); return tl::unexpected( e ); } if( aType == PCB_PAD_T && !dynamic_cast( aContainer ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create a pad in {}, which is not a footprint", aContainer->GetFriendlyName().ToStdString() ) ); return tl::unexpected( e ); } else if( aType == PCB_FOOTPRINT_T && !dynamic_cast( aContainer ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create a footprint in {}, which is not a board", aContainer->GetFriendlyName().ToStdString() ) ); return tl::unexpected( e ); } std::unique_ptr created = CreateItemForType( aType, aContainer ); if( !created ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create an item of type {}, which is unhandled", magic_enum::enum_name( aType ) ) ); return tl::unexpected( e ); } return created; } HANDLER_RESULT API_HANDLER_PCB::handleCreateUpdateItemsInternal( bool aCreate, const HANDLER_CONTEXT& aCtx, const types::ItemHeader &aHeader, const google::protobuf::RepeatedPtrField& aItems, std::function aItemHandler ) { ApiResponseStatus e; auto containerResult = validateItemHeaderDocument( aHeader ); if( !containerResult && containerResult.error().status() == ApiStatusCode::AS_UNHANDLED ) { // No message needed for AS_UNHANDLED; this is an internal flag for the API server e.set_status( ApiStatusCode::AS_UNHANDLED ); return tl::unexpected( e ); } else if( !containerResult ) { e.CopyFrom( containerResult.error() ); return tl::unexpected( e ); } BOARD* board = frame()->GetBoard(); BOARD_ITEM_CONTAINER* container = board; if( containerResult->has_value() ) { const KIID& containerId = **containerResult; std::optional optItem = getItemById( containerId ); if( optItem ) { container = dynamic_cast( *optItem ); if( !container ) { e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "The requested container {} is not a valid board item container", containerId.AsStdString() ) ); return tl::unexpected( e ); } } else { e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "The requested container {} does not exist in this document", containerId.AsStdString() ) ); return tl::unexpected( e ); } } BOARD_COMMIT* commit = static_cast( getCurrentCommit( aCtx ) ); for( const google::protobuf::Any& anyItem : aItems ) { ItemStatus status; std::optional type = TypeNameFromAny( anyItem ); if( !type ) { status.set_code( ItemStatusCode::ISC_INVALID_TYPE ); status.set_error_message( fmt::format( "Could not decode a valid type from {}", anyItem.type_url() ) ); aItemHandler( status, anyItem ); continue; } if( type == PCB_DIMENSION_T ) { board::types::Dimension dimension; anyItem.UnpackTo( &dimension ); switch( dimension.dimension_style_case() ) { case board::types::Dimension::kAligned: type = PCB_DIM_ALIGNED_T; break; case board::types::Dimension::kOrthogonal: type = PCB_DIM_ORTHOGONAL_T; break; case board::types::Dimension::kRadial: type = PCB_DIM_RADIAL_T; break; case board::types::Dimension::kLeader: type = PCB_DIM_LEADER_T; break; case board::types::Dimension::kCenter: type = PCB_DIM_CENTER_T; break; case board::types::Dimension::DIMENSION_STYLE_NOT_SET: break; } } HANDLER_RESULT> creationResult = createItemForType( *type, container ); if( !creationResult ) { status.set_code( ItemStatusCode::ISC_INVALID_TYPE ); status.set_error_message( creationResult.error().error_message() ); aItemHandler( status, anyItem ); continue; } std::unique_ptr item( std::move( *creationResult ) ); if( !item->Deserialize( anyItem ) ) { e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "could not unpack {} from request", item->GetClass().ToStdString() ) ); return tl::unexpected( e ); } std::optional optItem = getItemById( item->m_Uuid ); if( aCreate && optItem ) { status.set_code( ItemStatusCode::ISC_EXISTING ); status.set_error_message( fmt::format( "an item with UUID {} already exists", item->m_Uuid.AsStdString() ) ); aItemHandler( status, anyItem ); continue; } else if( !aCreate && !optItem ) { status.set_code( ItemStatusCode::ISC_NONEXISTENT ); status.set_error_message( fmt::format( "an item with UUID {} does not exist", item->m_Uuid.AsStdString() ) ); aItemHandler( status, anyItem ); continue; } if( aCreate && !board->GetEnabledLayers().Contains( item->GetLayer() ) ) { status.set_code( ItemStatusCode::ISC_INVALID_DATA ); status.set_error_message( fmt::format( "attempted to add item on disabled layer {}", LayerName( item->GetLayer() ).ToStdString() ) ); aItemHandler( status, anyItem ); continue; } status.set_code( ItemStatusCode::ISC_OK ); google::protobuf::Any newItem; if( aCreate ) { item->Serialize( newItem ); commit->Add( item.release() ); } else { BOARD_ITEM* boardItem = *optItem; commit->Modify( boardItem ); boardItem->SwapItemData( item.get() ); boardItem->Serialize( newItem ); } aItemHandler( status, newItem ); } if( !m_activeClients.count( aCtx.ClientName ) ) { pushCurrentCommit( aCtx, aCreate ? _( "Created items via API" ) : _( "Added items via API" ) ); } return ItemRequestStatus::IRS_OK; } HANDLER_RESULT API_HANDLER_PCB::handleGetItems( GetItems& aMsg, const HANDLER_CONTEXT& ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aMsg.header() ) ) { ApiResponseStatus e; // No message needed for AS_UNHANDLED; this is an internal flag for the API server e.set_status( ApiStatusCode::AS_UNHANDLED ); return tl::unexpected( e ); } GetItemsResponse response; BOARD* board = frame()->GetBoard(); std::vector items; std::set typesRequested, typesInserted; bool handledAnything = false; for( int typeRaw : aMsg.types() ) { auto typeMessage = static_cast( typeRaw ); KICAD_T type = FromProtoEnum( typeMessage ); if( type == TYPE_NOT_INIT ) continue; typesRequested.emplace( type ); if( typesInserted.count( type ) ) continue; switch( type ) { case PCB_TRACE_T: case PCB_ARC_T: case PCB_VIA_T: handledAnything = true; std::copy( board->Tracks().begin(), board->Tracks().end(), std::back_inserter( items ) ); typesInserted.insert( { PCB_TRACE_T, PCB_ARC_T, PCB_VIA_T } ); break; case PCB_PAD_T: { handledAnything = true; for( FOOTPRINT* fp : board->Footprints() ) { std::copy( fp->Pads().begin(), fp->Pads().end(), std::back_inserter( items ) ); } typesInserted.insert( PCB_PAD_T ); break; } case PCB_FOOTPRINT_T: { handledAnything = true; std::copy( board->Footprints().begin(), board->Footprints().end(), std::back_inserter( items ) ); typesInserted.insert( PCB_FOOTPRINT_T ); break; } case PCB_SHAPE_T: case PCB_TEXT_T: case PCB_TEXTBOX_T: { handledAnything = true; bool inserted = false; for( BOARD_ITEM* item : board->Drawings() ) { if( item->Type() == type ) { items.emplace_back( item ); inserted = true; } } if( inserted ) typesInserted.insert( PCB_SHAPE_T ); break; } case PCB_ZONE_T: { handledAnything = true; std::copy( board->Zones().begin(), board->Zones().end(), std::back_inserter( items ) ); typesInserted.insert( PCB_ZONE_T ); break; } default: break; } } if( !handledAnything ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "none of the requested types are valid for a Board object" ); return tl::unexpected( e ); } for( const BOARD_ITEM* item : items ) { if( !typesRequested.count( item->Type() ) ) continue; google::protobuf::Any itemBuf; item->Serialize( itemBuf ); response.mutable_items()->Add( std::move( itemBuf ) ); } response.set_status( ItemRequestStatus::IRS_OK ); return response; } void API_HANDLER_PCB::deleteItemsInternal( std::map& aItemsToDelete, const HANDLER_CONTEXT& aCtx ) { BOARD* board = frame()->GetBoard(); std::vector validatedItems; for( std::pair pair : aItemsToDelete ) { if( BOARD_ITEM* item = board->GetItem( pair.first ) ) { validatedItems.push_back( item ); aItemsToDelete[pair.first] = ItemDeletionStatus::IDS_OK; } // Note: we don't currently support locking items from API modification, but here is where // to add it in the future (and return IDS_IMMUTABLE) } COMMIT* commit = getCurrentCommit( aCtx ); for( BOARD_ITEM* item : validatedItems ) commit->Remove( item ); if( !m_activeClients.count( aCtx.ClientName ) ) pushCurrentCommit( aCtx, _( "Deleted items via API" ) ); } std::optional API_HANDLER_PCB::getItemFromDocument( const DocumentSpecifier& aDocument, const KIID& aId ) { if( !validateDocument( aDocument ) ) return std::nullopt; return getItemById( aId ); } HANDLER_RESULT API_HANDLER_PCB::handleGetStackup( GetBoardStackup& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BoardStackupResponse response; google::protobuf::Any any; frame()->GetBoard()->GetStackupOrDefault().Serialize( any ); any.UnpackTo( response.mutable_stackup() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetGraphicsDefaults( GetGraphicsDefaults& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); const BOARD_DESIGN_SETTINGS& bds = frame()->GetBoard()->GetDesignSettings(); GraphicsDefaultsResponse response; // TODO: This should change to be an enum class constexpr std::array classOrder = { kiapi::board::BLC_SILKSCREEN, kiapi::board::BLC_COPPER, kiapi::board::BLC_EDGES, kiapi::board::BLC_COURTYARD, kiapi::board::BLC_FABRICATION, kiapi::board::BLC_OTHER }; for( int i = 0; i < LAYER_CLASS_COUNT; ++i ) { kiapi::board::BoardLayerGraphicsDefaults* l = response.mutable_defaults()->add_layers(); l->set_layer( classOrder[i] ); l->mutable_line_thickness()->set_value_nm( bds.m_LineThickness[i] ); kiapi::common::types::TextAttributes* text = l->mutable_text(); text->mutable_size()->set_x_nm( bds.m_TextSize[i].x ); text->mutable_size()->set_y_nm( bds.m_TextSize[i].y ); text->mutable_stroke_width()->set_value_nm( bds.m_TextThickness[i] ); text->set_italic( bds.m_TextItalic[i] ); text->set_keep_upright( bds.m_TextUpright[i] ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetBoundingBox( GetBoundingBox& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aMsg.header() ) ) { ApiResponseStatus e; // No message needed for AS_UNHANDLED; this is an internal flag for the API server e.set_status( ApiStatusCode::AS_UNHANDLED ); return tl::unexpected( e ); } GetBoundingBoxResponse response; bool includeText = aMsg.mode() == BoundingBoxMode::BBM_ITEM_AND_CHILD_TEXT; for( const types::KIID& idMsg : aMsg.items() ) { KIID id( idMsg.value() ); std::optional optItem = getItemById( id ); if( !optItem ) continue; BOARD_ITEM* item = *optItem; BOX2I bbox; if( item->Type() == PCB_FOOTPRINT_T ) bbox = static_cast( item )->GetBoundingBox( includeText ); else bbox = item->GetBoundingBox(); response.add_items()->set_value( idMsg.value() ); PackBox2( *response.add_boxes(), bbox ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetPadShapeAsPolygon( GetPadShapeAsPolygon& aMsg, const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); SHAPE_POLY_SET poly; PadShapeAsPolygonResponse response; PCB_LAYER_ID layer = FromProtoEnum( aMsg.layer() ); for( const types::KIID& padRequest : aMsg.pads() ) { KIID id( padRequest.value() ); std::optional optPad = getItemById( id ); if( !optPad || ( *optPad )->Type() != PCB_PAD_T ) continue; response.add_pads()->set_value( padRequest.value() ); PAD* pad = static_cast( *optPad ); pad->TransformShapeToPolygon( poly, pad->Padstack().EffectiveLayerFor( layer ), 0, ARC_HIGH_DEF, ERROR_INSIDE ); types::PolygonWithHoles* polyMsg = response.mutable_polygons()->Add(); PackPolyLine( *polyMsg->mutable_outline(), poly.COutline( 0 ) ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetTitleBlockInfo( GetTitleBlockInfo& aMsg, const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aMsg.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BOARD* board = frame()->GetBoard(); const TITLE_BLOCK& block = board->GetTitleBlock(); types::TitleBlockInfo response; response.set_title( block.GetTitle().ToUTF8() ); response.set_date( block.GetDate().ToUTF8() ); response.set_revision( block.GetRevision().ToUTF8() ); response.set_company( block.GetCompany().ToUTF8() ); response.set_comment1( block.GetComment( 0 ).ToUTF8() ); response.set_comment2( block.GetComment( 1 ).ToUTF8() ); response.set_comment3( block.GetComment( 2 ).ToUTF8() ); response.set_comment4( block.GetComment( 3 ).ToUTF8() ); response.set_comment5( block.GetComment( 4 ).ToUTF8() ); response.set_comment6( block.GetComment( 5 ).ToUTF8() ); response.set_comment7( block.GetComment( 6 ).ToUTF8() ); response.set_comment8( block.GetComment( 7 ).ToUTF8() ); response.set_comment9( block.GetComment( 8 ).ToUTF8() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleExpandTextVariables( ExpandTextVariables& aMsg, const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aMsg.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); ExpandTextVariablesResponse reply; BOARD* board = frame()->GetBoard(); std::function textResolver = [&]( wxString* token ) -> bool { // Handles m_board->GetTitleBlock() *and* m_board->GetProject() return board->ResolveTextVar( token, 0 ); }; for( const std::string& textMsg : aMsg.text() ) { wxString text = ExpandTextVars( wxString::FromUTF8( textMsg ), &textResolver ); reply.add_text( text.ToUTF8() ); } return reply; } HANDLER_RESULT API_HANDLER_PCB::handleInteractiveMoveItems( InteractiveMoveItems& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); TOOL_MANAGER* mgr = frame()->GetToolManager(); std::vector toSelect; for( const kiapi::common::types::KIID& id : aMsg.items() ) { if( std::optional item = getItemById( KIID( id.value() ) ) ) toSelect.emplace_back( static_cast( *item ) ); } if( toSelect.empty() ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "None of the given items exist on the board", aMsg.board().board_filename() ) ); return tl::unexpected( e ); } PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); selectionTool->GetSelection().SetReferencePoint( toSelect[0]->GetPosition() ); mgr->RunAction( PCB_ACTIONS::selectionClear ); mgr->RunAction( PCB_ACTIONS::selectItems, &toSelect ); COMMIT* commit = getCurrentCommit( aCtx ); mgr->PostAction( PCB_ACTIONS::move, commit ); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetNets( GetNets& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); NetsResponse response; BOARD* board = frame()->GetBoard(); std::set netclassFilter; for( const std::string& nc : aMsg.netclass_filter() ) netclassFilter.insert( wxString( nc.c_str(), wxConvUTF8 ) ); for( NETINFO_ITEM* net : board->GetNetInfo() ) { NETCLASS* nc = net->GetNetClass(); if( !netclassFilter.empty() && nc && !netclassFilter.count( nc->GetName() ) ) continue; board::types::Net* netProto = response.add_nets(); netProto->set_name( net->GetNetname() ); netProto->mutable_code()->set_value( net->GetNetCode() ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleRefillZones( RefillZones& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); if( aMsg.zones().empty() ) { TOOL_MANAGER* mgr = frame()->GetToolManager(); frame()->CallAfter( [mgr]() { mgr->RunAction( PCB_ACTIONS::zoneFillAll ); } ); } else { // TODO ApiResponseStatus e; e.set_status( ApiStatusCode::AS_UNIMPLEMENTED ); return tl::unexpected( e ); } return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleSaveDocumentToString( SaveDocumentToString& aMsg, const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aMsg.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); SavedDocumentResponse response; response.mutable_document()->CopyFrom( aMsg.document() ); CLIPBOARD_IO io; io.SetWriter( [&]( const wxString& aData ) { response.set_contents( aData.ToUTF8() ); } ); io.SaveBoard( wxEmptyString, frame()->GetBoard(), nullptr ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleSaveSelectionToString( SaveSelectionToString& aMsg, const HANDLER_CONTEXT& aCtx ) { SavedSelectionResponse response; TOOL_MANAGER* mgr = frame()->GetToolManager(); PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); PCB_SELECTION& selection = selectionTool->GetSelection(); CLIPBOARD_IO io; io.SetWriter( [&]( const wxString& aData ) { response.set_contents( aData.ToUTF8() ); } ); io.SetBoard( frame()->GetBoard() ); io.SaveSelection( selection, false ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleParseAndCreateItemsFromString( ParseAndCreateItemsFromString& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); CreateItemsResponse response; return response; }