From b5904b040119f237b3e5d1a630ae4aff36824867 Mon Sep 17 00:00:00 2001 From: Jeff Young Date: Sat, 9 Nov 2019 19:39:08 +0000 Subject: [PATCH] Installment one of project Save As... feature. ADD: Adds Save As... to the File menu for the project window. Fixes: lp:594051 * https://bugs.launchpad.net/kicad/+bug/594051 --- common/gestfich.cpp | 15 ++ eeschema/CMakeLists.txt | 2 + eeschema/eeschema.cpp | 160 ++++++++++++++++- gerbview/gerbview.cpp | 123 +++++++++++++ gerbview/json11.cpp | 102 ++++++++--- gerbview/json11.hpp | 11 +- include/gestfich.h | 8 + include/kiway.h | 20 ++- kicad/menubar.cpp | 3 + kicad/tools/kicad_manager_actions.cpp | 2 +- kicad/tools/kicad_manager_control.cpp | 237 ++++++++++++++++++++++++++ kicad/tools/kicad_manager_control.h | 1 + libs/sexpr/sexpr.cpp | 2 +- pagelayout_editor/pl_editor.cpp | 39 +++++ pcbnew/pcbnew.cpp | 101 ++++++++++- 15 files changed, 788 insertions(+), 38 deletions(-) diff --git a/common/gestfich.cpp b/common/gestfich.cpp index 058cc57761..e1dd0ae766 100644 --- a/common/gestfich.cpp +++ b/common/gestfich.cpp @@ -359,6 +359,21 @@ bool CanPrintFile( const wxString& file ) } +void CopyFile( const wxString& aSrcPath, const wxString& aDestPath, std::string& aErrors ) +{ + if( !wxCopyFile( aSrcPath, aDestPath ) ) + { + wxString msg; + + if( !aErrors.empty() ) + aErrors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), aDestPath ); + aErrors += msg; + } +} + + wxString QuoteFullPath( wxFileName& fn, wxPathFormat format ) { return wxT( "\"" ) + fn.GetFullPath( format ) + wxT( "\"" ); diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt index f228864145..b31b421e61 100644 --- a/eeschema/CMakeLists.txt +++ b/eeschema/CMakeLists.txt @@ -28,6 +28,7 @@ include_directories( ./tools ../common ../common/dialogs + ../libs/sexpr/include ${INC_AFTER} ) @@ -353,6 +354,7 @@ target_include_directories( eeschema_kiface PUBLIC target_link_libraries( eeschema_kiface common + sexpr ${wxWidgets_LIBRARIES} ${GDI_PLUS_LIBRARIES} ) diff --git a/eeschema/eeschema.cpp b/eeschema/eeschema.cpp index 9c85004268..a8d77a1660 100644 --- a/eeschema/eeschema.cpp +++ b/eeschema/eeschema.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include #include #include @@ -42,6 +41,9 @@ #include #include #include +#include +#include +#include // The main sheet of the project SCH_SHEET* g_RootSheet = NULL; @@ -127,6 +129,16 @@ static struct IFACE : public KIFACE_I return NULL; } + /** + * Function SaveFileAs + * Saving a file under a different name is delegated to the various KIFACEs because + * the project doesn't know the internal format of the various files (which may have + * paths in them that need updating). + */ + void SaveFileAs( const std::string& aProjectBasePath, const std::string& aProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) override; + } kiface( "eeschema", KIWAY::FACE_SCH ); } // namespace @@ -291,3 +303,149 @@ void IFACE::OnKifaceEnd() wxConfigSaveSetups( KifaceSettings(), cfg_params() ); end_common(); } + +static void traverseSEXPR( SEXPR::SEXPR* aNode, + const std::function& aVisitor ) +{ + aVisitor( aNode ); + + if( aNode->IsList() ) + { + for( int i = 0; i < aNode->GetNumberOfChildren(); i++ ) + traverseSEXPR( aNode->GetChild( i ), aVisitor ); + } +} + + +void IFACE::SaveFileAs( const std::string& aProjectBasePath, const std::string& aProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) +{ + wxFileName destFile( aSrcFilePath ); + wxString destPath = destFile.GetPath(); + wxString ext = destFile.GetExt(); + + if( destPath.StartsWith( aProjectBasePath ) ) + { + destPath.Replace( aProjectBasePath, aNewProjectBasePath, false ); + destFile.SetPath( destPath ); + } + + if( ext == "sch" || ext == "sch-bak" ) + { + if( destFile.GetName() == aProjectName ) + destFile.SetName( aNewProjectName ); + + // JEY TODO: need to update at least sheet-paths... + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( ext == "sym" ) + { + // Symbols are not project-specific. Keep their source names. + wxCopyFile( aSrcFilePath, destFile.GetFullPath() ); + } + else if( ext == "lib" ) + { + if( destFile.GetName() == aProjectName ) + destFile.SetName( aNewProjectName ); + else if( destFile.GetName() == aProjectName + "-cache" ) + destFile.SetName( aNewProjectName + "-cache" ); + else if( destFile.GetName() == aProjectName + "-rescue" ) + destFile.SetName( aNewProjectName + "-rescue" ); + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( ext == "net" ) + { + bool success = false; + + if( destFile.GetName() == aProjectName ) + destFile.SetName( aNewProjectName ); + + try + { + SEXPR::PARSER parser; + std::unique_ptr sexpr( parser.ParseFromFile( aSrcFilePath ) ); + + traverseSEXPR( sexpr.get(), [&]( SEXPR::SEXPR* node ) + { + if( node->IsList() && node->GetNumberOfChildren() > 1 + && node->GetChild( 0 )->IsSymbol() + && node->GetChild( 0 )->GetSymbol() == "source" ) + { + auto pathNode = dynamic_cast( node->GetChild( 1 ) ); + wxString path( pathNode->m_value ); + + if( path == aProjectName + ".sch" ) + path = aNewProjectName + ".sch"; + else if( path == aProjectBasePath + "/" + aProjectName + ".sch" ) + path = aNewProjectBasePath + "/" + aNewProjectName + ".sch"; + else if( path.StartsWith( aProjectBasePath ) ) + path.Replace( aProjectBasePath, aNewProjectBasePath, false ); + + pathNode->m_value = path; + } + } ); + + wxFile destNetList( destFile.GetFullPath(), wxFile::write ); + + if( destNetList.IsOpened() ) + success = destNetList.Write( sexpr->AsString( 0 ) ); + + // wxFile dtor will close the file + } + catch( ... ) + { + success = false; + } + + if( !success ) + { + wxString msg; + + if( !aErrors.empty() ) + aErrors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), destFile.GetFullPath() ); + aErrors += msg; + } + } + else if( destFile.GetName() == "sym-lib-table" ) + { + SYMBOL_LIB_TABLE symbolLibTable; + symbolLibTable.Load( aSrcFilePath ); + + for( int i = 0; i < symbolLibTable.GetCount(); i++ ) + { + LIB_TABLE_ROW& row = symbolLibTable.At( i ); + wxString uri = row.GetFullURI(); + + uri.Replace( "/" + aProjectName + "-cache.lib", "/" + aNewProjectName + "-cache.lib" ); + uri.Replace( "/" + aProjectName + "-rescue.lib", "/" + aNewProjectName + "-rescue.lib" ); + uri.Replace( "/" + aProjectName + ".lib", "/" + aNewProjectName + ".lib" ); + + row.SetFullURI( uri ); + } + + try + { + symbolLibTable.Save( destFile.GetFullPath() ); + } + catch( ... ) + { + wxString msg; + + if( !aErrors.empty() ) + aErrors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), destFile.GetFullPath() ); + aErrors += msg; + } + } + else + { + wxFAIL_MSG( "Unexpected filetype for Eeschema::SaveFileAs()" ); + } +} + diff --git a/gerbview/gerbview.cpp b/gerbview/gerbview.cpp index bb97f5ddf8..79e399eadf 100644 --- a/gerbview/gerbview.cpp +++ b/gerbview/gerbview.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include "json11.hpp" const wxChar* g_GerberPageSizeList[] = { @@ -92,6 +94,16 @@ static struct IFACE : public KIFACE_I return NULL; } + /** + * Function SaveFileAs + * Saving a file under a different name is delegated to the various KIFACEs because + * the project doesn't know the internal format of the various files (which may have + * paths in them that need updating). + */ + void SaveFileAs( const std::string& aProjectBasePath, const std::string& aProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) override; + } kiface( "gerbview", KIWAY::FACE_GERBVIEW ); } // namespace @@ -130,3 +142,114 @@ void IFACE::OnKifaceEnd() { end_common(); } + + +void IFACE::SaveFileAs( const std::string& aProjectBasePath, const std::string& aProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) +{ + wxFileName destFile( aSrcFilePath ); + wxString destPath = destFile.GetPath(); + wxString ext = destFile.GetExt(); + + if( destPath.StartsWith( aProjectBasePath ) ) + { + destPath.Replace( aProjectBasePath, aNewProjectBasePath, false ); + destFile.SetPath( destPath ); + } + + if( ext == "gbr" ) + { + wxString destFileName = destFile.GetName(); + + if( destFileName.StartsWith( aProjectName + "-" ) ) + { + destFileName.Replace( aProjectName, aNewProjectName, false ); + destFile.SetName( destFileName ); + } + + wxCopyFile( aSrcFilePath, destFile.GetFullPath() ); + } + else if( ext == "gbrjob" ) + { + if( destFile.GetName() == aProjectName + "-job" ) + destFile.SetName( aNewProjectName + "-job" ); + + FILE_LINE_READER jobfileReader( aSrcFilePath ); + + char* line; + wxString data; + + while( ( line = jobfileReader.ReadLine() ) ) + data << line << '\n'; + + // detect the file format: old (deprecated) gerber format or official JSON format + if( !data.Contains( "{" ) ) + { + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + return; + } + + bool success = false; + + try + { + std::string err; + json11::Json json = json11::Json::parse(TO_UTF8( data ), err ); + + if( err.empty() ) + { + for( auto& entry : json[ "FilesAttributes" ].array_items() ) + { + wxString path = entry[ "Path" ].string_value(); + + if( path.StartsWith( aProjectName + "-" ) ) + { + path.Replace( aProjectName, aNewProjectName, false ); + entry[ "Path" ].set_string_value( path.ToStdString() ); + } + } + + wxFile destJobFile( destFile.GetFullPath(), wxFile::write ); + + if( destJobFile.IsOpened() ) + success = destJobFile.Write( json.dump( 0 ) ); + + // wxFile dtor will close the file + } + } + catch( ... ) + { + success = false; + } + + if( !success ) + { + wxString msg; + + if( !aErrors.empty() ) + aErrors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), destFile.GetFullPath() ); + aErrors += msg; + } + } + else if( ext == "drl" ) + { + wxString destFileName = destFile.GetName(); + + if( destFileName == aProjectName ) + destFileName = aNewProjectName; + else if( destFileName.StartsWith( aProjectName + "-" ) ) + destFileName.Replace( aProjectName, aNewProjectName, false ); + + destFile.SetName( destFileName ); + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else + { + wxFAIL_MSG( "Unexpected filetype for GerbView::SaveFileAs()" ); + } +} + diff --git a/gerbview/json11.cpp b/gerbview/json11.cpp index 17bbee7f56..7a149f9e17 100644 --- a/gerbview/json11.cpp +++ b/gerbview/json11.cpp @@ -50,18 +50,27 @@ struct NullStruct * Serialization */ -static void dump( NullStruct, string& out ) +static void indent( int level, string& out ) +{ + for( int i = 0; i < level * 2; i++ ) + out += " "; +} + + +static void dump( NullStruct, int level, string& out ) { out += "null"; } -static void dump( double value, string& out ) +static void dump( double value, int level, string& out ) { + indent( level, out ); + if( std::isfinite( value ) ) { char buf[32]; - snprintf( buf, sizeof buf, "%.17g", value ); + snprintf( buf, sizeof buf, "%.3f", value ); out += buf; } else @@ -71,8 +80,10 @@ static void dump( double value, string& out ) } -static void dump( int value, string& out ) +static void dump( int value, int level, string& out ) { + indent( level, out ); + char buf[32]; snprintf( buf, sizeof buf, "%d", value ); @@ -80,14 +91,18 @@ static void dump( int value, string& out ) } -static void dump( bool value, string& out ) +static void dump( bool value, int level, string& out ) { + indent( level, out ); + out += value ? "true" : "false"; } -static void dump( const string& value, string& out ) +static void dump( const string& value, int level, string& out ) { + indent( level, out ); + out += '"'; for( size_t i = 0; i < value.length(); i++ ) @@ -150,49 +165,71 @@ static void dump( const string& value, string& out ) } -static void dump( const Json::array& values, string& out ) +static void dump( const Json::array& values, int level, string& out ) { bool first = true; - out += "["; + indent( level, out ); + out += "[\n"; for( const auto& value : values ) { if( !first ) - out += ", "; + out += ",\n"; - value.dump( out ); + value.dump( level + 1, out ); first = false; } + out += "\n"; + indent( level, out ); out += "]"; } -static void dump( const Json::object& values, string& out ) +static void dump( const Json::object& values, int level, string& out ) { bool first = true; - out += "{"; + indent( level, out ); + out += "{\n"; - for( const auto& kv : values ) + for( int i : { 1, 2 } ) { - if( !first ) - out += ", "; + for( const auto& kv : values ) + { + if( i == 1 && kv.first != "Header" ) + continue; + else if( i == 2 && kv.first == "Header" ) + continue; - dump( kv.first, out ); - out += ": "; - kv.second.dump( out ); - first = false; + if( !first ) + out += ",\n"; + + dump( kv.first, level + 1, out ); + out += ": "; + if( kv.second.is_object() || kv.second.is_array() ) + { + out += "\n"; + kv.second.dump( level + 1, out ); + } + else + { + kv.second.dump( 1, out ); + } + first = false; + } } + out += "\n"; + indent( level, out ); out += "}"; } -void Json::dump( string& out ) const +void Json::dump( int level, string& out ) const { - m_ptr->dump( out ); + m_ptr->dump( level, out ); } @@ -226,8 +263,8 @@ protected: return m_value < static_cast*>(other)->m_value; } - const T m_value; - void dump( string& out ) const override { json11::dump( m_value, out ); } + T m_value; + void dump( int level, string& out ) const override { json11::dump( m_value, level, out ); } }; class JsonDouble final : public Value @@ -276,6 +313,11 @@ class JsonString final : public Value { const string& string_value() const override { return m_value; } + void set_string_value( string value ) const override + { + const_cast( this )->m_value = value; + } + public: explicit JsonString( const string& value ) : Value( value ) {} explicit JsonString( string&& value ) : Value( move( value ) ) {} @@ -436,6 +478,12 @@ const string& Json::string_value() const } +void Json::set_string_value( string value ) const +{ + m_ptr->set_string_value( value ); +} + + const vector& Json::array_items() const { return m_ptr->array_items(); @@ -483,6 +531,10 @@ const string& JsonValue::string_value() const return statics().empty_string; } +void JsonValue::set_string_value( string value ) const +{ +} + const vector& JsonValue::array_items() const { @@ -1135,7 +1187,7 @@ bool Json::has_shape( const shape& types, string& err ) const { if( !is_object() ) { - err = "expected JSON object, got " + dump(); + err = "expected JSON object, got " + dump( 0 ); return false; } @@ -1143,7 +1195,7 @@ bool Json::has_shape( const shape& types, string& err ) const { if( (*this)[item.first].type() != item.second ) { - err = "bad type for " + item.first + " in " + dump(); + err = "bad type for " + item.first + " in " + dump( 0 ); return false; } } diff --git a/gerbview/json11.hpp b/gerbview/json11.hpp index 78c4b109be..80173a76b1 100644 --- a/gerbview/json11.hpp +++ b/gerbview/json11.hpp @@ -148,6 +148,8 @@ public: // Return the enclosed string if this is a string, "" otherwise. const std::string& string_value() const; + void set_string_value( std::string value ) const; + // Return the enclosed std::vector if this is an array, or an empty vector otherwise. const array& array_items() const; @@ -161,13 +163,13 @@ public: const Json& operator[]( const std::string& key ) const; // Serialize. - void dump( std::string& out ) const; + void dump( int level, std::string& out ) const; - std::string dump() const + std::string dump( int level ) const { std::string out; - dump( out ); + dump( level, out ); return out; } @@ -236,11 +238,12 @@ protected: virtual Json::Type type() const = 0; virtual bool equals( const JsonValue* other ) const = 0; virtual bool less( const JsonValue* other ) const = 0; - virtual void dump( std::string& out ) const = 0; + virtual void dump( int level, std::string& out ) const = 0; virtual double number_value() const; virtual int int_value() const; virtual bool bool_value() const; virtual const std::string& string_value() const; + virtual void set_string_value( std::string value ) const; virtual const Json::array& array_items() const; virtual const Json& operator[]( size_t i ) const; virtual const Json::object& object_items() const; diff --git a/include/gestfich.h b/include/gestfich.h index 5eca47e039..10f6b8ddc8 100644 --- a/include/gestfich.h +++ b/include/gestfich.h @@ -59,6 +59,14 @@ void OpenFile( const wxString& file ); void PrintFile( const wxString& file ); bool CanPrintFile( const wxString& file ); +/** + * Function CopyFile + * @param aSrcPath + * @param aDestPath + * @param aErrors a string to *append* any errors to + */ +void CopyFile( const wxString& aSrcPath, const wxString& aDestPath, std::string& aErrors ); + /** * Function EDA_FILE_SELECTOR * diff --git a/include/kiway.h b/include/kiway.h index 22e4c9d641..3698d1cec4 100644 --- a/include/kiway.h +++ b/include/kiway.h @@ -208,8 +208,24 @@ struct KIFACE * and old school cast. dynamic_cast is problematic since it needs typeinfo probably * not contained in the caller's link image. */ - VTBL_ENTRY wxWindow* CreateWindow( wxWindow* aParent, int aClassId, - KIWAY* aKIWAY, int aCtlBits = 0 ) = 0; + VTBL_ENTRY wxWindow* CreateWindow( wxWindow* aParent, int aClassId, + KIWAY* aKIWAY, int aCtlBits = 0 ) = 0; + + /** + * Function SaveFileAs + * Saving a file under a different name is delegated to the various KIFACEs because + * the project doesn't know the internal format of the various files (which may have + * paths in them that need updating). + */ + VTBL_ENTRY void SaveFileAs( const std::string& srcProjectBasePath, + const std::string& srcProjectName, + const std::string& newProjectBasePath, + const std::string& newProjectName, + const std::string& srcFilePath, + std::string& aErrors ) + { + // If a KIFACE owns files then it needs to implement this.... + } /** * Function IfaceOrAddress diff --git a/kicad/menubar.cpp b/kicad/menubar.cpp index 62054d046a..2a38246637 100644 --- a/kicad/menubar.cpp +++ b/kicad/menubar.cpp @@ -66,6 +66,9 @@ void KICAD_MANAGER_FRAME::ReCreateMenuBar() fileMenu->AddItem( KICAD_MANAGER_ACTIONS::openProject, SELECTION_CONDITIONS::ShowAlways ); fileMenu->AddMenu( openRecentMenu, SELECTION_CONDITIONS::ShowAlways ); + fileMenu->AddSeparator(); + fileMenu->AddItem( ACTIONS::saveAs, SELECTION_CONDITIONS::ShowAlways ); + fileMenu->AddSeparator(); fileMenu->AddItem( ID_IMPORT_EAGLE_PROJECT, _( "Import EAGLE Project..." ), diff --git a/kicad/tools/kicad_manager_actions.cpp b/kicad/tools/kicad_manager_actions.cpp index 5e70074d45..1973936842 100644 --- a/kicad/tools/kicad_manager_actions.cpp +++ b/kicad/tools/kicad_manager_actions.cpp @@ -51,7 +51,7 @@ TOOL_ACTION KICAD_MANAGER_ACTIONS::openProject( "kicad.Control.openProject", AS_GLOBAL, MD_CTRL + 'O', LEGACY_HK_NAME( "Open Project" ), _( "Open Project..." ), _( "Open an existing project" ), - open_project_xpm ); + directory_xpm ); TOOL_ACTION KICAD_MANAGER_ACTIONS::editSchematic( "kicad.Control.editSchematic", AS_GLOBAL, diff --git a/kicad/tools/kicad_manager_control.cpp b/kicad/tools/kicad_manager_control.cpp index ba6c0ebcf1..51d70de3a2 100644 --- a/kicad/tools/kicad_manager_control.cpp +++ b/kicad/tools/kicad_manager_control.cpp @@ -313,6 +313,242 @@ int KICAD_MANAGER_CONTROL::OpenProject( const TOOL_EVENT& aEvent ) } +class SAVE_AS_TRAVERSER : public wxDirTraverser +{ +private: + KICAD_MANAGER_FRAME* m_frame; + + wxString m_projectDirPath; + wxString m_projectName; + wxString m_newProjectDirPath; + wxString m_newProjectName; + + wxFileName m_newProjectFile; + std::string m_errors; + +public: + SAVE_AS_TRAVERSER( KICAD_MANAGER_FRAME* aFrame, + const std::string& aSrcProjectDirPath, + const std::string& aSrcProjectName, + const std::string& aNewProjectDirPath, + const std::string& aNewProjectName ) : + m_frame( aFrame ), + m_projectDirPath( aSrcProjectDirPath ), + m_projectName( aSrcProjectName ), + m_newProjectDirPath( aNewProjectDirPath ), + m_newProjectName( aNewProjectName ) + { + } + + virtual wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) + { + wxFileName destFile( aSrcFilePath ); + wxString ext = destFile.GetExt(); + bool atRoot = destFile.GetPath() == m_projectDirPath; + + if( ext == "pro" ) + { + wxString destPath = destFile.GetPath(); + + if( destPath.StartsWith( m_projectDirPath ) ) + { + destPath.Replace( m_projectDirPath, m_newProjectDirPath, false ); + destFile.SetPath( destPath ); + } + + if( destFile.GetName() == m_projectName ) + { + destFile.SetName( m_newProjectName ); + + if( atRoot ) + m_newProjectFile = destFile; + } + + // Currently all paths in the settings file are relative, so we can just do a + // straight copy + CopyFile( aSrcFilePath, destFile.GetFullPath(), m_errors ); + } + else if( ext == "sch" + || ext == "sch-bak" + || ext == "sym" + || ext == "lib" + || ext == "net" + || destFile.GetName() == "sym-lib-table" ) + { + KIFACE* eeschema = m_frame->Kiway().KiFACE( KIWAY::FACE_SCH ); + eeschema->SaveFileAs( m_projectDirPath, m_projectName, m_newProjectDirPath, + m_newProjectName, aSrcFilePath, m_errors ); + } + else if( ext == "kicad_pcb" + || ext == "kicad_pcb-bak" + || ext == "brd" + || ext == "kicad_mod" + || ext == "mod" + || ext == "cmp" + || destFile.GetName() == "fp-lib-table" ) + { + KIFACE* pcbnew = m_frame->Kiway().KiFACE( KIWAY::FACE_PCB ); + pcbnew->SaveFileAs( m_projectDirPath, m_projectName, m_newProjectDirPath, + m_newProjectName, aSrcFilePath, m_errors ); + } + else if( ext == "kicad_wks" ) + { + KIFACE* pleditor = m_frame->Kiway().KiFACE( KIWAY::FACE_PL_EDITOR ); + pleditor->SaveFileAs( m_projectDirPath, m_projectName, m_newProjectDirPath, + m_newProjectName, aSrcFilePath, m_errors ); + } + else if( ext == "gbr" + || ext == "gbrjob" + || ext == "drl" ) + { + KIFACE* gerbview = m_frame->Kiway().KiFACE( KIWAY::FACE_GERBVIEW ); + gerbview->SaveFileAs( m_projectDirPath, m_projectName, m_newProjectDirPath, + m_newProjectName, aSrcFilePath, m_errors ); + } + else + { + // Everything we don't recognize just gets a straight copy + wxString destPath = destFile.GetPath(); + + if( destPath.StartsWith( m_projectDirPath ) ) + { + destPath.Replace( m_projectDirPath, m_newProjectDirPath, false ); + destFile.SetPath( destPath ); + } + + if( destFile.GetName() == m_projectName ) + destFile.SetName( m_newProjectName ); + + CopyFile( aSrcFilePath, destFile.GetFullPath(), m_errors ); + } + + /* TODO: what about these? + MacrosFileExtension; + FootprintPlaceFileExtension; + KiCadFootprintLibPathExtension; + GedaPcbFootprintLibFileExtension; + EagleFootprintLibPathExtension; + KiCadLib3DShapesPathExtension; + SpecctraDsnFileExtension; + IpcD356FileExtension; + */ + + return wxDIR_CONTINUE; + } + + virtual wxDirTraverseResult OnDir( const wxString& dirPath ) + { + wxFileName destDir( dirPath ); + wxString destDirPath = destDir.GetPath(); // strips off last directory + + if( destDirPath.StartsWith( m_projectDirPath ) ) + { + destDirPath.Replace( m_projectDirPath, m_newProjectDirPath, false ); + destDir.SetPath( destDirPath ); + } + + if( destDir.GetName() == m_projectName ) + destDir.SetName( m_newProjectName ); + + if( !wxMkdir( destDir.GetFullPath() ) ) + { + wxString msg; + + if( !m_errors.empty() ) + m_errors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), destDir.GetFullPath() ); + m_errors += msg; + } + + return wxDIR_CONTINUE; + } + + wxString GetErrors() { return m_errors; } + + wxFileName GetNewProjectFile() { return m_newProjectFile; } +}; + + +int KICAD_MANAGER_CONTROL::SaveProjectAs( const TOOL_EVENT& aEvent ) +{ + wxString msg; + + wxFileName currentProjectFile( Prj().GetProjectFullName() ); + wxString currentProjectDirPath = currentProjectFile.GetPath(); + wxString currentProjectName = Prj().GetProjectName(); + + wxString default_dir = m_frame->GetMruPath(); + + if( default_dir == currentProjectDirPath + || default_dir == currentProjectDirPath + wxFileName::GetPathSeparator() ) + { + // Don't start within the current project + wxFileName default_dir_fn( default_dir ); + default_dir_fn.RemoveLastDir(); + default_dir = default_dir_fn.GetPath(); + } + + wxFileDialog dlg( m_frame, _( "Save Project To" ), default_dir, wxEmptyString, wxEmptyString, + wxFD_SAVE ); + + if( dlg.ShowModal() == wxID_CANCEL ) + return -1; + + wxFileName newProjectDir( dlg.GetPath() ); + + if( !newProjectDir.IsAbsolute() ) + newProjectDir.MakeAbsolute(); + + if( wxDirExists( newProjectDir.GetFullPath() ) ) + { + msg.Printf( _( "\"%s\" already exists." ), newProjectDir.GetFullPath() ); + DisplayErrorMessage( m_frame, msg ); + return -1; + } + + if( !wxMkdir( newProjectDir.GetFullPath() ) ) + { + msg.Printf( _( "Directory \"%s\" could not be created.\n\n" + "Please make sure you have write permissions and try again." ), + newProjectDir.GetPath() ); + DisplayErrorMessage( m_frame, msg ); + return -1; + } + + if( !newProjectDir.IsDirWritable() ) + { + msg.Printf( _( "Cannot write to folder \"%s\"." ), newProjectDir.GetFullPath() ); + wxMessageDialog msgDlg( m_frame, msg, _( "Error!" ), wxICON_ERROR | wxOK | wxCENTER ); + msgDlg.SetExtendedMessage( _( "Please check your access permissions to this folder " + "and try again." ) ); + msgDlg.ShowModal(); + return -1; + } + + const wxString& newProjectDirPath = newProjectDir.GetFullPath(); + const wxString& newProjectName = newProjectDir.GetName(); + wxDir currentProjectDir( currentProjectDirPath ); + + SAVE_AS_TRAVERSER traverser( m_frame, + currentProjectDirPath, currentProjectName, + newProjectDirPath, newProjectName ); + + currentProjectDir.Traverse( traverser ); + + if( !traverser.GetErrors().empty() ) + DisplayErrorMessage( m_frame, traverser.GetErrors() ); + + if( traverser.GetNewProjectFile().FileExists() ) + { + m_frame->CreateNewProject( traverser.GetNewProjectFile() ); + m_frame->LoadProject( traverser.GetNewProjectFile() ); + } + + return 0; +} + + int KICAD_MANAGER_CONTROL::Refresh( const TOOL_EVENT& aEvent ) { m_frame->RefreshProjectTree(); @@ -492,6 +728,7 @@ void KICAD_MANAGER_CONTROL::setTransitions() Go( &KICAD_MANAGER_CONTROL::NewProject, KICAD_MANAGER_ACTIONS::newProject.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::NewFromTemplate, KICAD_MANAGER_ACTIONS::newFromTemplate.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::OpenProject, KICAD_MANAGER_ACTIONS::openProject.MakeEvent() ); + Go( &KICAD_MANAGER_CONTROL::SaveProjectAs, ACTIONS::saveAs.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::Refresh, ACTIONS::zoomRedraw.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::UpdateMenu, ACTIONS::updateMenu.MakeEvent() ); diff --git a/kicad/tools/kicad_manager_control.h b/kicad/tools/kicad_manager_control.h index f83829d55d..213563b757 100644 --- a/kicad/tools/kicad_manager_control.h +++ b/kicad/tools/kicad_manager_control.h @@ -49,6 +49,7 @@ public: int NewProject( const TOOL_EVENT& aEvent ); int NewFromTemplate( const TOOL_EVENT& aEvent ); int OpenProject( const TOOL_EVENT& aEvent ); + int SaveProjectAs( const TOOL_EVENT& aEvent ); int Refresh( const TOOL_EVENT& aEvent ); int UpdateMenu( const TOOL_EVENT& aEvent ); diff --git a/libs/sexpr/sexpr.cpp b/libs/sexpr/sexpr.cpp index 832be80219..4c49f2a43b 100644 --- a/libs/sexpr/sexpr.cpp +++ b/libs/sexpr/sexpr.cpp @@ -156,7 +156,7 @@ namespace SEXPR result = "\n"; } - result.append( aLevel* 4, ' ' ); + result.append( aLevel * 2, ' ' ); aLevel++; result += "("; diff --git a/pagelayout_editor/pl_editor.cpp b/pagelayout_editor/pl_editor.cpp index fb4f668483..9ef3500cc0 100644 --- a/pagelayout_editor/pl_editor.cpp +++ b/pagelayout_editor/pl_editor.cpp @@ -86,6 +86,16 @@ static struct IFACE : public KIFACE_I return NULL; } + /** + * Function SaveFileAs + * Saving a file under a different name is delegated to the various KIFACEs because + * the project doesn't know the internal format of the various files (which may have + * paths in them that need updating). + */ + void SaveFileAs( const std::string& aProjectBasePath, const std::string& aSrcProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) override; + } kiface( "pl_editor", KIWAY::FACE_PL_EDITOR ); } // namespace @@ -124,3 +134,32 @@ void IFACE::OnKifaceEnd() { end_common(); } + + +void IFACE::SaveFileAs( const std::string& aProjectBasePath, const std::string& aSrcProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) +{ + wxFileName destFile( aSrcFilePath ); + wxString destPath = destFile.GetPath(); + wxString ext = destFile.GetExt(); + + if( destPath.StartsWith( aProjectBasePath ) ) + { + destPath.Replace( aProjectBasePath, aNewProjectBasePath, false ); + destFile.SetPath( destPath ); + } + + if( ext == "kicad_wks" ) + { + if( destFile.GetName() == aSrcProjectName ) + destFile.SetName( aNewProjectName ); + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else + { + wxFAIL_MSG( "Unexpected filetype for Pcbnew::SaveFileAs()" ); + } +} + diff --git a/pcbnew/pcbnew.cpp b/pcbnew/pcbnew.cpp index 103157bfbb..ddb788cc16 100644 --- a/pcbnew/pcbnew.cpp +++ b/pcbnew/pcbnew.cpp @@ -40,13 +40,10 @@ #include #include #include -#include #include #include -#include #include #include -#include #include #include #include @@ -55,7 +52,6 @@ #include #include #include -#include #include #include "invoke_pcb_dialog.h" #include "dialog_global_fp_lib_table_config.h" @@ -165,6 +161,16 @@ static struct IFACE : public KIFACE_I } } + /** + * Function SaveFileAs + * Saving a file under a different name is delegated to the various KIFACEs because + * the project doesn't know the internal format of the various files (which may have + * paths in them that need updating). + */ + void SaveFileAs( const std::string& aProjectBasePath, const std::string& aSrcProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) override; + } kiface( "pcbnew", KIWAY::FACE_PCB ); } // namespace @@ -378,3 +384,90 @@ void IFACE::OnKifaceEnd() end_common(); } + + +void IFACE::SaveFileAs( const std::string& aProjectBasePath, const std::string& aSrcProjectName, + const std::string& aNewProjectBasePath, const std::string& aNewProjectName, + const std::string& aSrcFilePath, std::string& aErrors ) +{ + wxFileName destFile( aSrcFilePath ); + wxString destPath = destFile.GetPath(); + wxString ext = destFile.GetExt(); + + if( destPath.StartsWith( aProjectBasePath ) ) + { + destPath.Replace( aProjectBasePath, aNewProjectBasePath, false ); + destFile.SetPath( destPath ); + } + + if( ext == "kicad_pcb" || ext == "kicad_pcb-bak" ) + { + if( destFile.GetName() == aSrcProjectName ) + destFile.SetName( aNewProjectName ); + + // JEY TODO: are there filepaths in a PCB file that need updating? + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( ext == "brd" ) + { + if( destFile.GetName() == aSrcProjectName ) + destFile.SetName( aNewProjectName ); + + // JEY TODO: are there filepaths in a legacy PCB file that need updating? + + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( ext == "mod" || ext == "kicad_mod" ) + { + // Footprints are not project-specific. Keep their source names. + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( ext == "cmp" ) + { + // JEY TODO + } + else if( ext == "rpt" ) + { + // DRC must be the "gold standard". Since we can't gaurantee that there aren't + // any non-deterministic cases in the save-as algorithm, we don't want to certify + // the result with the source's DRC report. Therefore copy it under the old + // name. + CopyFile( aSrcFilePath, destFile.GetFullPath(), aErrors ); + } + else if( destFile.GetName() == "fp-lib-table" ) + { + try + { + FP_LIB_TABLE fpLibTable; + fpLibTable.Load( aSrcFilePath ); + + for( int i = 0; i < fpLibTable.GetCount(); i++ ) + { + LIB_TABLE_ROW& row = fpLibTable.At( i ); + wxString uri = row.GetFullURI(); + + uri.Replace( "/" + aSrcProjectName + ".pretty", "/" + aNewProjectName + ".pretty" ); + + row.SetFullURI( uri ); + } + + fpLibTable.Save( destFile.GetFullPath() ); + } + catch( ... ) + { + wxString msg; + + if( !aErrors.empty() ) + aErrors += "\n"; + + msg.Printf( _( "Cannot copy file \"%s\"." ), destFile.GetFullPath() ); + aErrors += msg; + } + } + else + { + wxFAIL_MSG( "Unexpected filetype for Pcbnew::SaveFileAs()" ); + } +} +