/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2020 Jon Evans * Copyright (C) 2020 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Flag to enable settings tracing * @ingroup trace_env_vars */ const char* traceSettings = "SETTINGS"; SETTINGS_MANAGER::SETTINGS_MANAGER() : m_common_settings( nullptr ), m_migration_source() { // Check if the settings directory already exists, and if not, perform a migration if possible if( !MigrateIfNeeded() ) { m_ok = false; return; } m_ok = true; // create the common settings shared by all applications. Not loaded immediately m_common_settings = static_cast( RegisterSettings( new COMMON_SETTINGS, false ) ); loadAllColorSettings(); } SETTINGS_MANAGER::~SETTINGS_MANAGER() { m_settings.clear(); m_color_settings.clear(); } JSON_SETTINGS* SETTINGS_MANAGER::RegisterSettings( JSON_SETTINGS* aSettings, bool aLoadNow ) { std::unique_ptr ptr( aSettings ); ptr->SetManager( this ); wxLogTrace( traceSettings, "Registered new settings object %s", ptr->GetFilename() ); if( aLoadNow ) ptr->LoadFromFile( GetPathForSettingsFile( ptr.get() ) ); m_settings.push_back( std::move( ptr ) ); return m_settings.back().get(); } void SETTINGS_MANAGER::Load() { // TODO(JE) We should check for dirty settings here and write them if so, because // Load() could be called late in the application lifecycle for( auto&& settings : m_settings ) settings->LoadFromFile( GetPathForSettingsFile( settings.get() ) ); } void SETTINGS_MANAGER::Load( JSON_SETTINGS* aSettings ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) ( *it )->LoadFromFile( GetPathForSettingsFile( it->get() ) ); } void SETTINGS_MANAGER::Save() { for( auto&& settings : m_settings ) { // Never automatically save color settings, caller should use SaveColorSettings if( dynamic_cast( settings.get() ) ) continue; settings->SaveToFile( GetPathForSettingsFile( settings.get() ) ); } } void SETTINGS_MANAGER::Save( JSON_SETTINGS* aSettings ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) { wxLogTrace( traceSettings, "Saving %s", ( *it )->GetFilename() ); ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) ); } } void SETTINGS_MANAGER::FlushAndRelease( JSON_SETTINGS* aSettings ) { auto it = std::find_if( m_settings.begin(), m_settings.end(), [&aSettings]( const std::unique_ptr& aPtr ) { return aPtr.get() == aSettings; } ); if( it != m_settings.end() ) { wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFilename() ); ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) ); m_settings.erase( it ); } } COLOR_SETTINGS* SETTINGS_MANAGER::GetColorSettings( const wxString& aName ) { if( m_color_settings.count( aName ) ) return m_color_settings.at( aName ); COLOR_SETTINGS* ret = nullptr; if( !aName.empty() ) ret = loadColorSettingsByName( aName ); // This had better work if( !ret ) ret = m_color_settings.at( "user" ); return ret; } COLOR_SETTINGS* SETTINGS_MANAGER::loadColorSettingsByName( const wxString& aName ) { wxLogTrace( traceSettings, "Attempting to load color theme %s", aName ); wxFileName fn( GetColorSettingsPath(), aName, "json" ); if( !fn.IsOk() || !fn.Exists() ) { wxLogTrace( traceSettings, "Theme file %s.json not found, falling back to user", aName ); return nullptr; } auto cs = static_cast( RegisterSettings( new COLOR_SETTINGS( aName.ToStdString() ) ) ); if( cs->GetFilename() != aName.ToStdString() ) { wxLogTrace( traceSettings, "Warning: stored filename is actually %s, ", cs->GetFilename() ); } m_color_settings[aName] = cs; return cs; } class COLOR_SETTINGS_LOADER : public wxDirTraverser { private: std::function m_action; public: explicit COLOR_SETTINGS_LOADER( std::function aAction ) : m_action( std::move( aAction ) ) { } wxDirTraverseResult OnFile( const wxString& aFilePath ) override { wxFileName file( aFilePath ); if( file.GetExt() != "json" ) return wxDIR_CONTINUE; if( file.GetName() == "user" ) return wxDIR_CONTINUE; m_action( file.GetName() ); return wxDIR_CONTINUE; } wxDirTraverseResult OnDir( const wxString& dirPath ) override { return wxDIR_IGNORE; } }; void SETTINGS_MANAGER::registerColorSettings( const wxString& aFilename ) { if( m_color_settings.count( aFilename ) ) return; m_color_settings[aFilename] = static_cast( RegisterSettings( new COLOR_SETTINGS( aFilename.ToStdString() ) ) ); } COLOR_SETTINGS* SETTINGS_MANAGER::AddNewColorSettings( const wxString& aFilename ) { wxString filename = aFilename; if( filename.EndsWith( wxT( ".json" ) ) ) filename = filename.BeforeLast( '.' ); registerColorSettings( filename ); return m_color_settings[filename]; } void SETTINGS_MANAGER::loadAllColorSettings() { // Create the default color settings registerColorSettings( "user" ); // Search for and load any other settings COLOR_SETTINGS_LOADER loader( [&]( const wxString& aFilename ) { registerColorSettings( aFilename ); } ); wxDir colors_dir( GetColorSettingsPath() ); if( colors_dir.IsOpened() ) colors_dir.Traverse( loader ); } void SETTINGS_MANAGER::ReloadColorSettings() { m_color_settings.clear(); loadAllColorSettings(); } void SETTINGS_MANAGER::SaveColorSettings( COLOR_SETTINGS* aSettings, const std::string& aNamespace ) { // The passed settings should already be managed wxASSERT( std::find_if( m_color_settings.begin(), m_color_settings.end(), [aSettings] ( const std::pair& el ) { return el.second->GetFilename() == aSettings->GetFilename(); } ) != m_color_settings.end() ); nlohmann::json::json_pointer ptr = JSON_SETTINGS::PointerFromString( aNamespace ); if( !aSettings->Store() ) { wxLogTrace( traceSettings, "Color scheme %s not modified; skipping save", aSettings->GetFilename(), aNamespace ); return; } wxASSERT( aSettings->contains( ptr ) ); wxLogTrace( traceSettings, "Saving color scheme %s, preserving %s", aSettings->GetFilename(), aNamespace ); nlohmann::json backup = aSettings->at( ptr ); std::string path = GetColorSettingsPath(); aSettings->LoadFromFile( path ); ( *aSettings )[ptr].update( backup ); aSettings->Load(); aSettings->SaveToFile( path, true ); } std::string SETTINGS_MANAGER::GetPathForSettingsFile( JSON_SETTINGS* aSettings ) { wxASSERT( aSettings ); switch( aSettings->GetLocation() ) { case SETTINGS_LOC::USER: return GetUserSettingsPath(); case SETTINGS_LOC::PROJECT: // TODO(JE) return ""; case SETTINGS_LOC::COLORS: return GetColorSettingsPath(); default: wxASSERT_MSG( false, "Unknown settings location!" ); } return ""; } class MIGRATION_TRAVERSER : public wxDirTraverser { private: wxString m_src; wxString m_dest; wxString m_errors; public: MIGRATION_TRAVERSER( const wxString& aSrcDir, const wxString& aDestDir ) : m_src( aSrcDir ), m_dest( aDestDir ) { } wxString GetErrors() { return m_errors; } virtual wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) override { wxFileName file( aSrcFilePath ); wxString path = file.GetPath(); path.Replace( m_src, m_dest, false ); file.SetPath( path ); wxLogTrace( traceSettings, "Copying %s to %s", aSrcFilePath, file.GetFullPath() ); // For now, just copy everything CopyFile( aSrcFilePath, file.GetFullPath(), m_errors ); return wxDIR_CONTINUE; } virtual wxDirTraverseResult OnDir( const wxString& dirPath ) override { wxFileName dir( dirPath ); // Whitelist of directories to migrate if( dir.GetName() == "colors" || dir.GetName() == "3d" ) { wxString path = dir.GetPath(); path.Replace( m_src, m_dest, false ); dir.SetPath( path ); wxMkdir( dir.GetFullPath() ); return wxDIR_CONTINUE; } else { return wxDIR_IGNORE; } } }; bool SETTINGS_MANAGER::MigrateIfNeeded() { wxFileName path( GetUserSettingsPath(), "" ); wxLogTrace( traceSettings, "Using settings path %s", path.GetFullPath() ); if( path.DirExists() ) { wxFileName common = path; common.SetName( "kicad_common" ); common.SetExt( "json" ); if( common.Exists() ) { wxLogTrace( traceSettings, "Path exists and has a kicad_common, continuing!" ); return true; } } // Now we have an empty path, let's figure out what to put in it DIALOG_MIGRATE_SETTINGS dlg( this ); if( dlg.ShowModal() != wxID_OK ) { wxLogTrace( traceSettings, "Migration dialog canceled; exiting" ); return false; } if( !path.DirExists() ) { wxLogTrace( traceSettings, "Path didn't exist; creating it" ); path.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ); } if( m_migration_source.IsEmpty() ) { wxLogTrace( traceSettings, "No migration source given; starting with defaults" ); return true; } MIGRATION_TRAVERSER traverser( m_migration_source, path.GetFullPath() ); wxDir source_dir( m_migration_source ); source_dir.Traverse( traverser ); if( !traverser.GetErrors().empty() ) DisplayErrorMessage( nullptr, traverser.GetErrors() ); return true; } bool SETTINGS_MANAGER::GetPreviousVersionPaths( std::vector* aPaths ) { wxASSERT( aPaths ); aPaths->clear(); wxDir dir; std::vector base_paths; base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false ), "" ) ); // If the env override is set, also check the default paths if( wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), nullptr ) ) base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false, false ), "" ) ); wxString subdir; std::string mine = GetSettingsVersion(); auto check_dir = [&] ( const wxString& aSubDir ) { // Only older versions are valid for migration if( compareVersions( aSubDir.ToStdString(), mine ) <= 0 ) { wxString sub_path = dir.GetNameWithSep() + aSubDir; if( IsSettingsPathValid( sub_path ) ) { aPaths->push_back( sub_path ); wxLogTrace( traceSettings, "GetPreviousVersionName: %s is valid", sub_path ); } } }; for( auto base_path : base_paths ) { if( !dir.Open( base_path.GetFullPath() ) ) { wxLogTrace( traceSettings, "GetPreviousVersionName: could not open base path %s", base_path.GetFullPath() ); continue; } wxLogTrace( traceSettings, "GetPreviousVersionName: checking base path %s", base_path.GetFullPath() ); if( dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS ) ) { if( subdir != mine ) check_dir( subdir ); while( dir.GetNext( &subdir ) ) { if( subdir != mine ) check_dir( subdir ); } } // If we didn't find one yet, check for legacy settings without a version directory if( IsSettingsPathValid( dir.GetNameWithSep() ) ) { wxLogTrace( traceSettings, "GetPreviousVersionName: root path %s is valid", dir.GetName() ); aPaths->push_back( dir.GetName() ); } } return aPaths->size() > 0; } bool SETTINGS_MANAGER::IsSettingsPathValid( const wxString& aPath ) { wxFileName test( aPath, "kicad_common" ); return test.Exists(); } std::string SETTINGS_MANAGER::GetColorSettingsPath() { wxFileName path; path.AssignDir( GetUserSettingsPath() ); path.AppendDir( "colors" ); return path.GetPath().ToStdString(); } std::string SETTINGS_MANAGER::GetUserSettingsPath() { static std::string user_settings_path; if( user_settings_path.empty() ) user_settings_path = calculateUserSettingsPath(); return user_settings_path; } std::string SETTINGS_MANAGER::calculateUserSettingsPath( bool aIncludeVer, bool aUseEnv ) { wxFileName cfgpath; // http://docs.wxwidgets.org/3.0/classwx_standard_paths.html#a7c7cf595d94d29147360d031647476b0 cfgpath.AssignDir( wxStandardPaths::Get().GetUserConfigDir() ); // GetUserConfigDir() does not default to ~/.config which is the current standard // configuration file location on Linux. This has been fixed in later versions of wxWidgets. #if !defined( __WXMSW__ ) && !defined( __WXMAC__ ) wxArrayString dirs = cfgpath.GetDirs(); if( dirs.Last() != ".config" ) cfgpath.AppendDir( ".config" ); #endif wxString envstr; // This shouldn't cause any issues on Windows or MacOS. if( wxGetEnv( wxT( "XDG_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() ) { // Override the assignment above with XDG_CONFIG_HOME cfgpath.AssignDir( envstr ); } cfgpath.AppendDir( TO_STR( KICAD_CONFIG_DIR ) ); // Use KICAD_CONFIG_HOME to allow the user to force a specific configuration path. if( aUseEnv && wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() ) { // Override the assignment above with KICAD_CONFIG_HOME cfgpath.AssignDir( envstr ); } if( aIncludeVer ) cfgpath.AppendDir( GetSettingsVersion() ); return cfgpath.GetPath().ToStdString(); } std::string SETTINGS_MANAGER::GetSettingsVersion() { // CMake computes the major.minor string for us. return GetMajorMinorVersion().ToStdString(); } int SETTINGS_MANAGER::compareVersions( const std::string& aFirst, const std::string& aSecond ) { int a_maj = 0; int a_min = 0; int b_maj = 0; int b_min = 0; if( !extractVersion( aFirst, &a_maj, &a_min ) || !extractVersion( aSecond, &b_maj, &b_min ) ) { wxLogTrace( traceSettings, "compareSettingsVersions: bad input (%s, %s)", aFirst, aSecond ); return -1; } if( a_maj < b_maj ) { return -1; } else if( a_maj > b_maj ) { return 1; } else { if( a_min < b_min ) { return -1; } else if( a_min > b_min ) { return 1; } else { return 0; } } } bool SETTINGS_MANAGER::extractVersion( const std::string& aVersionString, int* aMajor, int* aMinor ) { std::regex re_version( "(\\d+)\\.(\\d+)" ); std::smatch match; if( std::regex_match( aVersionString, match, re_version ) ) { try { *aMajor = std::stoi( match[1].str() ); *aMinor = std::stoi( match[2].str() ); } catch( ... ) { return false; } return true; } return false; }