1 /*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2020 Jon Evans <jon@craftyjon.com>
5 * Copyright (C) 2021 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "settings/json_settings.h"
22 #include <regex>
23 #include <wx/debug.h>
24 #include <wx/dir.h>
25 #include <wx/filename.h>
26 #include <wx/snglinst.h>
27 #include <wx/stdpaths.h>
28 #include <wx/utils.h>
29
30 #include <build_version.h>
31 #include <confirm.h>
32 #include <dialogs/dialog_migrate_settings.h>
33 #include <gestfich.h>
34 #include <kiplatform/environment.h>
35 #include <kiway.h>
36 #include <lockfile.h>
37 #include <macros.h>
38 #include <pgm_base.h>
39 #include <paths.h>
40 #include <project.h>
41 #include <project/project_archiver.h>
42 #include <project/project_file.h>
43 #include <project/project_local_settings.h>
44 #include <settings/color_settings.h>
45 #include <settings/common_settings.h>
46 #include <settings/json_settings_internals.h>
47 #include <settings/settings_manager.h>
48 #include <wildcards_and_files_ext.h>
49
50
SETTINGS_MANAGER(bool aHeadless)51 SETTINGS_MANAGER::SETTINGS_MANAGER( bool aHeadless ) :
52 m_headless( aHeadless ),
53 m_kiway( nullptr ),
54 m_common_settings( nullptr ),
55 m_migration_source(),
56 m_migrateLibraryTables( true )
57 {
58 PATHS::EnsureUserPathsExist();
59
60 // Check if the settings directory already exists, and if not, perform a migration if possible
61 if( !MigrateIfNeeded() )
62 {
63 m_ok = false;
64 return;
65 }
66
67 m_ok = true;
68
69 // create the common settings shared by all applications. Not loaded immediately
70 m_common_settings = RegisterSettings( new COMMON_SETTINGS, false );
71 }
72
~SETTINGS_MANAGER()73 SETTINGS_MANAGER::~SETTINGS_MANAGER()
74 {
75 m_settings.clear();
76 m_color_settings.clear();
77 m_projects.clear();
78 }
79
80
registerSettings(JSON_SETTINGS * aSettings,bool aLoadNow)81 JSON_SETTINGS* SETTINGS_MANAGER::registerSettings( JSON_SETTINGS* aSettings, bool aLoadNow )
82 {
83 std::unique_ptr<JSON_SETTINGS> ptr( aSettings );
84
85 ptr->SetManager( this );
86
87 wxLogTrace( traceSettings, "Registered new settings object <%s>", ptr->GetFullFilename() );
88
89 if( aLoadNow )
90 ptr->LoadFromFile( GetPathForSettingsFile( ptr.get() ) );
91
92 m_settings.push_back( std::move( ptr ) );
93 return m_settings.back().get();
94 }
95
96
Load()97 void SETTINGS_MANAGER::Load()
98 {
99 // TODO(JE) We should check for dirty settings here and write them if so, because
100 // Load() could be called late in the application lifecycle
101
102 for( auto&& settings : m_settings )
103 settings->LoadFromFile( GetPathForSettingsFile( settings.get() ) );
104 }
105
106
Load(JSON_SETTINGS * aSettings)107 void SETTINGS_MANAGER::Load( JSON_SETTINGS* aSettings )
108 {
109 auto it = std::find_if( m_settings.begin(), m_settings.end(),
110 [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
111 {
112 return aPtr.get() == aSettings;
113 } );
114
115 if( it != m_settings.end() )
116 ( *it )->LoadFromFile( GetPathForSettingsFile( it->get() ) );
117 }
118
119
Save()120 void SETTINGS_MANAGER::Save()
121 {
122 for( auto&& settings : m_settings )
123 {
124 // Never automatically save color settings, caller should use SaveColorSettings
125 if( dynamic_cast<COLOR_SETTINGS*>( settings.get() ) )
126 continue;
127
128 settings->SaveToFile( GetPathForSettingsFile( settings.get() ) );
129 }
130 }
131
132
Save(JSON_SETTINGS * aSettings)133 void SETTINGS_MANAGER::Save( JSON_SETTINGS* aSettings )
134 {
135 auto it = std::find_if( m_settings.begin(), m_settings.end(),
136 [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
137 {
138 return aPtr.get() == aSettings;
139 } );
140
141 if( it != m_settings.end() )
142 {
143 wxLogTrace( traceSettings, "Saving %s", ( *it )->GetFullFilename() );
144 ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
145 }
146 }
147
148
FlushAndRelease(JSON_SETTINGS * aSettings,bool aSave)149 void SETTINGS_MANAGER::FlushAndRelease( JSON_SETTINGS* aSettings, bool aSave )
150 {
151 auto it = std::find_if( m_settings.begin(), m_settings.end(),
152 [&aSettings]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
153 {
154 return aPtr.get() == aSettings;
155 } );
156
157 if( it != m_settings.end() )
158 {
159 wxLogTrace( traceSettings, "Flush and release %s", ( *it )->GetFullFilename() );
160
161 if( aSave )
162 ( *it )->SaveToFile( GetPathForSettingsFile( it->get() ) );
163
164 size_t typeHash = typeid( *it->get() ).hash_code();
165
166 if( m_app_settings_cache.count( typeHash ) )
167 m_app_settings_cache.erase( typeHash );
168
169 m_settings.erase( it );
170 }
171 }
172
173
GetColorSettings(const wxString & aName)174 COLOR_SETTINGS* SETTINGS_MANAGER::GetColorSettings( const wxString& aName )
175 {
176 if( m_color_settings.count( aName ) )
177 return m_color_settings.at( aName );
178
179 if( !aName.empty() )
180 {
181 COLOR_SETTINGS* ret = loadColorSettingsByName( aName );
182
183 if( !ret )
184 {
185 ret = registerColorSettings( aName );
186 *ret = *m_color_settings.at( "_builtin_default" );
187 ret->SetFilename( wxT( "user" ) );
188 ret->SetReadOnly( false );
189 }
190
191 return ret;
192 }
193
194 // This had better work
195 return m_color_settings.at( "_builtin_default" );
196 }
197
198
loadColorSettingsByName(const wxString & aName)199 COLOR_SETTINGS* SETTINGS_MANAGER::loadColorSettingsByName( const wxString& aName )
200 {
201 wxLogTrace( traceSettings, "Attempting to load color theme %s", aName );
202
203 wxFileName fn( GetColorSettingsPath(), aName, "json" );
204
205 if( !fn.IsOk() || !fn.Exists() )
206 {
207 wxLogTrace( traceSettings, "Theme file %s.json not found, falling back to user", aName );
208 return nullptr;
209 }
210
211 COLOR_SETTINGS* settings = RegisterSettings( new COLOR_SETTINGS( aName ) );
212
213 if( settings->GetFilename() != aName.ToStdString() )
214 {
215 wxLogTrace( traceSettings, "Warning: stored filename is actually %s, ",
216 settings->GetFilename() );
217 }
218
219 m_color_settings[aName] = settings;
220
221 return settings;
222 }
223
224
225 class JSON_DIR_TRAVERSER : public wxDirTraverser
226 {
227 private:
228 std::function<void( const wxFileName& )> m_action;
229
230 public:
JSON_DIR_TRAVERSER(std::function<void (const wxFileName &)> aAction)231 explicit JSON_DIR_TRAVERSER( std::function<void( const wxFileName& )> aAction )
232 : m_action( std::move( aAction ) )
233 {
234 }
235
OnFile(const wxString & aFilePath)236 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
237 {
238 wxFileName file( aFilePath );
239
240 if( file.GetExt() == "json" )
241 m_action( file );
242
243 return wxDIR_CONTINUE;
244 }
245
OnDir(const wxString & dirPath)246 wxDirTraverseResult OnDir( const wxString& dirPath ) override
247 {
248 return wxDIR_CONTINUE;
249 }
250 };
251
252
registerColorSettings(const wxString & aName,bool aAbsolutePath)253 COLOR_SETTINGS* SETTINGS_MANAGER::registerColorSettings( const wxString& aName, bool aAbsolutePath )
254 {
255 if( !m_color_settings.count( aName ) )
256 {
257 COLOR_SETTINGS* colorSettings = RegisterSettings( new COLOR_SETTINGS( aName,
258 aAbsolutePath ) );
259 m_color_settings[aName] = colorSettings;
260 }
261
262 return m_color_settings.at( aName );
263 }
264
265
AddNewColorSettings(const wxString & aName)266 COLOR_SETTINGS* SETTINGS_MANAGER::AddNewColorSettings( const wxString& aName )
267 {
268 if( aName.EndsWith( wxT( ".json" ) ) )
269 return registerColorSettings( aName.BeforeLast( '.' ) );
270 else
271 return registerColorSettings( aName );
272 }
273
274
GetMigratedColorSettings()275 COLOR_SETTINGS* SETTINGS_MANAGER::GetMigratedColorSettings()
276 {
277 if( !m_color_settings.count( "user" ) )
278 {
279 COLOR_SETTINGS* settings = registerColorSettings( wxT( "user" ) );
280 settings->SetName( wxT( "User" ) );
281 Save( settings );
282 }
283
284 return m_color_settings.at( "user" );
285 }
286
287
loadAllColorSettings()288 void SETTINGS_MANAGER::loadAllColorSettings()
289 {
290 // Create the built-in color settings
291 for( COLOR_SETTINGS* settings : COLOR_SETTINGS::CreateBuiltinColorSettings() )
292 m_color_settings[settings->GetFilename()] = RegisterSettings( settings, false );
293
294 wxFileName third_party_path;
295 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
296 auto it = env.find( "KICAD6_3RD_PARTY" );
297
298 if( it != env.end() && !it->second.GetValue().IsEmpty() )
299 third_party_path.SetPath( it->second.GetValue() );
300 else
301 third_party_path.SetPath( PATHS::GetDefault3rdPartyPath() );
302
303 third_party_path.AppendDir( "colors" );
304
305 wxDir third_party_colors_dir( third_party_path.GetFullPath() );
306 wxString color_settings_path = GetColorSettingsPath();
307
308 // Search for and load any other settings
309 JSON_DIR_TRAVERSER loader( [&]( const wxFileName& aFilename )
310 {
311 registerColorSettings( aFilename.GetName() );
312 } );
313
314 JSON_DIR_TRAVERSER thirdPartyLoader(
315 [&]( const wxFileName& aFilename )
316 {
317 COLOR_SETTINGS* settings = registerColorSettings( aFilename.GetFullPath(), true );
318 settings->SetReadOnly( true );
319 } );
320
321 wxDir colors_dir( color_settings_path );
322
323 if( colors_dir.IsOpened() )
324 {
325 if( third_party_colors_dir.IsOpened() )
326 third_party_colors_dir.Traverse( thirdPartyLoader );
327
328 colors_dir.Traverse( loader );
329 }
330 }
331
332
ReloadColorSettings()333 void SETTINGS_MANAGER::ReloadColorSettings()
334 {
335 m_color_settings.clear();
336 loadAllColorSettings();
337 }
338
339
SaveColorSettings(COLOR_SETTINGS * aSettings,const std::string & aNamespace)340 void SETTINGS_MANAGER::SaveColorSettings( COLOR_SETTINGS* aSettings, const std::string& aNamespace )
341 {
342 // The passed settings should already be managed
343 wxASSERT( std::find_if( m_color_settings.begin(), m_color_settings.end(),
344 [aSettings] ( const std::pair<wxString, COLOR_SETTINGS*>& el )
345 {
346 return el.second->GetFilename() == aSettings->GetFilename();
347 }
348 ) != m_color_settings.end() );
349
350 if( aSettings->IsReadOnly() )
351 return;
352
353 if( !aSettings->Store() )
354 {
355 wxLogTrace( traceSettings, "Color scheme %s not modified; skipping save",
356 aNamespace );
357 return;
358 }
359
360 wxASSERT( aSettings->Contains( aNamespace ) );
361
362 wxLogTrace( traceSettings, "Saving color scheme %s, preserving %s",
363 aSettings->GetFilename(),
364 aNamespace );
365
366 OPT<nlohmann::json> backup = aSettings->GetJson( aNamespace );
367 wxString path = GetColorSettingsPath();
368
369 aSettings->LoadFromFile( path );
370
371 if( backup )
372 ( *aSettings->Internals() )[aNamespace].update( *backup );
373
374 aSettings->Load();
375
376 aSettings->SaveToFile( path, true );
377 }
378
379
GetPathForSettingsFile(JSON_SETTINGS * aSettings)380 wxString SETTINGS_MANAGER::GetPathForSettingsFile( JSON_SETTINGS* aSettings )
381 {
382 wxASSERT( aSettings );
383
384 switch( aSettings->GetLocation() )
385 {
386 case SETTINGS_LOC::USER:
387 return GetUserSettingsPath();
388
389 case SETTINGS_LOC::PROJECT:
390 return Prj().GetProjectPath();
391
392 case SETTINGS_LOC::COLORS:
393 return GetColorSettingsPath();
394
395 case SETTINGS_LOC::NONE:
396 return "";
397
398 default:
399 wxASSERT_MSG( false, "Unknown settings location!" );
400 }
401
402 return "";
403 }
404
405
406 class MIGRATION_TRAVERSER : public wxDirTraverser
407 {
408 private:
409 wxString m_src;
410 wxString m_dest;
411 wxString m_errors;
412 bool m_migrateTables;
413
414 public:
MIGRATION_TRAVERSER(const wxString & aSrcDir,const wxString & aDestDir,bool aMigrateTables)415 MIGRATION_TRAVERSER( const wxString& aSrcDir, const wxString& aDestDir, bool aMigrateTables ) :
416 m_src( aSrcDir ),
417 m_dest( aDestDir ),
418 m_migrateTables( aMigrateTables )
419 {
420 }
421
GetErrors()422 wxString GetErrors() { return m_errors; }
423
OnFile(const wxString & aSrcFilePath)424 wxDirTraverseResult OnFile( const wxString& aSrcFilePath ) override
425 {
426 wxFileName file( aSrcFilePath );
427
428 if( !m_migrateTables && ( file.GetName() == wxT( "sym-lib-table" ) ||
429 file.GetName() == wxT( "fp-lib-table" ) ) )
430 {
431 return wxDIR_CONTINUE;
432 }
433
434 // Skip migrating PCM installed packages as packages themselves are not moved
435 if( file.GetFullName() == wxT( "installed_packages.json" ) )
436 return wxDIR_CONTINUE;
437
438 // Don't migrate hotkeys config files; we don't have a reasonable migration handler for them
439 // and so there is no way to resolve conflicts at the moment
440 if( file.GetExt() == wxT( "hotkeys" ) )
441 return wxDIR_CONTINUE;
442
443 wxString path = file.GetPath();
444
445 path.Replace( m_src, m_dest, false );
446 file.SetPath( path );
447
448 wxLogTrace( traceSettings, "Copying %s to %s", aSrcFilePath, file.GetFullPath() );
449
450 // For now, just copy everything
451 KiCopyFile( aSrcFilePath, file.GetFullPath(), m_errors );
452
453 return wxDIR_CONTINUE;
454 }
455
OnDir(const wxString & dirPath)456 wxDirTraverseResult OnDir( const wxString& dirPath ) override
457 {
458 wxFileName dir( dirPath );
459
460 // Whitelist of directories to migrate
461 if( dir.GetName() == "colors" ||
462 dir.GetName() == "3d" )
463 {
464
465 wxString path = dir.GetPath();
466
467 path.Replace( m_src, m_dest, false );
468 dir.SetPath( path );
469
470 wxMkdir( dir.GetFullPath() );
471
472 return wxDIR_CONTINUE;
473 }
474 else
475 {
476 return wxDIR_IGNORE;
477 }
478 }
479 };
480
481
MigrateIfNeeded()482 bool SETTINGS_MANAGER::MigrateIfNeeded()
483 {
484 if( m_headless )
485 {
486 wxLogTrace( traceSettings, "Settings migration not checked; running headless" );
487 return false;
488 }
489
490 wxFileName path( GetUserSettingsPath(), "" );
491 wxLogTrace( traceSettings, "Using settings path %s", path.GetFullPath() );
492
493 if( path.DirExists() )
494 {
495 wxFileName common = path;
496 common.SetName( "kicad_common" );
497 common.SetExt( "json" );
498
499 if( common.Exists() )
500 {
501 wxLogTrace( traceSettings, "Path exists and has a kicad_common, continuing!" );
502 return true;
503 }
504 }
505
506 // Now we have an empty path, let's figure out what to put in it
507 DIALOG_MIGRATE_SETTINGS dlg( this );
508
509 if( dlg.ShowModal() != wxID_OK )
510 {
511 wxLogTrace( traceSettings, "Migration dialog canceled; exiting" );
512 return false;
513 }
514
515 if( !path.DirExists() )
516 {
517 wxLogTrace( traceSettings, "Path didn't exist; creating it" );
518 path.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
519 }
520
521 if( m_migration_source.IsEmpty() )
522 {
523 wxLogTrace( traceSettings, "No migration source given; starting with defaults" );
524 return true;
525 }
526
527 wxLogTrace( traceSettings, "Migrating from path %s", m_migration_source );
528
529 MIGRATION_TRAVERSER traverser( m_migration_source, path.GetFullPath(), m_migrateLibraryTables );
530 wxDir source_dir( m_migration_source );
531
532 source_dir.Traverse( traverser );
533
534 if( !traverser.GetErrors().empty() )
535 DisplayErrorMessage( nullptr, traverser.GetErrors() );
536
537 // Remove any library configuration if we didn't choose to import
538 if( !m_migrateLibraryTables )
539 {
540 COMMON_SETTINGS common;
541 wxString commonPath = GetPathForSettingsFile( &common );
542 common.LoadFromFile( commonPath );
543
544 const std::vector<wxString> libKeys = {
545 wxT( "KICAD6_SYMBOL_DIR" ),
546 wxT( "KICAD6_3DMODEL_DIR" ),
547 wxT( "KICAD6_FOOTPRINT_DIR" ),
548 wxT( "KICAD6_TEMPLATE_DIR" ), // Stores the default library table to be copied
549
550 // Deprecated keys
551 wxT( "KICAD_PTEMPLATES" ),
552 wxT( "KISYS3DMOD" ),
553 wxT( "KISYSMOD" ),
554 wxT( "KICAD_SYMBOL_DIR" ),
555 };
556
557 for( const wxString& key : libKeys )
558 common.m_Env.vars.erase( key );
559
560 common.SaveToFile( commonPath );
561 }
562
563 return true;
564 }
565
566
GetPreviousVersionPaths(std::vector<wxString> * aPaths)567 bool SETTINGS_MANAGER::GetPreviousVersionPaths( std::vector<wxString>* aPaths )
568 {
569 wxASSERT( aPaths );
570
571 aPaths->clear();
572
573 wxDir dir;
574 std::vector<wxFileName> base_paths;
575
576 base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false ), "" ) );
577
578 // If the env override is set, also check the default paths
579 if( wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), nullptr ) )
580 base_paths.emplace_back( wxFileName( calculateUserSettingsPath( false, false ), "" ) );
581
582 #ifdef __WXGTK__
583 // When running inside FlatPak, KIPLATFORM::ENV::GetUserConfigPath() will return a sandboxed
584 // path. In case the user wants to move from non-FlatPak KiCad to FlatPak KiCad, let's add our
585 // best guess as to the non-FlatPak config path. Unfortunately FlatPak also hides the host
586 // XDG_CONFIG_HOME, so if the user customizes their config path, they will have to browse
587 // for it.
588 {
589 wxFileName wxGtkPath;
590 wxGtkPath.AssignDir( "~/.config/kicad" );
591 wxGtkPath.MakeAbsolute();
592 base_paths.emplace_back( wxGtkPath.GetPath() );
593
594 // We also want to pick up regular flatpak if we are nightly
595 wxGtkPath.AssignDir( "~/.var/app/org.kicad.KiCad/config/kicad" );
596 wxGtkPath.MakeAbsolute();
597 base_paths.emplace_back( wxGtkPath.GetPath() );
598 }
599 #endif
600
601 wxString subdir;
602 std::string mine = GetSettingsVersion();
603
604 auto check_dir = [&] ( const wxString& aSubDir )
605 {
606 // Only older versions are valid for migration
607 if( compareVersions( aSubDir.ToStdString(), mine ) <= 0 )
608 {
609 wxString sub_path = dir.GetNameWithSep() + aSubDir;
610
611 if( IsSettingsPathValid( sub_path ) )
612 {
613 aPaths->push_back( sub_path );
614 wxLogTrace( traceSettings, "GetPreviousVersionName: %s is valid", sub_path );
615 }
616 }
617 };
618
619 std::set<wxString> checkedPaths;
620
621 for( auto base_path : base_paths )
622 {
623 if( checkedPaths.count( base_path.GetFullPath() ) )
624 continue;
625
626 checkedPaths.insert( base_path.GetFullPath() );
627
628 if( !dir.Open( base_path.GetFullPath() ) )
629 {
630 wxLogTrace( traceSettings, "GetPreviousVersionName: could not open base path %s",
631 base_path.GetFullPath() );
632 continue;
633 }
634
635 wxLogTrace( traceSettings, "GetPreviousVersionName: checking base path %s",
636 base_path.GetFullPath() );
637
638 if( dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS ) )
639 {
640 if( subdir != mine )
641 check_dir( subdir );
642
643 while( dir.GetNext( &subdir ) )
644 {
645 if( subdir != mine )
646 check_dir( subdir );
647 }
648 }
649
650 // If we didn't find one yet, check for legacy settings without a version directory
651 if( IsSettingsPathValid( dir.GetNameWithSep() ) )
652 {
653 wxLogTrace( traceSettings,
654 "GetPreviousVersionName: root path %s is valid", dir.GetName() );
655 aPaths->push_back( dir.GetName() );
656 }
657 }
658
659 return aPaths->size() > 0;
660 }
661
662
IsSettingsPathValid(const wxString & aPath)663 bool SETTINGS_MANAGER::IsSettingsPathValid( const wxString& aPath )
664 {
665 wxFileName test( aPath, "kicad_common" );
666
667 if( test.Exists() )
668 return true;
669
670 test.SetExt( "json" );
671
672 return test.Exists();
673 }
674
675
GetColorSettingsPath()676 wxString SETTINGS_MANAGER::GetColorSettingsPath()
677 {
678 wxFileName path;
679
680 path.AssignDir( GetUserSettingsPath() );
681 path.AppendDir( "colors" );
682
683 if( !path.DirExists() )
684 {
685 if( !wxMkdir( path.GetPath() ) )
686 {
687 wxLogTrace( traceSettings,
688 "GetColorSettingsPath(): Path %s missing and could not be created!",
689 path.GetPath() );
690 }
691 }
692
693 return path.GetPath();
694 }
695
696
GetUserSettingsPath()697 wxString SETTINGS_MANAGER::GetUserSettingsPath()
698 {
699 static wxString user_settings_path;
700
701 if( user_settings_path.empty() )
702 user_settings_path = calculateUserSettingsPath();
703
704 return user_settings_path;
705 }
706
707
calculateUserSettingsPath(bool aIncludeVer,bool aUseEnv)708 wxString SETTINGS_MANAGER::calculateUserSettingsPath( bool aIncludeVer, bool aUseEnv )
709 {
710 wxFileName cfgpath;
711
712 // http://docs.wxwidgets.org/3.0/classwx_standard_paths.html#a7c7cf595d94d29147360d031647476b0
713
714 wxString envstr;
715 if( aUseEnv && wxGetEnv( wxT( "KICAD_CONFIG_HOME" ), &envstr ) && !envstr.IsEmpty() )
716 {
717 // Override the assignment above with KICAD_CONFIG_HOME
718 cfgpath.AssignDir( envstr );
719 }
720 else
721 {
722 cfgpath.AssignDir( KIPLATFORM::ENV::GetUserConfigPath() );
723
724 cfgpath.AppendDir( TO_STR( KICAD_CONFIG_DIR ) );
725 }
726
727 if( aIncludeVer )
728 cfgpath.AppendDir( GetSettingsVersion() );
729
730 return cfgpath.GetPath();
731 }
732
733
GetSettingsVersion()734 std::string SETTINGS_MANAGER::GetSettingsVersion()
735 {
736 // CMake computes the major.minor string for us.
737 return GetMajorMinorVersion().ToStdString();
738 }
739
740
compareVersions(const std::string & aFirst,const std::string & aSecond)741 int SETTINGS_MANAGER::compareVersions( const std::string& aFirst, const std::string& aSecond )
742 {
743 int a_maj = 0;
744 int a_min = 0;
745 int b_maj = 0;
746 int b_min = 0;
747
748 if( !extractVersion( aFirst, &a_maj, &a_min ) || !extractVersion( aSecond, &b_maj, &b_min ) )
749 {
750 wxLogTrace( traceSettings, "compareSettingsVersions: bad input (%s, %s)", aFirst, aSecond );
751 return -1;
752 }
753
754 if( a_maj < b_maj )
755 {
756 return -1;
757 }
758 else if( a_maj > b_maj )
759 {
760 return 1;
761 }
762 else
763 {
764 if( a_min < b_min )
765 {
766 return -1;
767 }
768 else if( a_min > b_min )
769 {
770 return 1;
771 }
772 else
773 {
774 return 0;
775 }
776 }
777 }
778
779
extractVersion(const std::string & aVersionString,int * aMajor,int * aMinor)780 bool SETTINGS_MANAGER::extractVersion( const std::string& aVersionString, int* aMajor, int* aMinor )
781 {
782 std::regex re_version( "(\\d+)\\.(\\d+)" );
783 std::smatch match;
784
785 if( std::regex_match( aVersionString, match, re_version ) )
786 {
787 try
788 {
789 *aMajor = std::stoi( match[1].str() );
790 *aMinor = std::stoi( match[2].str() );
791 }
792 catch( ... )
793 {
794 return false;
795 }
796
797 return true;
798 }
799
800 return false;
801 }
802
803
LoadProject(const wxString & aFullPath,bool aSetActive)804 bool SETTINGS_MANAGER::LoadProject( const wxString& aFullPath, bool aSetActive )
805 {
806 // Normalize path to new format even if migrating from a legacy file
807 wxFileName path( aFullPath );
808
809 if( path.GetExt() == LegacyProjectFileExtension )
810 path.SetExt( ProjectFileExtension );
811
812 wxString fullPath = path.GetFullPath();
813
814 // If already loaded, we are all set. This might be called more than once over a project's
815 // lifetime in case the project is first loaded by the KiCad manager and then eeschema or
816 // pcbnew try to load it again when they are launched.
817 if( m_projects.count( fullPath ) )
818 return true;
819
820 bool readOnly = false;
821 std::unique_ptr<wxSingleInstanceChecker> lockFile = ::LockFile( fullPath );
822
823 if( !lockFile )
824 {
825 wxLogTrace( traceSettings, "Project %s is locked; opening read-only", fullPath );
826 readOnly = true;
827 }
828
829 // No MDI yet
830 if( aSetActive && !m_projects.empty() )
831 {
832 PROJECT* oldProject = m_projects.begin()->second;
833 unloadProjectFile( oldProject, false );
834 m_projects.erase( m_projects.begin() );
835
836 auto it = std::find_if( m_projects_list.begin(), m_projects_list.end(),
837 [&]( const std::unique_ptr<PROJECT>& ptr )
838 {
839 return ptr.get() == oldProject;
840 } );
841
842 wxASSERT( it != m_projects_list.end() );
843 m_projects_list.erase( it );
844 }
845
846 wxLogTrace( traceSettings, "Load project %s", fullPath );
847
848 std::unique_ptr<PROJECT> project = std::make_unique<PROJECT>();
849 project->setProjectFullName( fullPath );
850
851 bool success = loadProjectFile( *project );
852
853 if( success )
854 {
855 project->SetReadOnly( readOnly || project->GetProjectFile().IsReadOnly() );
856
857 if( lockFile )
858 m_project_lock.reset( lockFile.release() );
859 }
860
861 m_projects_list.push_back( std::move( project ) );
862 m_projects[fullPath] = m_projects_list.back().get();
863
864 wxString fn( path.GetName() );
865
866 PROJECT_LOCAL_SETTINGS* settings = new PROJECT_LOCAL_SETTINGS( m_projects[fullPath], fn );
867
868 if( aSetActive )
869 settings = RegisterSettings( settings );
870
871 m_projects[fullPath]->setLocalSettings( settings );
872
873 if( aSetActive && m_kiway )
874 m_kiway->ProjectChanged();
875
876 return success;
877 }
878
879
UnloadProject(PROJECT * aProject,bool aSave)880 bool SETTINGS_MANAGER::UnloadProject( PROJECT* aProject, bool aSave )
881 {
882 if( !aProject || !m_projects.count( aProject->GetProjectFullName() ) )
883 return false;
884
885 if( !unloadProjectFile( aProject, aSave ) )
886 return false;
887
888 wxString projectPath = aProject->GetProjectFullName();
889 wxLogTrace( traceSettings, "Unload project %s", projectPath );
890
891 PROJECT* toRemove = m_projects.at( projectPath );
892 auto it = std::find_if( m_projects_list.begin(), m_projects_list.end(),
893 [&]( const std::unique_ptr<PROJECT>& ptr )
894 {
895 return ptr.get() == toRemove;
896 } );
897
898 wxASSERT( it != m_projects_list.end() );
899 m_projects_list.erase( it );
900
901 m_projects.erase( projectPath );
902
903 // Immediately reload a null project; this is required until the rest of the application
904 // is refactored to not assume that Prj() always works
905 if( m_projects.empty() )
906 LoadProject( "" );
907
908 // Remove the reference in the environment to the previous project
909 wxSetEnv( PROJECT_VAR_NAME, "" );
910
911 // Release lock on the file, in case we had one
912 m_project_lock = nullptr;
913
914 if( m_kiway )
915 m_kiway->ProjectChanged();
916
917 return true;
918 }
919
920
Prj() const921 PROJECT& SETTINGS_MANAGER::Prj() const
922 {
923 // No MDI yet: First project in the list is the active project
924 wxASSERT_MSG( m_projects_list.size(), "no project in list" );
925 return *m_projects_list.begin()->get();
926 }
927
928
IsProjectOpen() const929 bool SETTINGS_MANAGER::IsProjectOpen() const
930 {
931 return !m_projects.empty();
932 }
933
934
GetProject(const wxString & aFullPath) const935 PROJECT* SETTINGS_MANAGER::GetProject( const wxString& aFullPath ) const
936 {
937 if( m_projects.count( aFullPath ) )
938 return m_projects.at( aFullPath );
939
940 return nullptr;
941 }
942
943
GetOpenProjects() const944 std::vector<wxString> SETTINGS_MANAGER::GetOpenProjects() const
945 {
946 std::vector<wxString> ret;
947
948 for( const std::pair<const wxString, PROJECT*>& pair : m_projects )
949 ret.emplace_back( pair.first );
950
951 return ret;
952 }
953
954
SaveProject(const wxString & aFullPath)955 bool SETTINGS_MANAGER::SaveProject( const wxString& aFullPath )
956 {
957 wxString path = aFullPath;
958
959 if( path.empty() )
960 path = Prj().GetProjectFullName();
961
962 // TODO: refactor for MDI
963 if( Prj().IsReadOnly() )
964 return false;
965
966 if( !m_project_files.count( path ) )
967 return false;
968
969 PROJECT_FILE* project = m_project_files.at( path );
970 wxString projectPath = GetPathForSettingsFile( project );
971
972 project->SaveToFile( projectPath );
973 Prj().GetLocalSettings().SaveToFile( projectPath );
974
975 return true;
976 }
977
978
SaveProjectAs(const wxString & aFullPath)979 void SETTINGS_MANAGER::SaveProjectAs( const wxString& aFullPath )
980 {
981 wxString oldName = Prj().GetProjectFullName();
982
983 if( aFullPath.IsSameAs( oldName ) )
984 {
985 SaveProject( aFullPath );
986 return;
987 }
988
989 // Changing this will cause UnloadProject to not save over the "old" project when loading below
990 Prj().setProjectFullName( aFullPath );
991
992 wxFileName fn( aFullPath );
993
994 PROJECT_FILE* project = m_project_files.at( oldName );
995
996 // Ensure read-only flags are copied; this allows doing a "Save As" on a standalong board/sch
997 // without creating project files if the checkbox is turned off
998 project->SetReadOnly( Prj().IsReadOnly() );
999 Prj().GetLocalSettings().SetReadOnly( Prj().IsReadOnly() );
1000
1001 project->SetFilename( fn.GetName() );
1002 project->SaveToFile( fn.GetPath() );
1003
1004 Prj().GetLocalSettings().SetFilename( fn.GetName() );
1005 Prj().GetLocalSettings().SaveToFile( fn.GetPath() );
1006
1007 m_project_files[fn.GetFullPath()] = project;
1008 m_project_files.erase( oldName );
1009
1010 m_projects[fn.GetFullPath()] = m_projects[oldName];
1011 m_projects.erase( oldName );
1012 }
1013
1014
SaveProjectCopy(const wxString & aFullPath)1015 void SETTINGS_MANAGER::SaveProjectCopy( const wxString& aFullPath )
1016 {
1017 PROJECT_FILE* project = m_project_files.at( Prj().GetProjectFullName() );
1018 wxString oldName = project->GetFilename();
1019 wxFileName fn( aFullPath );
1020
1021 bool readOnly = project->IsReadOnly();
1022 project->SetReadOnly( false );
1023
1024 project->SetFilename( fn.GetName() );
1025 project->SaveToFile( fn.GetPath() );
1026 project->SetFilename( oldName );
1027
1028 Prj().GetLocalSettings().SetFilename( fn.GetName() );
1029 Prj().GetLocalSettings().SaveToFile( fn.GetPath() );
1030 Prj().GetLocalSettings().SetFilename( oldName );
1031
1032 project->SetReadOnly( readOnly );
1033 }
1034
1035
loadProjectFile(PROJECT & aProject)1036 bool SETTINGS_MANAGER::loadProjectFile( PROJECT& aProject )
1037 {
1038 wxFileName fullFn( aProject.GetProjectFullName() );
1039 wxString fn( fullFn.GetName() );
1040
1041 PROJECT_FILE* file = RegisterSettings( new PROJECT_FILE( fn ), false );
1042
1043 m_project_files[aProject.GetProjectFullName()] = file;
1044
1045 aProject.setProjectFile( file );
1046 file->SetProject( &aProject );
1047
1048 wxString path( fullFn.GetPath() );
1049
1050 return file->LoadFromFile( path );
1051 }
1052
1053
unloadProjectFile(PROJECT * aProject,bool aSave)1054 bool SETTINGS_MANAGER::unloadProjectFile( PROJECT* aProject, bool aSave )
1055 {
1056 if( !aProject )
1057 return false;
1058
1059 wxString name = aProject->GetProjectFullName();
1060
1061 if( !m_project_files.count( name ) )
1062 return false;
1063
1064 PROJECT_FILE* file = m_project_files[name];
1065
1066 auto it = std::find_if( m_settings.begin(), m_settings.end(),
1067 [&file]( const std::unique_ptr<JSON_SETTINGS>& aPtr )
1068 {
1069 return aPtr.get() == file;
1070 } );
1071
1072 if( it != m_settings.end() )
1073 {
1074 wxString projectPath = GetPathForSettingsFile( it->get() );
1075
1076 FlushAndRelease( &aProject->GetLocalSettings(), aSave );
1077
1078 if( aSave )
1079 ( *it )->SaveToFile( projectPath );
1080
1081 m_settings.erase( it );
1082 }
1083
1084 m_project_files.erase( name );
1085
1086 return true;
1087 }
1088
1089
GetProjectBackupsPath() const1090 wxString SETTINGS_MANAGER::GetProjectBackupsPath() const
1091 {
1092 return Prj().GetProjectPath() + Prj().GetProjectName() + PROJECT_BACKUPS_DIR_SUFFIX;
1093 }
1094
1095
1096 wxString SETTINGS_MANAGER::backupDateTimeFormat = wxT( "%Y-%m-%d_%H%M%S" );
1097
1098
BackupProject(REPORTER & aReporter) const1099 bool SETTINGS_MANAGER::BackupProject( REPORTER& aReporter ) const
1100 {
1101 wxDateTime timestamp = wxDateTime::Now();
1102
1103 wxString fileName = wxString::Format( wxT( "%s-%s" ), Prj().GetProjectName(),
1104 timestamp.Format( backupDateTimeFormat ) );
1105
1106 wxFileName target;
1107 target.SetPath( GetProjectBackupsPath() );
1108 target.SetName( fileName );
1109 target.SetExt( ArchiveFileExtension );
1110
1111 wxDir dir( target.GetPath() );
1112
1113 if( !target.DirExists() && !wxMkdir( target.GetPath() ) )
1114 {
1115 wxLogTrace( traceSettings, "Could not create project backup path %s", target.GetPath() );
1116 return false;
1117 }
1118
1119 if( !target.IsDirWritable() )
1120 {
1121 wxLogTrace( traceSettings, "Backup directory %s is not writable", target.GetPath() );
1122 return false;
1123 }
1124
1125 wxLogTrace( traceSettings, "Backing up project to %s", target.GetPath() );
1126
1127 PROJECT_ARCHIVER archiver;
1128
1129 return archiver.Archive( Prj().GetProjectPath(), target.GetFullPath(), aReporter );
1130 }
1131
1132
1133 class VECTOR_INSERT_TRAVERSER : public wxDirTraverser
1134 {
1135 public:
VECTOR_INSERT_TRAVERSER(std::vector<wxString> & aVec,std::function<bool (const wxString &)> aCond)1136 VECTOR_INSERT_TRAVERSER( std::vector<wxString>& aVec,
1137 std::function<bool( const wxString& )> aCond ) :
1138 m_files( aVec ),
1139 m_condition( aCond )
1140 {
1141 }
1142
OnFile(const wxString & aFile)1143 wxDirTraverseResult OnFile( const wxString& aFile ) override
1144 {
1145 if( m_condition( aFile ) )
1146 m_files.emplace_back( aFile );
1147
1148 return wxDIR_CONTINUE;
1149 }
1150
OnDir(const wxString & aDirName)1151 wxDirTraverseResult OnDir( const wxString& aDirName ) override
1152 {
1153 return wxDIR_CONTINUE;
1154 }
1155
1156 private:
1157 std::vector<wxString>& m_files;
1158
1159 std::function<bool( const wxString& )> m_condition;
1160 };
1161
1162
TriggerBackupIfNeeded(REPORTER & aReporter) const1163 bool SETTINGS_MANAGER::TriggerBackupIfNeeded( REPORTER& aReporter ) const
1164 {
1165 COMMON_SETTINGS::AUTO_BACKUP settings = GetCommonSettings()->m_Backup;
1166
1167 if( !settings.enabled )
1168 return true;
1169
1170 wxString prefix = Prj().GetProjectName() + '-';
1171
1172 auto modTime =
1173 [&prefix]( const wxString& aFile )
1174 {
1175 wxDateTime dt;
1176 wxString fn( wxFileName( aFile ).GetName() );
1177 fn.Replace( prefix, "" );
1178 dt.ParseFormat( fn, backupDateTimeFormat );
1179 return dt;
1180 };
1181
1182 wxFileName projectPath( Prj().GetProjectPath() );
1183
1184 // Skip backup if project path isn't valid or writable
1185 if( !projectPath.IsOk() || !projectPath.Exists() || !projectPath.IsDirWritable() )
1186 return true;
1187
1188 wxString backupPath = GetProjectBackupsPath();
1189
1190 if( !wxDirExists( backupPath ) )
1191 {
1192 wxLogTrace( traceSettings, "Backup path %s doesn't exist, creating it", backupPath );
1193
1194 if( !wxMkdir( backupPath ) )
1195 {
1196 wxLogTrace( traceSettings, "Could not create backups path! Skipping backup" );
1197 return false;
1198 }
1199 }
1200
1201 wxDir dir( backupPath );
1202
1203 if( !dir.IsOpened() )
1204 {
1205 wxLogTrace( traceSettings, "Could not open project backups path %s", dir.GetName() );
1206 return false;
1207 }
1208
1209 std::vector<wxString> files;
1210
1211 VECTOR_INSERT_TRAVERSER traverser( files,
1212 [&modTime]( const wxString& aFile )
1213 {
1214 return modTime( aFile ).IsValid();
1215 } );
1216
1217 dir.Traverse( traverser, wxT( "*.zip" ) );
1218
1219 // Sort newest-first
1220 std::sort( files.begin(), files.end(),
1221 [&]( const wxString& aFirst, const wxString& aSecond ) -> bool
1222 {
1223 wxDateTime first = modTime( aFirst );
1224 wxDateTime second = modTime( aSecond );
1225
1226 return first.GetTicks() > second.GetTicks();
1227 } );
1228
1229 // Do we even need to back up?
1230 if( !files.empty() )
1231 {
1232 wxDateTime lastTime = modTime( files[0] );
1233
1234 if( lastTime.IsValid() )
1235 {
1236 wxTimeSpan delta = wxDateTime::Now() - modTime( files[0] );
1237
1238 if( delta.IsShorterThan( wxTimeSpan::Seconds( settings.min_interval ) ) )
1239 return true;
1240 }
1241 }
1242
1243 // Now that we know a backup is needed, apply the retention policy
1244
1245 // Step 1: if we're over the total file limit, remove the oldest
1246 if( !files.empty() && settings.limit_total_files > 0 )
1247 {
1248 while( files.size() > static_cast<size_t>( settings.limit_total_files ) )
1249 {
1250 wxRemoveFile( files.back() );
1251 files.pop_back();
1252 }
1253 }
1254
1255 // Step 2: Stay under the total size limit
1256 if( settings.limit_total_size > 0 )
1257 {
1258 wxULongLong totalSize = 0;
1259
1260 for( const wxString& file : files )
1261 totalSize += wxFileName::GetSize( file );
1262
1263 while( !files.empty() && totalSize > static_cast<wxULongLong>( settings.limit_total_size ) )
1264 {
1265 totalSize -= wxFileName::GetSize( files.back() );
1266 wxRemoveFile( files.back() );
1267 files.pop_back();
1268 }
1269 }
1270
1271 // Step 3: Stay under the daily limit
1272 if( settings.limit_daily_files > 0 && files.size() > 1 )
1273 {
1274 wxDateTime day = modTime( files[0] );
1275 int num = 1;
1276
1277 wxASSERT( day.IsValid() );
1278
1279 std::vector<wxString> filesToDelete;
1280
1281 for( size_t i = 1; i < files.size(); i++ )
1282 {
1283 wxDateTime dt = modTime( files[i] );
1284
1285 if( dt.IsSameDate( day ) )
1286 {
1287 num++;
1288
1289 if( num > settings.limit_daily_files )
1290 filesToDelete.emplace_back( files[i] );
1291 }
1292 else
1293 {
1294 day = dt;
1295 num = 1;
1296 }
1297 }
1298
1299 for( const wxString& file : filesToDelete )
1300 wxRemoveFile( file );
1301 }
1302
1303 return BackupProject( aReporter );
1304 }
1305