KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_handler_editor.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 Jon Evans <[email protected]>
5 * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
22#include <api/api_utils.h>
23#include <eda_base_frame.h>
24#include <eda_item.h>
25#include <wx/wx.h>
26
27using namespace kiapi::common::commands;
28
29
32 m_frame( aFrame )
33{
34 registerHandler<BeginCommit, BeginCommitResponse>( &API_HANDLER_EDITOR::handleBeginCommit );
35 registerHandler<EndCommit, EndCommitResponse>( &API_HANDLER_EDITOR::handleEndCommit );
36 registerHandler<CreateItems, CreateItemsResponse>( &API_HANDLER_EDITOR::handleCreateItems );
37 registerHandler<UpdateItems, UpdateItemsResponse>( &API_HANDLER_EDITOR::handleUpdateItems );
38 registerHandler<DeleteItems, DeleteItemsResponse>( &API_HANDLER_EDITOR::handleDeleteItems );
39 registerHandler<HitTest, HitTestResponse>( &API_HANDLER_EDITOR::handleHitTest );
40}
41
42
45{
46 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
47 return tl::unexpected( *busy );
48
49 if( m_commits.count( aCtx.ClientName ) )
50 {
51 ApiResponseStatus e;
52 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
53 e.set_error_message( fmt::format( "the client {} already has a commit in progress",
54 aCtx.ClientName ) );
55 return tl::unexpected( e );
56 }
57
58 wxASSERT( !m_activeClients.count( aCtx.ClientName ) );
59
60 BeginCommitResponse response;
61
62 KIID id;
63 m_commits[aCtx.ClientName] = std::make_pair( id, createCommit() );
64 response.mutable_id()->set_value( id.AsStdString() );
65
66 m_activeClients.insert( aCtx.ClientName );
67
68 return response;
69}
70
71
73 const HANDLER_CONTEXT<EndCommit>& aCtx )
74{
75 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
76 return tl::unexpected( *busy );
77
78 if( !m_commits.count( aCtx.ClientName ) )
79 {
80 ApiResponseStatus e;
81 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
82 e.set_error_message( fmt::format( "the client {} does not has a commit in progress",
83 aCtx.ClientName ) );
84 return tl::unexpected( e );
85 }
86
87 wxASSERT( m_activeClients.count( aCtx.ClientName ) );
88
89 const std::pair<KIID, std::unique_ptr<COMMIT>>& pair = m_commits.at( aCtx.ClientName );
90 const KIID& id = pair.first;
91 const std::unique_ptr<COMMIT>& commit = pair.second;
92
93 EndCommitResponse response;
94
95 // Do not check IDs with drop; it is a safety net in case the id was lost on the client side
96 switch( aCtx.Request.action() )
97 {
98 case kiapi::common::commands::CMA_DROP:
99 {
100 commit->Revert();
101 m_commits.erase( aCtx.ClientName );
102 m_activeClients.erase( aCtx.ClientName );
103 break;
104 }
105
106 case kiapi::common::commands::CMA_COMMIT:
107 {
108 if( aCtx.Request.id().value().compare( id.AsStdString() ) != 0 )
109 {
110 ApiResponseStatus e;
111 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
112 e.set_error_message( fmt::format( "the id {} does not match the commit in progress",
113 aCtx.Request.id().value() ) );
114 return tl::unexpected( e );
115 }
116
117 pushCurrentCommit( aCtx.ClientName, wxString( aCtx.Request.message().c_str(), wxConvUTF8 ) );
118 break;
119 }
120
121 default:
122 break;
123 }
124
125 return response;
126}
127
128
129COMMIT* API_HANDLER_EDITOR::getCurrentCommit( const std::string& aClientName )
130{
131 if( !m_commits.count( aClientName ) )
132 {
133 KIID id;
134 m_commits[aClientName] = std::make_pair( id, createCommit() );
135 }
136
137 return m_commits.at( aClientName ).second.get();
138}
139
140
141void API_HANDLER_EDITOR::pushCurrentCommit( const std::string& aClientName,
142 const wxString& aMessage )
143{
144 auto it = m_commits.find( aClientName );
145
146 if( it == m_commits.end() )
147 return;
148
149 it->second.second->Push( aMessage.IsEmpty() ? m_defaultCommitMessage : aMessage );
150 m_commits.erase( it );
151 m_activeClients.erase( aClientName );
152}
153
154
156{
157 if( !validateDocumentInternal( aDocument ) )
158 {
159 ApiResponseStatus e;
160 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
161 e.set_error_message( fmt::format( "the requested document {} is not open",
162 aDocument.board_filename() ) );
163 return tl::unexpected( e );
164 }
165
166 return true;
167}
168
169
171 const types::ItemHeader& aHeader )
172{
173 if( !aHeader.has_document() || aHeader.document().type() != thisDocumentType() )
174 {
175 ApiResponseStatus e;
176 e.set_status( ApiStatusCode::AS_UNHANDLED );
177 // No error message, this is a flag that the server should try a different handler
178 return tl::unexpected( e );
179 }
180
181 HANDLER_RESULT<bool> documentValidation = validateDocument( aHeader.document() );
182
183 if( !documentValidation )
184 return tl::unexpected( documentValidation.error() );
185
186 if( !validateDocumentInternal( aHeader.document() ) )
187 {
188 ApiResponseStatus e;
189 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
190 e.set_error_message( fmt::format( "the requested document {} is not open",
191 aHeader.document().board_filename() ) );
192 return tl::unexpected( e );
193 }
194
195 if( aHeader.has_container() )
196 {
197 return KIID( aHeader.container().value() );
198 }
199
200 // Valid header, but no container provided
201 return std::nullopt;
202}
203
204
205std::optional<ApiResponseStatus> API_HANDLER_EDITOR::checkForBusy()
206{
208 {
209 ApiResponseStatus e;
210 e.set_status( ApiStatusCode::AS_BUSY );
211 e.set_error_message( "KiCad is busy and cannot respond to API requests right now" );
212 return e;
213 }
214
215 return std::nullopt;
216}
217
218
220 const HANDLER_CONTEXT<CreateItems>& aCtx )
221{
222 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
223 return tl::unexpected( *busy );
224
225 CreateItemsResponse response;
226
228 aCtx.ClientName,
229 aCtx.Request.header(), aCtx.Request.items(),
230 [&]( const ItemStatus& aStatus, const google::protobuf::Any& aItem )
231 {
232 ItemCreationResult itemResult;
233 itemResult.mutable_status()->CopyFrom( aStatus );
234 itemResult.mutable_item()->CopyFrom( aItem );
235 response.mutable_created_items()->Add( std::move( itemResult ) );
236 } );
237
238 if( !result.has_value() )
239 return tl::unexpected( result.error() );
240
241 response.set_status( *result );
242 return response;
243}
244
245
247 const HANDLER_CONTEXT<UpdateItems>& aCtx )
248{
249 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
250 return tl::unexpected( *busy );
251
252 UpdateItemsResponse response;
253
255 aCtx.ClientName,
256 aCtx.Request.header(), aCtx.Request.items(),
257 [&]( const ItemStatus& aStatus, const google::protobuf::Any& aItem )
258 {
259 ItemUpdateResult itemResult;
260 itemResult.mutable_status()->CopyFrom( aStatus );
261 itemResult.mutable_item()->CopyFrom( aItem );
262 response.mutable_updated_items()->Add( std::move( itemResult ) );
263 } );
264
265 if( !result.has_value() )
266 return tl::unexpected( result.error() );
267
268 response.set_status( *result );
269 return response;
270}
271
272
274 const HANDLER_CONTEXT<DeleteItems>& aCtx )
275{
276 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
277 return tl::unexpected( *busy );
278
279 if( !validateItemHeaderDocument( aCtx.Request.header() ) )
280 {
281 ApiResponseStatus e;
282 // No message needed for AS_UNHANDLED; this is an internal flag for the API server
283 e.set_status( ApiStatusCode::AS_UNHANDLED );
284 return tl::unexpected( e );
285 }
286
287 std::map<KIID, ItemDeletionStatus> itemsToDelete;
288
289 for( const kiapi::common::types::KIID& kiidBuf : aCtx.Request.item_ids() )
290 {
291 if( !kiidBuf.value().empty() )
292 {
293 KIID kiid( kiidBuf.value() );
294 itemsToDelete[kiid] = ItemDeletionStatus::IDS_NONEXISTENT;
295 }
296 }
297
298 if( itemsToDelete.empty() )
299 {
300 ApiResponseStatus e;
301 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
302 e.set_error_message( "no valid items to delete were given" );
303 return tl::unexpected( e );
304 }
305
306 deleteItemsInternal( itemsToDelete, aCtx.ClientName );
307
308 DeleteItemsResponse response;
309
310 for( const auto& [id, status] : itemsToDelete )
311 {
312 ItemDeletionResult result;
313 result.mutable_id()->set_value( id.AsStdString() );
314 result.set_status( status );
315 }
316
317 response.set_status( kiapi::common::types::ItemRequestStatus::IRS_OK );
318 return response;
319}
320
321
323 const HANDLER_CONTEXT<HitTest>& aCtx )
324{
325 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
326 return tl::unexpected( *busy );
327
328 if( !validateItemHeaderDocument( aCtx.Request.header() ) )
329 {
330 ApiResponseStatus e;
331 // No message needed for AS_UNHANDLED; this is an internal flag for the API server
332 e.set_status( ApiStatusCode::AS_UNHANDLED );
333 return tl::unexpected( e );
334 }
335
336 HitTestResponse response;
337
338 std::optional<EDA_ITEM*> item = getItemFromDocument( aCtx.Request.header().document(),
339 KIID( aCtx.Request.id().value() ) );
340
341 if( !item )
342 {
343 ApiResponseStatus e;
344 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
345 e.set_error_message( "the requested item ID is not present in the given document" );
346 return tl::unexpected( e );
347 }
348
349 if( ( *item )->HitTest( UnpackVector2( aCtx.Request.position() ), aCtx.Request.tolerance() ) )
350 response.set_result( HitTestResult::HTR_HIT );
351 else
352 response.set_result( HitTestResult::HTR_NO_HIT );
353
354 return response;
355}
tl::expected< T, ApiResponseStatus > HANDLER_RESULT
Definition: api_handler.h:45
virtual std::unique_ptr< COMMIT > createCommit()=0
Override this to create an appropriate COMMIT subclass for the frame in question.
HANDLER_RESULT< bool > validateDocument(const DocumentSpecifier &aDocument)
HANDLER_RESULT< commands::DeleteItemsResponse > handleDeleteItems(const HANDLER_CONTEXT< commands::DeleteItems > &aCtx)
virtual std::optional< EDA_ITEM * > getItemFromDocument(const DocumentSpecifier &aDocument, const KIID &aId)=0
HANDLER_RESULT< std::optional< KIID > > validateItemHeaderDocument(const kiapi::common::types::ItemHeader &aHeader)
If the header is valid, returns the item container.
HANDLER_RESULT< commands::EndCommitResponse > handleEndCommit(const HANDLER_CONTEXT< commands::EndCommit > &aCtx)
API_HANDLER_EDITOR(EDA_BASE_FRAME *aFrame=nullptr)
HANDLER_RESULT< commands::CreateItemsResponse > handleCreateItems(const HANDLER_CONTEXT< commands::CreateItems > &aCtx)
COMMIT * getCurrentCommit(const std::string &aClientName)
virtual void pushCurrentCommit(const std::string &aClientName, const wxString &aMessage)
virtual HANDLER_RESULT< ItemRequestStatus > handleCreateUpdateItemsInternal(bool aCreate, const std::string &aClientName, const types::ItemHeader &aHeader, const google::protobuf::RepeatedPtrField< google::protobuf::Any > &aItems, std::function< void(commands::ItemStatus, google::protobuf::Any)> aItemHandler)=0
std::set< std::string > m_activeClients
std::map< std::string, std::pair< KIID, std::unique_ptr< COMMIT > > > m_commits
HANDLER_RESULT< commands::UpdateItemsResponse > handleUpdateItems(const HANDLER_CONTEXT< commands::UpdateItems > &aCtx)
virtual void deleteItemsInternal(std::map< KIID, ItemDeletionStatus > &aItemsToDelete, const std::string &aClientName)=0
virtual std::optional< ApiResponseStatus > checkForBusy()
Checks if the editor can accept commands.
virtual types::DocumentType thisDocumentType() const =0
Override this to specify which document type this editor handles.
HANDLER_RESULT< commands::BeginCommitResponse > handleBeginCommit(const HANDLER_CONTEXT< commands::BeginCommit > &aCtx)
HANDLER_RESULT< commands::HitTestResponse > handleHitTest(const HANDLER_CONTEXT< commands::HitTest > &aCtx)
EDA_BASE_FRAME * m_frame
virtual bool validateDocumentInternal(const DocumentSpecifier &aDocument) const =0
static const wxString m_defaultCommitMessage
Definition: api_handler.h:131
Represent a set of changes (additions, deletions or modifications) of a data model (e....
Definition: commit.h:74
The base frame for deriving all KiCad main window classes.
virtual bool CanAcceptApiCommands()
Checks if this frame is ready to accept API commands.
Definition: kiid.h:49
Base window classes and related definitions.
KICOMMON_API VECTOR2I UnpackVector2(const types::Vector2 &aInput)
Definition: api_utils.cpp:84
std::string ClientName
Definition: api_handler.h:51
RequestMessageType Request
Definition: api_handler.h:52