636 lines
16 KiB
C++
636 lines
16 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <regex>
|
|
#include <wx/debug.h>
|
|
#include <wx/filename.h>
|
|
#include <wx/stdpaths.h>
|
|
#include <wx/utils.h>
|
|
|
|
#include <build_version.h>
|
|
#include <confirm.h>
|
|
#include <dialogs/dialog_migrate_settings.h>
|
|
#include <gestfich.h>
|
|
#include <macros.h>
|
|
#include <settings/app_settings.h>
|
|
#include <settings/common_settings.h>
|
|
#include <settings/settings_manager.h>
|
|
#include <settings/color_settings.h>
|
|
|
|
|
|
/**
|
|
* 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<COMMON_SETTINGS*>( 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<JSON_SETTINGS> ptr( aSettings );
|
|
|
|
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<JSON_SETTINGS>& 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<COLOR_SETTINGS*>( 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<JSON_SETTINGS>& 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<JSON_SETTINGS>& 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 )
|
|
{
|
|
COLOR_SETTINGS* ret = nullptr;
|
|
|
|
try
|
|
{
|
|
ret = m_color_settings.at( aName );
|
|
}
|
|
catch( std::out_of_range& )
|
|
{
|
|
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<COLOR_SETTINGS*>(
|
|
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<void( const wxString& )> m_action;
|
|
|
|
public:
|
|
explicit COLOR_SETTINGS_LOADER( std::function<void( const wxString& )> 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 )
|
|
{
|
|
m_color_settings[aFilename] = static_cast<COLOR_SETTINGS*>(
|
|
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::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 auto& el ) {
|
|
return el.second == aSettings;
|
|
} ) != m_color_settings.end() );
|
|
|
|
nlohmann::json::json_pointer ptr = JSON_SETTINGS::PointerFromString( aNamespace );
|
|
|
|
aSettings->Store();
|
|
|
|
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 );
|
|
}
|
|
|
|
|
|
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<wxString>* aPaths )
|
|
{
|
|
wxASSERT( aPaths );
|
|
|
|
aPaths->clear();
|
|
|
|
wxDir dir;
|
|
std::vector<wxFileName> 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;
|
|
}
|