kicad-source/pcbnew/api/api_handler_pcb.cpp
Jeff Young f58fc0b952 Rewrite GROUP undo based on uuids.
This also removes the GROUP/UNGROUP-specific undo actions.

This also fixes a bunch of undo bugs when duplicating
group members, creating pins, etc.

This also fixes some undo bugs when dividing wires etc.

This also fixes some bugs with new sch items not
being created within an entered group.
2025-05-21 14:24:59 +01:00

1460 lines
48 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 Jon Evans <jon@craftyjon.com>
* 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 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 <http://www.gnu.org/licenses/>.
*/
#include <magic_enum.hpp>
#include <api/api_handler_pcb.h>
#include <api/api_pcb_utils.h>
#include <api/api_enums.h>
#include <api/api_utils.h>
#include <board_commit.h>
#include <board_design_settings.h>
#include <footprint.h>
#include <kicad_clipboard.h>
#include <netinfo.h>
#include <pad.h>
#include <pcb_edit_frame.h>
#include <pcb_group.h>
#include <pcb_reference_image.h>
#include <pcb_shape.h>
#include <pcb_text.h>
#include <pcb_textbox.h>
#include <pcb_track.h>
#include <pcbnew_id.h>
#include <project.h>
#include <tool/tool_manager.h>
#include <tools/pcb_actions.h>
#include <tools/pcb_selection_tool.h>
#include <zone.h>
#include <api/common/types/base_types.pb.h>
#include <widgets/appearance_controls.h>
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<RunAction, RunActionResponse>( &API_HANDLER_PCB::handleRunAction );
registerHandler<GetOpenDocuments, GetOpenDocumentsResponse>(
&API_HANDLER_PCB::handleGetOpenDocuments );
registerHandler<SaveDocument, Empty>( &API_HANDLER_PCB::handleSaveDocument );
registerHandler<SaveCopyOfDocument, Empty>( &API_HANDLER_PCB::handleSaveCopyOfDocument );
registerHandler<RevertDocument, Empty>( &API_HANDLER_PCB::handleRevertDocument );
registerHandler<GetItems, GetItemsResponse>( &API_HANDLER_PCB::handleGetItems );
registerHandler<GetSelection, SelectionResponse>( &API_HANDLER_PCB::handleGetSelection );
registerHandler<ClearSelection, Empty>( &API_HANDLER_PCB::handleClearSelection );
registerHandler<AddToSelection, SelectionResponse>( &API_HANDLER_PCB::handleAddToSelection );
registerHandler<RemoveFromSelection, SelectionResponse>(
&API_HANDLER_PCB::handleRemoveFromSelection );
registerHandler<GetBoardStackup, BoardStackupResponse>( &API_HANDLER_PCB::handleGetStackup );
registerHandler<GetGraphicsDefaults, GraphicsDefaultsResponse>(
&API_HANDLER_PCB::handleGetGraphicsDefaults );
registerHandler<GetBoundingBox, GetBoundingBoxResponse>(
&API_HANDLER_PCB::handleGetBoundingBox );
registerHandler<GetPadShapeAsPolygon, PadShapeAsPolygonResponse>(
&API_HANDLER_PCB::handleGetPadShapeAsPolygon );
registerHandler<GetTitleBlockInfo, types::TitleBlockInfo>(
&API_HANDLER_PCB::handleGetTitleBlockInfo );
registerHandler<ExpandTextVariables, ExpandTextVariablesResponse>(
&API_HANDLER_PCB::handleExpandTextVariables );
registerHandler<GetBoardOrigin, types::Vector2>( &API_HANDLER_PCB::handleGetBoardOrigin );
registerHandler<SetBoardOrigin, Empty>( &API_HANDLER_PCB::handleSetBoardOrigin );
registerHandler<InteractiveMoveItems, Empty>( &API_HANDLER_PCB::handleInteractiveMoveItems );
registerHandler<GetNets, NetsResponse>( &API_HANDLER_PCB::handleGetNets );
registerHandler<GetNetClassForNets, NetClassForNetsResponse>(
&API_HANDLER_PCB::handleGetNetClassForNets );
registerHandler<RefillZones, Empty>( &API_HANDLER_PCB::handleRefillZones );
registerHandler<SaveDocumentToString, SavedDocumentResponse>(
&API_HANDLER_PCB::handleSaveDocumentToString );
registerHandler<SaveSelectionToString, SavedSelectionResponse>(
&API_HANDLER_PCB::handleSaveSelectionToString );
registerHandler<ParseAndCreateItemsFromString, CreateItemsResponse>(
&API_HANDLER_PCB::handleParseAndCreateItemsFromString );
registerHandler<GetVisibleLayers, BoardLayers>( &API_HANDLER_PCB::handleGetVisibleLayers );
registerHandler<SetVisibleLayers, Empty>( &API_HANDLER_PCB::handleSetVisibleLayers );
registerHandler<GetActiveLayer, BoardLayerResponse>( &API_HANDLER_PCB::handleGetActiveLayer );
registerHandler<SetActiveLayer, Empty>( &API_HANDLER_PCB::handleSetActiveLayer );
registerHandler<GetBoardEditorAppearanceSettings, BoardEditorAppearanceSettings>(
&API_HANDLER_PCB::handleGetBoardEditorAppearanceSettings );
registerHandler<SetBoardEditorAppearanceSettings, Empty>(
&API_HANDLER_PCB::handleSetBoardEditorAppearanceSettings );
}
PCB_EDIT_FRAME* API_HANDLER_PCB::frame() const
{
return static_cast<PCB_EDIT_FRAME*>( m_frame );
}
HANDLER_RESULT<RunActionResponse> API_HANDLER_PCB::handleRunAction(
const HANDLER_CONTEXT<RunAction>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
RunActionResponse response;
if( frame()->GetToolManager()->RunAction( aCtx.Request.action(), true ) )
response.set_status( RunActionStatus::RAS_OK );
else
response.set_status( RunActionStatus::RAS_INVALID );
return response;
}
HANDLER_RESULT<GetOpenDocumentsResponse> API_HANDLER_PCB::handleGetOpenDocuments(
const HANDLER_CONTEXT<GetOpenDocuments>& aCtx )
{
if( aCtx.Request.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;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSaveDocument(
const HANDLER_CONTEXT<SaveDocument>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
frame()->Files_io_from_id( ID_SAVE_BOARD );
return Empty();
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSaveCopyOfDocument(
const HANDLER_CONTEXT<SaveCopyOfDocument>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
wxFileName boardPath( frame()->Prj().AbsolutePath( wxString::FromUTF8( aCtx.Request.path() ) ) );
if( !boardPath.IsOk() || !boardPath.IsDirWritable() )
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( fmt::format( "save path '{}' could not be opened",
boardPath.GetFullPath().ToStdString() ) );
return tl::unexpected( e );
}
if( boardPath.FileExists()
&& ( !boardPath.IsFileWritable() || !aCtx.Request.options().overwrite() ) )
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( fmt::format( "save path '{}' exists and cannot be overwritten",
boardPath.GetFullPath().ToStdString() ) );
return tl::unexpected( e );
}
if( boardPath.GetExt() != FILEEXT::KiCadPcbFileExtension )
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( fmt::format( "save path '{}' must have a kicad_pcb extension",
boardPath.GetFullPath().ToStdString() ) );
return tl::unexpected( e );
}
BOARD* board = frame()->GetBoard();
if( board->GetFileName().Matches( boardPath.GetFullPath() ) )
{
frame()->Files_io_from_id( ID_SAVE_BOARD );
return Empty();
}
bool includeProject = true;
if( aCtx.Request.has_options() )
includeProject = aCtx.Request.options().include_project();
frame()->SavePcbCopy( boardPath.GetFullPath(), includeProject, /* aHeadless = */ true );
return Empty();
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleRevertDocument(
const HANDLER_CONTEXT<RevertDocument>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
wxFileName fn = frame()->Prj().AbsolutePath( frame()->GetBoard()->GetFileName() );
frame()->GetScreen()->SetContentModified( false );
frame()->ReleaseFile();
frame()->OpenProjectFiles( std::vector<wxString>( 1, fn.GetFullPath() ), KICTL_REVERT );
return Empty();
}
void API_HANDLER_PCB::pushCurrentCommit( const std::string& aClientName, const wxString& aMessage )
{
API_HANDLER_EDITOR::pushCurrentCommit( aClientName, aMessage );
frame()->Refresh();
}
std::unique_ptr<COMMIT> API_HANDLER_PCB::createCommit()
{
return std::make_unique<BOARD_COMMIT>( frame() );
}
std::optional<BOARD_ITEM*> API_HANDLER_PCB::getItemById( const KIID& aId ) const
{
BOARD_ITEM* item = frame()->GetBoard()->ResolveItem( aId, true );
if( !item )
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<std::unique_ptr<BOARD_ITEM>> 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<FOOTPRINT*>( 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<BOARD*>( 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<BOARD_ITEM> 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<ItemRequestStatus> API_HANDLER_PCB::handleCreateUpdateItemsInternal( bool aCreate,
const std::string& aClientName,
const types::ItemHeader &aHeader,
const google::protobuf::RepeatedPtrField<google::protobuf::Any>& aItems,
std::function<void( ItemStatus, google::protobuf::Any )> 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<BOARD_ITEM*> optItem = getItemById( containerId );
if( optItem )
{
container = dynamic_cast<BOARD_ITEM_CONTAINER*>( *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<BOARD_COMMIT*>( getCurrentCommit( aClientName ) );
for( const google::protobuf::Any& anyItem : aItems )
{
ItemStatus status;
std::optional<KICAD_T> 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<std::unique_ptr<BOARD_ITEM>> 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<BOARD_ITEM> 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<BOARD_ITEM*> 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() & item->GetLayerSet() ).any() )
{
status.set_code( ItemStatusCode::ISC_INVALID_DATA );
status.set_error_message(
"attempted to add item with no overlapping layers with the board" );
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;
// Footprints can't be modified by CopyFrom at the moment because the commit system
// doesn't currently know what to do with a footprint that has had its children
// replaced with other children; which results in things like the view not having its
// cached geometry for footprint children updated when you move a footprint around.
if( boardItem->Type() == PCB_FOOTPRINT_T )
{
commit->Remove( boardItem );
item->Serialize( newItem );
commit->Add( item.release() );
}
else
{
commit->Modify( boardItem );
boardItem->CopyFrom( item.get() );
boardItem->Serialize( newItem );
}
}
aItemHandler( status, newItem );
}
if( !m_activeClients.count( aClientName ) )
{
pushCurrentCommit( aClientName, aCreate ? _( "Created items via API" )
: _( "Modified items via API" ) );
}
return ItemRequestStatus::IRS_OK;
}
HANDLER_RESULT<GetItemsResponse> API_HANDLER_PCB::handleGetItems(
const HANDLER_CONTEXT<GetItems>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( !validateItemHeaderDocument( aCtx.Request.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<BOARD_ITEM*> items;
std::set<KICAD_T> typesRequested, typesInserted;
bool handledAnything = false;
for( int typeRaw : aCtx.Request.types() )
{
auto typeMessage = static_cast<common::types::KiCadObjectType>( typeRaw );
KICAD_T type = FromProtoEnum<KICAD_T>( 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<KIID, ItemDeletionStatus>& aItemsToDelete,
const std::string& aClientName )
{
BOARD* board = frame()->GetBoard();
std::vector<BOARD_ITEM*> validatedItems;
for( std::pair<const KIID, ItemDeletionStatus> pair : aItemsToDelete )
{
if( BOARD_ITEM* item = board->ResolveItem( pair.first, true ) )
{
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( aClientName );
for( BOARD_ITEM* item : validatedItems )
commit->Remove( item );
if( !m_activeClients.count( aClientName ) )
pushCurrentCommit( aClientName, _( "Deleted items via API" ) );
}
std::optional<EDA_ITEM*> API_HANDLER_PCB::getItemFromDocument( const DocumentSpecifier& aDocument,
const KIID& aId )
{
if( !validateDocument( aDocument ) )
return std::nullopt;
return getItemById( aId );
}
HANDLER_RESULT<SelectionResponse> API_HANDLER_PCB::handleGetSelection(
const HANDLER_CONTEXT<GetSelection>& aCtx )
{
if( !validateItemHeaderDocument( aCtx.Request.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 );
}
std::set<KICAD_T> filter;
for( int typeRaw : aCtx.Request.types() )
{
auto typeMessage = static_cast<types::KiCadObjectType>( typeRaw );
KICAD_T type = FromProtoEnum<KICAD_T>( typeMessage );
if( type == TYPE_NOT_INIT )
continue;
filter.insert( type );
}
TOOL_MANAGER* mgr = frame()->GetToolManager();
PCB_SELECTION_TOOL* selectionTool = mgr->GetTool<PCB_SELECTION_TOOL>();
SelectionResponse response;
for( EDA_ITEM* item : selectionTool->GetSelection() )
{
if( filter.empty() || filter.contains( item->Type() ) )
item->Serialize( *response.add_items() );
}
return response;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleClearSelection(
const HANDLER_CONTEXT<ClearSelection>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( !validateItemHeaderDocument( aCtx.Request.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 );
}
TOOL_MANAGER* mgr = frame()->GetToolManager();
mgr->RunAction( ACTIONS::selectionClear );
frame()->Refresh();
return Empty();
}
HANDLER_RESULT<SelectionResponse> API_HANDLER_PCB::handleAddToSelection(
const HANDLER_CONTEXT<AddToSelection>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( !validateItemHeaderDocument( aCtx.Request.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 );
}
TOOL_MANAGER* mgr = frame()->GetToolManager();
PCB_SELECTION_TOOL* selectionTool = mgr->GetTool<PCB_SELECTION_TOOL>();
std::vector<EDA_ITEM*> toAdd;
for( const types::KIID& id : aCtx.Request.items() )
{
if( std::optional<BOARD_ITEM*> item = getItemById( KIID( id.value() ) ) )
toAdd.emplace_back( *item );
}
selectionTool->AddItemsToSel( &toAdd );
frame()->Refresh();
SelectionResponse response;
for( EDA_ITEM* item : selectionTool->GetSelection() )
item->Serialize( *response.add_items() );
return response;
}
HANDLER_RESULT<SelectionResponse> API_HANDLER_PCB::handleRemoveFromSelection(
const HANDLER_CONTEXT<RemoveFromSelection>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( !validateItemHeaderDocument( aCtx.Request.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 );
}
TOOL_MANAGER* mgr = frame()->GetToolManager();
PCB_SELECTION_TOOL* selectionTool = mgr->GetTool<PCB_SELECTION_TOOL>();
std::vector<EDA_ITEM*> toRemove;
for( const types::KIID& id : aCtx.Request.items() )
{
if( std::optional<BOARD_ITEM*> item = getItemById( KIID( id.value() ) ) )
toRemove.emplace_back( *item );
}
selectionTool->RemoveItemsFromSel( &toRemove );
frame()->Refresh();
SelectionResponse response;
for( EDA_ITEM* item : selectionTool->GetSelection() )
item->Serialize( *response.add_items() );
return response;
}
HANDLER_RESULT<BoardStackupResponse> API_HANDLER_PCB::handleGetStackup(
const HANDLER_CONTEXT<GetBoardStackup>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
BoardStackupResponse response;
google::protobuf::Any any;
frame()->GetBoard()->GetStackupOrDefault().Serialize( any );
any.UnpackTo( response.mutable_stackup() );
// User-settable layer names are not stored in BOARD_STACKUP at the moment
for( board::BoardStackupLayer& layer : *response.mutable_stackup()->mutable_layers() )
{
if( layer.type() == board::BoardStackupLayerType::BSLT_DIELECTRIC )
continue;
PCB_LAYER_ID id = FromProtoEnum<PCB_LAYER_ID>( layer.layer() );
wxCHECK2( id != UNDEFINED_LAYER, continue );
layer.set_user_name( frame()->GetBoard()->GetLayerName( id ) );
}
return response;
}
HANDLER_RESULT<GraphicsDefaultsResponse> API_HANDLER_PCB::handleGetGraphicsDefaults(
const HANDLER_CONTEXT<GetGraphicsDefaults>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.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<kiapi::board::BoardLayerClass, LAYER_CLASS_COUNT> 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<types::Vector2> API_HANDLER_PCB::handleGetBoardOrigin(
const HANDLER_CONTEXT<GetBoardOrigin>& aCtx )
{
if( HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
!documentValidation )
{
return tl::unexpected( documentValidation.error() );
}
VECTOR2I origin;
const BOARD_DESIGN_SETTINGS& settings = frame()->GetBoard()->GetDesignSettings();
switch( aCtx.Request.type() )
{
case BOT_GRID:
origin = settings.GetGridOrigin();
break;
case BOT_DRILL:
origin = settings.GetAuxOrigin();
break;
default:
case BOT_UNKNOWN:
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( "Unexpected origin type" );
return tl::unexpected( e );
}
}
types::Vector2 reply;
PackVector2( reply, origin );
return reply;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSetBoardOrigin(
const HANDLER_CONTEXT<SetBoardOrigin>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
!documentValidation )
{
return tl::unexpected( documentValidation.error() );
}
VECTOR2I origin = UnpackVector2( aCtx.Request.origin() );
switch( aCtx.Request.type() )
{
case BOT_GRID:
{
PCB_EDIT_FRAME* f = frame();
frame()->CallAfter( [f, origin]()
{
// gridSetOrigin takes ownership and frees this
VECTOR2D* dorigin = new VECTOR2D( origin );
TOOL_MANAGER* mgr = f->GetToolManager();
mgr->RunAction( PCB_ACTIONS::gridSetOrigin, dorigin );
f->Refresh();
} );
break;
}
case BOT_DRILL:
{
PCB_EDIT_FRAME* f = frame();
frame()->CallAfter( [f, origin]()
{
TOOL_MANAGER* mgr = f->GetToolManager();
mgr->RunAction( PCB_ACTIONS::drillSetOrigin, origin );
f->Refresh();
} );
break;
}
default:
case BOT_UNKNOWN:
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( "Unexpected origin type" );
return tl::unexpected( e );
}
}
return Empty();
}
HANDLER_RESULT<GetBoundingBoxResponse> API_HANDLER_PCB::handleGetBoundingBox(
const HANDLER_CONTEXT<GetBoundingBox>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
if( !validateItemHeaderDocument( aCtx.Request.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 = aCtx.Request.mode() == BoundingBoxMode::BBM_ITEM_AND_CHILD_TEXT;
for( const types::KIID& idMsg : aCtx.Request.items() )
{
KIID id( idMsg.value() );
std::optional<BOARD_ITEM*> optItem = getItemById( id );
if( !optItem )
continue;
BOARD_ITEM* item = *optItem;
BOX2I bbox;
if( item->Type() == PCB_FOOTPRINT_T )
bbox = static_cast<FOOTPRINT*>( item )->GetBoundingBox( includeText );
else
bbox = item->GetBoundingBox();
response.add_items()->set_value( idMsg.value() );
PackBox2( *response.add_boxes(), bbox );
}
return response;
}
HANDLER_RESULT<PadShapeAsPolygonResponse> API_HANDLER_PCB::handleGetPadShapeAsPolygon(
const HANDLER_CONTEXT<GetPadShapeAsPolygon>& aCtx )
{
if( HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
!documentValidation )
{
return tl::unexpected( documentValidation.error() );
}
PadShapeAsPolygonResponse response;
PCB_LAYER_ID layer = FromProtoEnum<PCB_LAYER_ID, board::types::BoardLayer>( aCtx.Request.layer() );
for( const types::KIID& padRequest : aCtx.Request.pads() )
{
KIID id( padRequest.value() );
std::optional<BOARD_ITEM*> optPad = getItemById( id );
if( !optPad || ( *optPad )->Type() != PCB_PAD_T )
continue;
response.add_pads()->set_value( padRequest.value() );
PAD* pad = static_cast<PAD*>( *optPad );
SHAPE_POLY_SET poly;
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<types::TitleBlockInfo> API_HANDLER_PCB::handleGetTitleBlockInfo(
const HANDLER_CONTEXT<GetTitleBlockInfo>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.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<ExpandTextVariablesResponse> API_HANDLER_PCB::handleExpandTextVariables(
const HANDLER_CONTEXT<ExpandTextVariables>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
ExpandTextVariablesResponse reply;
BOARD* board = frame()->GetBoard();
std::function<bool( wxString* )> textResolver =
[&]( wxString* token ) -> bool
{
// Handles m_board->GetTitleBlock() *and* m_board->GetProject()
return board->ResolveTextVar( token, 0 );
};
for( const std::string& textMsg : aCtx.Request.text() )
{
wxString text = ExpandTextVars( wxString::FromUTF8( textMsg ), &textResolver );
reply.add_text( text.ToUTF8() );
}
return reply;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleInteractiveMoveItems(
const HANDLER_CONTEXT<InteractiveMoveItems>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
TOOL_MANAGER* mgr = frame()->GetToolManager();
std::vector<EDA_ITEM*> toSelect;
for( const kiapi::common::types::KIID& id : aCtx.Request.items() )
{
if( std::optional<BOARD_ITEM*> item = getItemById( KIID( id.value() ) ) )
toSelect.emplace_back( static_cast<EDA_ITEM*>( *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",
aCtx.Request.board().board_filename() ) );
return tl::unexpected( e );
}
PCB_SELECTION_TOOL* selectionTool = mgr->GetTool<PCB_SELECTION_TOOL>();
selectionTool->GetSelection().SetReferencePoint( toSelect[0]->GetPosition() );
mgr->RunAction( ACTIONS::selectionClear );
mgr->RunAction<EDA_ITEMS*>( ACTIONS::selectItems, &toSelect );
COMMIT* commit = getCurrentCommit( aCtx.ClientName );
mgr->PostAPIAction( PCB_ACTIONS::move, commit );
return Empty();
}
HANDLER_RESULT<NetsResponse> API_HANDLER_PCB::handleGetNets( const HANDLER_CONTEXT<GetNets>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
NetsResponse response;
BOARD* board = frame()->GetBoard();
std::set<wxString> netclassFilter;
for( const std::string& nc : aCtx.Request.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<NetClassForNetsResponse> API_HANDLER_PCB::handleGetNetClassForNets(
const HANDLER_CONTEXT<GetNetClassForNets>& aCtx )
{
NetClassForNetsResponse response;
BOARD* board = frame()->GetBoard();
NETINFO_LIST nets = board->GetNetInfo();
google::protobuf::Any any;
for( const board::types::Net& net : aCtx.Request.net() )
{
NETINFO_ITEM* netInfo = nets.GetNetItem( wxString::FromUTF8( net.name() ) );
if( !netInfo )
continue;
netInfo->GetNetClass()->Serialize( any );
auto [pair, rc] = response.mutable_classes()->insert( { net.name(), {} } );
any.UnpackTo( &pair->second );
}
return response;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleRefillZones( const HANDLER_CONTEXT<RefillZones>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
if( aCtx.Request.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<SavedDocumentResponse> API_HANDLER_PCB::handleSaveDocumentToString(
const HANDLER_CONTEXT<SaveDocumentToString>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
SavedDocumentResponse response;
response.mutable_document()->CopyFrom( aCtx.Request.document() );
CLIPBOARD_IO io;
io.SetWriter(
[&]( const wxString& aData )
{
response.set_contents( aData.ToUTF8() );
} );
io.SaveBoard( wxEmptyString, frame()->GetBoard(), nullptr );
return response;
}
HANDLER_RESULT<SavedSelectionResponse> API_HANDLER_PCB::handleSaveSelectionToString(
const HANDLER_CONTEXT<SaveSelectionToString>& aCtx )
{
SavedSelectionResponse response;
TOOL_MANAGER* mgr = frame()->GetToolManager();
PCB_SELECTION_TOOL* selectionTool = mgr->GetTool<PCB_SELECTION_TOOL>();
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<CreateItemsResponse> API_HANDLER_PCB::handleParseAndCreateItemsFromString(
const HANDLER_CONTEXT<ParseAndCreateItemsFromString>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.document() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
CreateItemsResponse response;
return response;
}
HANDLER_RESULT<BoardLayers> API_HANDLER_PCB::handleGetVisibleLayers(
const HANDLER_CONTEXT<GetVisibleLayers>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
BoardLayers response;
for( PCB_LAYER_ID layer : frame()->GetBoard()->GetVisibleLayers() )
response.add_layers( ToProtoEnum<PCB_LAYER_ID, board::types::BoardLayer>( layer ) );
return response;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSetVisibleLayers(
const HANDLER_CONTEXT<SetVisibleLayers>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
LSET visible;
LSET enabled = frame()->GetBoard()->GetEnabledLayers();
for( int layerIdx : aCtx.Request.layers() )
{
PCB_LAYER_ID layer =
FromProtoEnum<PCB_LAYER_ID>( static_cast<board::types::BoardLayer>( layerIdx ) );
if( enabled.Contains( layer ) )
visible.set( layer );
}
frame()->GetBoard()->SetVisibleLayers( visible );
frame()->GetAppearancePanel()->OnBoardChanged();
frame()->GetCanvas()->SyncLayersVisibility( frame()->GetBoard() );
frame()->Refresh();
return Empty();
}
HANDLER_RESULT<BoardLayerResponse> API_HANDLER_PCB::handleGetActiveLayer(
const HANDLER_CONTEXT<GetActiveLayer>& aCtx )
{
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
BoardLayerResponse response;
response.set_layer(
ToProtoEnum<PCB_LAYER_ID, board::types::BoardLayer>( frame()->GetActiveLayer() ) );
return response;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSetActiveLayer(
const HANDLER_CONTEXT<SetActiveLayer>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
HANDLER_RESULT<bool> documentValidation = validateDocument( aCtx.Request.board() );
if( !documentValidation )
return tl::unexpected( documentValidation.error() );
PCB_LAYER_ID layer = FromProtoEnum<PCB_LAYER_ID>( aCtx.Request.layer() );
if( !frame()->GetBoard()->GetEnabledLayers().Contains( layer ) )
{
ApiResponseStatus err;
err.set_status( ApiStatusCode::AS_BAD_REQUEST );
err.set_error_message( fmt::format( "Layer {} is not a valid layer for the given board",
magic_enum::enum_name( layer ) ) );
return tl::unexpected( err );
}
frame()->SetActiveLayer( layer );
return Empty();
}
HANDLER_RESULT<BoardEditorAppearanceSettings> API_HANDLER_PCB::handleGetBoardEditorAppearanceSettings(
const HANDLER_CONTEXT<GetBoardEditorAppearanceSettings>& aCtx )
{
BoardEditorAppearanceSettings reply;
// TODO: might be nice to put all these things in one place and have it derive SERIALIZABLE
const PCB_DISPLAY_OPTIONS& displayOptions = frame()->GetDisplayOptions();
reply.set_inactive_layer_display( ToProtoEnum<HIGH_CONTRAST_MODE, InactiveLayerDisplayMode>(
displayOptions.m_ContrastModeDisplay ) );
reply.set_net_color_display(
ToProtoEnum<NET_COLOR_MODE, NetColorDisplayMode>( displayOptions.m_NetColorMode ) );
reply.set_board_flip( frame()->GetCanvas()->GetView()->IsMirroredX()
? BoardFlipMode::BFM_FLIPPED_X
: BoardFlipMode::BFM_NORMAL );
PCBNEW_SETTINGS* editorSettings = frame()->GetPcbNewSettings();
reply.set_ratsnest_display( ToProtoEnum<RATSNEST_MODE, RatsnestDisplayMode>(
editorSettings->m_Display.m_RatsnestMode ) );
return reply;
}
HANDLER_RESULT<Empty> API_HANDLER_PCB::handleSetBoardEditorAppearanceSettings(
const HANDLER_CONTEXT<SetBoardEditorAppearanceSettings>& aCtx )
{
if( std::optional<ApiResponseStatus> busy = checkForBusy() )
return tl::unexpected( *busy );
PCB_DISPLAY_OPTIONS options = frame()->GetDisplayOptions();
KIGFX::PCB_VIEW* view = frame()->GetCanvas()->GetView();
PCBNEW_SETTINGS* editorSettings = frame()->GetPcbNewSettings();
const BoardEditorAppearanceSettings& newSettings = aCtx.Request.settings();
options.m_ContrastModeDisplay =
FromProtoEnum<HIGH_CONTRAST_MODE>( newSettings.inactive_layer_display() );
options.m_NetColorMode =
FromProtoEnum<NET_COLOR_MODE>( newSettings.net_color_display() );
bool flip = newSettings.board_flip() == BoardFlipMode::BFM_FLIPPED_X;
if( flip != view->IsMirroredX() )
{
view->SetMirror( !view->IsMirroredX(), view->IsMirroredY() );
view->RecacheAllItems();
}
editorSettings->m_Display.m_RatsnestMode =
FromProtoEnum<RATSNEST_MODE>( newSettings.ratsnest_display() );
frame()->SetDisplayOptions( options );
frame()->GetCanvas()->GetView()->UpdateAllLayersColor();
frame()->GetCanvas()->Refresh();
return Empty();
}