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 <sch_edit_frame.h>
 #include <lib_edit_frame.h>
 #include <viewlib_frame.h>
-#include <eda_text.h>
 #include <general.h>
 #include <class_libentry.h>
 #include <transform.h>
@@ -42,6 +41,9 @@
 #include <dialogs/panel_sym_lib_table.h>
 #include <kiway.h>
 #include <sim/sim_plot_frame.h>
+#include <kiface_ids.h>
+#include <libs/sexpr/include/sexpr/sexpr.h>
+#include <libs/sexpr/include/sexpr/sexpr_parser.h>
 
 // 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<void( SEXPR::SEXPR* )>& 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::SEXPR> 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<SEXPR::SEXPR_STRING*>( 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 <pgm_base.h>
 #include <gerbview.h>
 #include <gerbview_frame.h>
+#include <gestfich.h>
+#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<const Value<tag, T>*>(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<Json::NUMBER, double>
@@ -276,6 +313,11 @@ class JsonString final : public Value<Json::STRING, string>
 {
     const string& string_value() const override { return m_value; }
 
+    void set_string_value( string value ) const override
+    {
+        const_cast<JsonString*>( 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>& 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<Json>& 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 <macros.h>
 #include <pcb_edit_frame.h>
 #include <eda_dde.h>
-#include <wx/stdpaths.h>
 #include <wx/file.h>
 #include <wx/snglinst.h>
-#include <wx/dir.h>
 #include <gestfich.h>
 #include <pcbnew.h>
-#include <wildcards_and_files_ext.h>
 #include <class_board.h>
 #include <class_draw_panel_gal.h>
 #include <fp_lib_table.h>
@@ -55,7 +52,6 @@
 #include <footprint_wizard_frame.h>
 #include <footprint_preview_panel.h>
 #include <footprint_info_impl.h>
-#include <gl_context_mgr.h>
 #include <dialog_configure_paths.h>
 #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()" );
+    }
+}
+