mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-15 10:43:15 +02:00
Let cmake generate the needed version strings, so we don't have to spend program time doing it. This simplifies the settings manager versioning. Also, convert some file line endings to UNIX from Windows. A more robust fix for https://gitlab.com/kicad/code/kicad/-/issues/4015.
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;
|
|
}
|