diff --git a/api/proto/common/commands/editor_commands.proto b/api/proto/common/commands/editor_commands.proto index b1b8ff7103..7718256f53 100644 --- a/api/proto/common/commands/editor_commands.proto +++ b/api/proto/common/commands/editor_commands.proto @@ -47,6 +47,46 @@ message GetOpenDocumentsResponse repeated kiapi.common.types.DocumentSpecifier documents = 1; } +message SaveDocument +{ + kiapi.common.types.DocumentSpecifier document = 1; +} + +// Saves the given document to a new location and opens the new copy +// Note, this is not going to be implemented anytime soon as we don't currently +// want to allow API access to changing which project is open +//message SaveDocumentAs +//{ +// kiapi.common.types.DocumentSpecifier document = 1; +// +// string path = 2; +//} + +message SaveOptions +{ + // Overwrite destination file(s) if they exist + bool overwrite = 1; + + // If the file being saved normally requires a project (for example, a board or schematic), + // this flag will cause a new project to be saved alongside the new file + bool include_project = 2; +} + +// Saves the given document to a new location and does not open the new copy +message SaveCopyOfDocument +{ + kiapi.common.types.DocumentSpecifier document = 1; + + string path = 2; + + SaveOptions options = 3; +} + +message RevertDocument +{ + kiapi.common.types.DocumentSpecifier document = 1; +} + /* * Runs a TOOL_ACTION using the TOOL_MANAGER of a given frame. * WARNING: The TOOL_ACTIONs are specifically *not* an API. diff --git a/pcbnew/api/api_handler_pcb.cpp b/pcbnew/api/api_handler_pcb.cpp index 51473f41a6..d657571585 100644 --- a/pcbnew/api/api_handler_pcb.cpp +++ b/pcbnew/api/api_handler_pcb.cpp @@ -37,6 +37,7 @@ #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> @@ -58,7 +59,9 @@ API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* 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 ); @@ -147,6 +150,103 @@ HANDLER_RESULT<GetOpenDocumentsResponse> API_HANDLER_PCB::handleGetOpenDocuments } +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 ); diff --git a/pcbnew/api/api_handler_pcb.h b/pcbnew/api/api_handler_pcb.h index 3605b353f8..f4d1a43824 100644 --- a/pcbnew/api/api_handler_pcb.h +++ b/pcbnew/api/api_handler_pcb.h @@ -63,6 +63,14 @@ private: HANDLER_RESULT<commands::GetOpenDocumentsResponse> handleGetOpenDocuments( const HANDLER_CONTEXT<commands::GetOpenDocuments>& aCtx ); + HANDLER_RESULT<Empty> handleSaveDocument( const HANDLER_CONTEXT<commands::SaveDocument>& aCtx ); + + HANDLER_RESULT<Empty> handleSaveCopyOfDocument( + const HANDLER_CONTEXT<commands::SaveCopyOfDocument>& aCtx ); + + HANDLER_RESULT<Empty> handleRevertDocument( + const HANDLER_CONTEXT<commands::RevertDocument>& aCtx ); + HANDLER_RESULT<commands::GetItemsResponse> handleGetItems( const HANDLER_CONTEXT<commands::GetItems>& aCtx ); diff --git a/pcbnew/files.cpp b/pcbnew/files.cpp index b4849fbf4a..a75d7b0a09 100644 --- a/pcbnew/files.cpp +++ b/pcbnew/files.cpp @@ -1122,14 +1122,17 @@ bool PCB_EDIT_FRAME::SavePcbFile( const wxString& aFileName, bool addToHistory, } -bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject ) +bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject, bool aHeadless ) { wxFileName pcbFileName( EnsureFileExtension( aFileName, FILEEXT::KiCadPcbFileExtension ) ); if( !IsWritable( pcbFileName ) ) { - DisplayError( this, wxString::Format( _( "Insufficient permissions to write file '%s'." ), - pcbFileName.GetFullPath() ) ); + if( !aHeadless ) + { + DisplayError( this, wxString::Format( _( "Insufficient permissions to write file '%s'." ), + pcbFileName.GetFullPath() ) ); + } return false; } @@ -1149,9 +1152,12 @@ bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject } catch( const IO_ERROR& ioe ) { - DisplayError( this, wxString::Format( _( "Error saving board file '%s'.\n%s" ), - pcbFileName.GetFullPath(), - ioe.What() ) ); + if( !aHeadless ) + { + DisplayError( this, wxString::Format( _( "Error saving board file '%s'.\n%s" ), + pcbFileName.GetFullPath(), + ioe.What() ) ); + } return false; } @@ -1171,14 +1177,17 @@ bool PCB_EDIT_FRAME::SavePcbCopy( const wxString& aFileName, bool aCreateProject if( aCreateProject && currentRules.FileExists() && !rulesFile.FileExists() ) KiCopyFile( currentRules.GetFullPath(), rulesFile.GetFullPath(), msg ); - if( !msg.IsEmpty() ) + if( !msg.IsEmpty() && !aHeadless ) { DisplayError( this, wxString::Format( _( "Error saving custom rules file '%s'." ), rulesFile.GetFullPath() ) ); } - DisplayInfoMessage( this, wxString::Format( _( "Board copied to:\n%s" ), - pcbFileName.GetFullPath() ) ); + if( !aHeadless ) + { + DisplayInfoMessage( this, wxString::Format( _( "Board copied to:\n%s" ), + pcbFileName.GetFullPath() ) ); + } return true; } diff --git a/pcbnew/pcb_edit_frame.h b/pcbnew/pcb_edit_frame.h index 3e3cdfc284..f1a361548a 100644 --- a/pcbnew/pcb_edit_frame.h +++ b/pcbnew/pcb_edit_frame.h @@ -423,9 +423,11 @@ public: * * @param aFileName The file name to write. * @param aCreateProject will create an empty project alongside the board file + * @param aHeadless will suppress informational output (e.g. to be used from the API) * @return True if file was saved successfully. */ - bool SavePcbCopy( const wxString& aFileName, bool aCreateProject = false ); + bool SavePcbCopy( const wxString& aFileName, bool aCreateProject = false, + bool aHeadless = false ); /** * Delete all and reinitialize the current board.