KiCad PCB EDA Suite
Loading...
Searching...
No Matches
json_settings.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2020 Jon Evans <[email protected]>
5 * Copyright The 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
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <algorithm>
22#include <fstream>
23#include <iomanip>
24#include <utility>
25#include <sstream>
26
27#include <kiplatform/io.h>
28#include <locale_io.h>
29#include <gal/color4d.h>
33#include <settings/parameters.h>
37#include <wx/aui/framemanager.h>
38#include <wx/config.h>
39#include <wx/debug.h>
40#include <wx/fileconf.h>
41#include <wx/filename.h>
42#include <wx/gdicmn.h>
43#include <wx/log.h>
44#include <wx/stdstream.h>
45#include <wx/wfstream.h>
46
47
48nlohmann::json::json_pointer JSON_SETTINGS_INTERNALS::PointerFromString( std::string aPath )
49{
50 std::replace( aPath.begin(), aPath.end(), '.', '/' );
51 aPath.insert( 0, "/" );
52
53 nlohmann::json::json_pointer p;
54
55 try
56 {
57 p = nlohmann::json::json_pointer( aPath );
58 }
59 catch( ... )
60 {
61 wxASSERT_MSG( false, wxT( "Invalid pointer path in PointerFromString!" ) );
62 }
63
64 return p;
65}
66
67
68JSON_SETTINGS::JSON_SETTINGS( const wxString& aFilename, SETTINGS_LOC aLocation,
69 int aSchemaVersion, bool aCreateIfMissing, bool aCreateIfDefault,
70 bool aWriteFile ) :
71 m_filename( aFilename ),
73 m_location( aLocation ),
74 m_createIfMissing( aCreateIfMissing ),
75 m_createIfDefault( aCreateIfDefault ),
76 m_writeFile( aWriteFile ),
77 m_modified( false ),
78 m_fileSynced( false ),
79 m_lastSaveFailed( false ),
82 m_schemaVersion( aSchemaVersion ),
83 m_isFutureFormat( false ),
84 m_manager( nullptr )
85{
86 m_internals = std::make_unique<JSON_SETTINGS_INTERNALS>();
87
88 try
89 {
90 m_internals->SetFromString( "meta.filename", GetFullFilename() );
91 }
92 catch( ... )
93 {
94 wxLogTrace( traceSettings, wxT( "Error: Could not create filename field for %s" ), GetFullFilename() );
95 }
96
97
98 m_params.emplace_back( new PARAM<int>( "meta.version", &m_schemaVersion, m_schemaVersion, true ) );
99}
100
101
103{
104 // ReleaseNestedSettings erases from m_nested_settings, so snapshot first to avoid
105 // iterating an invalidated container
106 std::vector<NESTED_SETTINGS*> nestedToRelease( m_nested_settings );
107
108 for( NESTED_SETTINGS* nested : nestedToRelease )
109 ReleaseNestedSettings( nested );
110
111 for( PARAM_BASE* param: m_params )
112 delete param;
113
114 m_params.clear();
115}
116
117
119{
120 if( m_filename.AfterLast( '.' ) == getFileExt() )
121 return m_filename;
122
123 return wxString( m_filename + "." + getFileExt() );
124}
125
126
127nlohmann::json& JSON_SETTINGS::At( const std::string& aPath )
128{
129 return m_internals->At( aPath );
130}
131
132
133bool JSON_SETTINGS::Contains( const std::string& aPath ) const
134{
135 return m_internals->contains( JSON_SETTINGS_INTERNALS::PointerFromString( aPath ) );
136}
137
138
143
144
146{
147 for( PARAM_BASE* param : m_params )
148 {
149 try
150 {
151 param->Load( *this, m_resetParamsIfMissing );
152 }
153 catch( ... )
154 {
155 // Skip unreadable parameters in file
156 wxLogTrace( traceSettings, wxT( "param '%s' load err" ), param->GetJsonPath().c_str() );
157 }
158 }
159}
160
161
162bool JSON_SETTINGS::LoadFromFile( const wxString& aDirectory )
163{
164 // First, load all params to default values
165 m_internals->clear();
166 Load();
167
168 bool success = true;
169 bool migrated = false;
170 bool legacy_migrated = false;
171
172 LOCALE_IO locale;
173
174 auto migrateFromLegacy =
175 [&] ( wxFileName& aPath )
176 {
177 // Backup and restore during migration so that the original can be mutated if
178 // convenient
179 bool backed_up = false;
180 wxFileName temp;
181
182 if( aPath.IsDirWritable() )
183 {
184 temp.AssignTempFileName( aPath.GetFullPath() );
185
186 if( !wxCopyFile( aPath.GetFullPath(), temp.GetFullPath() ) )
187 {
188 wxLogTrace( traceSettings, wxT( "%s: could not create temp file for migration" ),
189 GetFullFilename() );
190 }
191 else
192 {
193 backed_up = true;
194 }
195 }
196
197 // Silence popups if legacy file is read-only
198 wxLogNull doNotLog;
199
200 wxConfigBase::DontCreateOnDemand();
201 auto cfg = std::make_unique<wxFileConfig>( wxT( "" ), wxT( "" ), aPath.GetFullPath() );
202
203 // If migrate fails or is not implemented, fall back to built-in defaults that
204 // were already loaded above
205 if( !MigrateFromLegacy( cfg.get() ) )
206 {
207 success = false;
208 wxLogTrace( traceSettings, wxT( "%s: migrated; not all settings were found in legacy file" ),
209 GetFullFilename() );
210 }
211 else
212 {
213 success = true;
214 wxLogTrace( traceSettings, wxT( "%s: migrated from legacy format" ), GetFullFilename() );
215 }
216
217 if( backed_up )
218 {
219 cfg.reset();
220
221 if( !wxCopyFile( temp.GetFullPath(), aPath.GetFullPath() ) )
222 {
223 wxLogTrace( traceSettings, wxT( "migrate; copy temp file %s to %s failed" ),
224 temp.GetFullPath(), aPath.GetFullPath() );
225 }
226
227 if( !wxRemoveFile( temp.GetFullPath() ) )
228 {
229 wxLogTrace( traceSettings, wxT( "migrate; failed to remove temp file %s" ),
230 temp.GetFullPath() );
231 }
232 }
233
234 // Either way, we want to clean up the old file afterwards
235 legacy_migrated = true;
236 };
237
238 wxFileName path;
239
240 if( aDirectory.empty() )
241 {
242 path.Assign( m_filename );
243 path.SetExt( getFileExt() );
244 }
245 else
246 {
247 path.Assign( aDirectory, m_filename, getFileExt() );
248 }
249
250 if( !path.Exists() )
251 {
252 // Case 1: legacy migration, no .json extension yet
253 path.SetExt( getLegacyFileExt() );
254
255 if( path.Exists() )
256 {
257 migrateFromLegacy( path );
258 }
259 // Case 2: legacy filename is different from new one
260 else if( !m_legacy_filename.empty() )
261 {
262 path.SetName( m_legacy_filename );
263
264 if( path.Exists() )
265 migrateFromLegacy( path );
266 }
267 else
268 {
269 success = false;
270 }
271 }
272 else
273 {
274 if( !path.IsFileWritable() )
275 m_writeFile = false;
276
277 try
278 {
279 wxFFileInputStream fp( path.GetFullPath(), wxT( "rt" ) );
280 wxStdInputStream fstream( fp );
281
282 if( fp.IsOk() )
283 {
284 *static_cast<nlohmann::json*>( m_internals.get() ) = nlohmann::json::parse( fstream, nullptr,
287
288 // Save whatever we loaded, before doing any migration etc
289 m_internals->m_original = *static_cast<nlohmann::json*>( m_internals.get() );
290
291 // If parse succeeds, check if schema migration is required
292 int filever = -1;
293
294 try
295 {
296 filever = m_internals->Get<int>( "meta.version" );
297 }
298 catch( ... )
299 {
300 wxLogTrace( traceSettings, wxT( "%s: file version could not be read!" ), GetFullFilename() );
301 success = false;
302 }
303
304 if( filever >= 0 && filever < m_schemaVersion )
305 {
306 wxLogTrace( traceSettings, wxT( "%s: attempting migration from version %d to %d" ),
307 GetFullFilename(), filever, m_schemaVersion );
308
309 if( Migrate() )
310 {
311 migrated = true;
312 }
313 else
314 {
315 wxLogTrace( traceSettings, wxT( "%s: migration failed!" ), GetFullFilename() );
316 }
317 }
318 else if( filever > m_schemaVersion )
319 {
320 wxLogTrace( traceSettings, wxT( "%s: warning: file version %d is newer than latest (%d)" ),
321 GetFullFilename(), filever, m_schemaVersion );
322 m_isFutureFormat = true;
323 }
324 }
325 else
326 {
327 wxLogTrace( traceSettings, wxT( "%s exists but can't be opened for read" ), GetFullFilename() );
328 }
329 }
330 catch( nlohmann::json::parse_error& error )
331 {
332 success = false;
333 wxLogTrace( traceSettings, wxT( "Json parse error reading %s: %s" ), path.GetFullPath(), error.what() );
334 wxLogTrace( traceSettings, wxT( "Attempting migration in case file is in legacy format" ) );
335 migrateFromLegacy( path );
336 }
337 }
338
339 // Now that we have new data in the JSON structure, load the params again
340 Load();
341
342 // And finally load any nested settings
343 for( NESTED_SETTINGS* settings : m_nested_settings )
344 settings->LoadFromFile();
345
346 wxLogTrace( traceSettings, wxT( "Loaded <%s> with schema %d" ), GetFullFilename(), m_schemaVersion );
347
348 m_modified = false;
349
350 // A missing backing file leaves the store empty, which must not count as synchronized
351 m_fileSynced = success;
352
353 // Skip writeback when running QA tests
354 bool doWrite = m_writeFile && !wxGetEnv( wxT( "KICAD_INHIBIT_SETTINGS_WRITES" ), nullptr );
355
356 // If we migrated, clean up the legacy file (with no extension). Save the migrated
357 // contents FIRST so that if the save fails we still have the legacy file on disk to
358 // fall back to -- otherwise a crash mid-migration leaves the user with neither copy.
359 if( doWrite && ( legacy_migrated || migrated ) )
360 {
362 {
363 bool savedMigrated = SaveToFile( aDirectory, true );
364
365 if( savedMigrated && legacy_migrated && !wxRemoveFile( path.GetFullPath() ) )
366 {
367 wxLogTrace( traceSettings, wxT( "Warning: could not remove legacy file %s" ), path.GetFullPath() );
368 }
369 else if( !savedMigrated )
370 {
371 wxLogTrace( traceSettings, wxT( "Migrated save of %s failed; leaving legacy file intact" ),
372 GetFullFilename() );
373 }
374 }
375 }
376
377 return success;
378}
379
380
382{
383 for( PARAM_BASE* param : m_params )
384 {
385 try
386 {
387 m_modified |= !param->MatchesFile( *this );
388 param->Store( this );
389 }
390 catch( const std::exception& e )
391 {
392 wxLogTrace( traceSettings, wxT( "param '%s' store err: %s" ), param->GetJsonPath().c_str(), e.what() );
393 }
394 }
395
396 return m_modified;
397}
398
399
401{
402 for( PARAM_BASE* param : m_params )
403 param->SetDefault();
404}
405
406
407std::map<std::string, nlohmann::json> JSON_SETTINGS::GetFileHistories()
408{
409 std::map<std::string, nlohmann::json> histories;
410
411 for( const std::string& candidate : { std::string( "system.file_history" ) } )
412 {
413 if( Contains( candidate ) )
414 histories[candidate] = GetJson( candidate ).value();
415 }
416
417 return histories;
418}
419
420
421bool JSON_SETTINGS::SaveToFile( const wxString& aDirectory, bool aForce )
422{
423 m_lastSaveFailed = false;
424
425 if( !m_writeFile )
426 return false;
427
428 // Default PROJECT won't have a filename set
429 if( m_filename.IsEmpty() )
430 return false;
431
432 wxFileName path;
433
434 if( aDirectory.empty() )
435 {
436 path.Assign( m_filename );
437 path.SetExt( getFileExt() );
438 }
439 else
440 {
441 wxString dir( aDirectory );
442 path.Assign( dir, m_filename, getFileExt() );
443 }
444
445 if( !m_createIfMissing && !path.FileExists() )
446 {
447 wxLogTrace( traceSettings,
448 wxT( "File for %s doesn't exist and m_createIfMissing == false; not saving" ),
449 GetFullFilename() );
450 return false;
451 }
452
453 bool modified = flushToStore();
454
455 if( !modified && !aForce && path.FileExists() )
456 {
457 wxLogTrace( traceSettings, wxT( "%s contents not modified, skipping save" ), GetFullFilename() );
458 return false;
459 }
460 else if( !modified && !aForce && !m_createIfDefault )
461 {
462 wxLogTrace( traceSettings, wxT( "%s contents still default and m_createIfDefault == false; not saving" ),
463 GetFullFilename() );
464 return false;
465 }
466
467 wxLogTrace( traceSettings, wxT( "Saving %s" ), GetFullFilename() );
468
469 bool success = true;
470
471 try
472 {
473 std::string payload = formatFileContents();
474 wxString writeError;
475
476 // Last-chance skip for the case where the dirty heuristic fired but the serialized payload
477 // still equals the on-disk bytes (e.g. key reordering or normalization). Avoids bumping the
478 // project file timestamps for a no-op rewrite; see #24402. A genuine change yields a
479 // differing payload and is always written.
480 if( !aForce && path.FileExists() )
481 {
482 std::ifstream existing( path.GetFullPath().fn_str(), std::ios::in | std::ios::binary );
483
484 if( existing )
485 {
486 std::string current( ( std::istreambuf_iterator<char>( existing ) ),
487 std::istreambuf_iterator<char>() );
488
489 // Only trust an equal comparison from a clean read; on any read error fall through
490 // and write, preferring data safety over avoiding a rewrite.
491 if( !existing.bad() && current == payload )
492 {
493 wxLogTrace( traceSettings, wxT( "%s on-disk contents match payload, skipping write" ),
494 GetFullFilename() );
495
496 m_modified = false;
497
498 return false;
499 }
500 }
501 }
502
503 // The destination is only checked once there is something to write, so that a store with
504 // no changes is skipped rather than reported as a failure to save
505 if( !path.DirExists() && !path.Mkdir() )
506 {
507 wxLogTrace( traceSettings, wxT( "Warning: could not create path %s, can't save %s" ),
508 path.GetPath(), GetFullFilename() );
509 success = false;
510 }
511 else if( ( path.FileExists() && !path.IsFileWritable() )
512 || ( !path.FileExists() && !path.IsDirWritable() ) )
513 {
514 wxLogTrace( traceSettings, wxT( "File for %s is read-only; not saving" ), GetFullFilename() );
515 success = false;
516 }
517 else if( !KIPLATFORM::IO::AtomicWriteFile( path.GetFullPath(), payload.data(), payload.size(),
518 &writeError ) )
519 {
520 wxLogTrace( traceSettings, wxT( "Warning: could not save %s: %s" ), GetFullFilename(), writeError );
521 success = false;
522 }
523 }
524 catch( nlohmann::json::exception& error )
525 {
526 wxLogTrace( traceSettings, wxT( "Catch error: could not save %s. Json error %s" ), GetFullFilename(),
527 error.what() );
528 success = false;
529 }
530 catch( ... )
531 {
532 wxLogTrace( traceSettings, wxT( "Error: could not save %s." ) );
533 success = false;
534 }
535
536 if( success )
537 {
538 m_modified = false;
539 m_fileSynced = true;
540 }
541
542 m_lastSaveFailed = !success;
543
544 return success;
545}
546
547
549{
550 bool modified = false;
551
552 for( NESTED_SETTINGS* settings : m_nested_settings )
553 {
554 wxCHECK2( settings, continue );
555
556 modified |= settings->SaveToFile();
557 }
558
559 modified |= Store();
560
561 return modified;
562}
563
564
565const nlohmann::json& JSON_SETTINGS::originalJson() const
566{
567 return m_internals->m_original;
568}
569
570
572{
574
575 nlohmann::json toSave = m_internals->m_original;
576
577 for( PARAM_BASE* param : m_params )
578 {
579 if( param->ClearUnknownKeys() )
580 {
581 nlohmann::json_pointer p = JSON_SETTINGS_INTERNALS::PointerFromString( param->GetJsonPath() );
582
583 toSave[p] = nlohmann::json::object();
584 }
585 }
586
587 toSave.update( m_internals->begin(), m_internals->end(), /* merge_objects = */ true );
588
589 return toSave.dump( 2 ) + '\n';
590}
591
592
594{
595 Store();
596
598
599 std::stringstream buffer;
600 buffer << std::setw( 2 ) << *m_internals << std::endl;
601
602 return buffer.str();
603}
604
605
606bool JSON_SETTINGS::LoadFromRawFile( const wxString& aPath )
607{
608 try
609 {
610 wxFFileInputStream fp( aPath, wxT( "rt" ) );
611 wxStdInputStream fstream( fp );
612
613 if( fp.IsOk() )
614 {
615 *static_cast<nlohmann::json*>( m_internals.get() ) = nlohmann::json::parse( fstream, nullptr,
618 }
619 else
620 {
621 return false;
622 }
623 }
624 catch( nlohmann::json::parse_error& error )
625 {
626 wxLogTrace( traceSettings, wxT( "Json parse error reading %s: %s" ), aPath, error.what() );
627
628 return false;
629 }
630
631 // Now that we have new data in the JSON structure, load the params again
632 Load();
633 return true;
634}
635
636
637std::optional<nlohmann::json> JSON_SETTINGS::GetJson( const std::string& aPath ) const
638{
639 nlohmann::json::json_pointer ptr = m_internals->PointerFromString( aPath );
640
641 if( m_internals->contains( ptr ) )
642 {
643 try
644 {
645 return std::optional<nlohmann::json>{ m_internals->at( ptr ) };
646 }
647 catch( ... )
648 {
649 }
650 }
651
652 return std::optional<nlohmann::json>{};
653}
654
655
656template<typename ValueType>
657std::optional<ValueType> JSON_SETTINGS::Get( const std::string& aPath ) const
658{
659 if( std::optional<nlohmann::json> ret = GetJson( aPath ) )
660 {
661 try
662 {
663 return ret->get<ValueType>();
664 }
665 catch( ... )
666 {
667 }
668 }
669
670 return std::nullopt;
671}
672
673
674// Instantiate all required templates here to allow reducing scope of json.hpp
675template KICOMMON_API std::optional<bool>
676 JSON_SETTINGS::Get<bool>( const std::string& aPath ) const;
677template KICOMMON_API std::optional<double>
678 JSON_SETTINGS::Get<double>( const std::string& aPath ) const;
679template KICOMMON_API std::optional<float>
680 JSON_SETTINGS::Get<float>( const std::string& aPath ) const;
681template KICOMMON_API std::optional<int> JSON_SETTINGS::Get<int>( const std::string& aPath ) const;
682template KICOMMON_API std::optional<unsigned int>
683 JSON_SETTINGS::Get<unsigned int>( const std::string& aPath ) const;
684template KICOMMON_API std::optional<unsigned long long>
685 JSON_SETTINGS::Get<unsigned long long>( const std::string& aPath ) const;
686template KICOMMON_API std::optional<std::string>
687 JSON_SETTINGS::Get<std::string>( const std::string& aPath ) const;
688template KICOMMON_API std::optional<nlohmann::json>
689 JSON_SETTINGS::Get<nlohmann::json>( const std::string& aPath ) const;
690template KICOMMON_API std::optional<KIGFX::COLOR4D>
691 JSON_SETTINGS::Get<KIGFX::COLOR4D>( const std::string& aPath ) const;
692template KICOMMON_API std::optional<BOM_FIELD>
693 JSON_SETTINGS::Get<BOM_FIELD>( const std::string& aPath ) const;
694template KICOMMON_API std::optional<BOM_PRESET>
695 JSON_SETTINGS::Get<BOM_PRESET>( const std::string& aPath ) const;
696template KICOMMON_API std::optional<BOM_FMT_PRESET>
697 JSON_SETTINGS::Get<BOM_FMT_PRESET>( const std::string& aPath ) const;
698template KICOMMON_API std::optional<GRID>
699 JSON_SETTINGS::Get<GRID>( const std::string& aPath ) const;
700template KICOMMON_API std::optional<wxPoint>
701 JSON_SETTINGS::Get<wxPoint>( const std::string& aPath ) const;
702template KICOMMON_API std::optional<wxSize>
703 JSON_SETTINGS::Get<wxSize>( const std::string& aPath ) const;
704template KICOMMON_API std::optional<wxRect>
705 JSON_SETTINGS::Get<wxRect>( const std::string& aPath ) const;
706template KICOMMON_API std::optional<wxAuiPaneInfo>
707 JSON_SETTINGS::Get<wxAuiPaneInfo>( const std::string& aPath ) const;
708template KICOMMON_API std::optional<KIGFX::CROSS_HAIR_MODE>
709 JSON_SETTINGS::Get<KIGFX::CROSS_HAIR_MODE>( const std::string& aPath ) const;
710
711template<typename ValueType>
712void JSON_SETTINGS::Set( const std::string& aPath, ValueType aVal )
713{
714 m_internals->SetFromString( aPath, std::move( aVal ) );
715}
716
717
718// Instantiate all required templates here to allow reducing scope of json.hpp
719template KICOMMON_API void JSON_SETTINGS::Set<bool>( const std::string& aPath, bool aValue );
720template KICOMMON_API void JSON_SETTINGS::Set<double>( const std::string& aPath, double aValue );
721template KICOMMON_API void JSON_SETTINGS::Set<float>( const std::string& aPath, float aValue );
722template KICOMMON_API void JSON_SETTINGS::Set<int>( const std::string& aPath, int aValue );
723template KICOMMON_API void JSON_SETTINGS::Set<unsigned int>( const std::string& aPath,
724 unsigned int aValue );
725template KICOMMON_API void JSON_SETTINGS::Set<unsigned long long>( const std::string& aPath,
726 unsigned long long aValue );
727template KICOMMON_API void JSON_SETTINGS::Set<const char*>( const std::string& aPath,
728 const char* aValue );
729template KICOMMON_API void JSON_SETTINGS::Set<std::string>( const std::string& aPath,
730 std::string aValue );
731template KICOMMON_API void JSON_SETTINGS::Set<nlohmann::json>( const std::string& aPath,
732 nlohmann::json aValue );
733template KICOMMON_API void JSON_SETTINGS::Set<KIGFX::COLOR4D>( const std::string& aPath,
734 KIGFX::COLOR4D aValue );
735template KICOMMON_API void JSON_SETTINGS::Set<BOM_FIELD>( const std::string& aPath,
736 BOM_FIELD aValue );
737template KICOMMON_API void JSON_SETTINGS::Set<BOM_PRESET>( const std::string& aPath,
738 BOM_PRESET aValue );
739template KICOMMON_API void JSON_SETTINGS::Set<BOM_FMT_PRESET>( const std::string& aPath,
740 BOM_FMT_PRESET aValue );
741template KICOMMON_API void JSON_SETTINGS::Set<GRID>( const std::string& aPath, GRID aValue );
742template KICOMMON_API void JSON_SETTINGS::Set<wxPoint>( const std::string& aPath, wxPoint aValue );
743template KICOMMON_API void JSON_SETTINGS::Set<wxSize>( const std::string& aPath, wxSize aValue );
744template KICOMMON_API void JSON_SETTINGS::Set<wxRect>( const std::string& aPath, wxRect aValue );
745template KICOMMON_API void JSON_SETTINGS::Set<wxAuiPaneInfo>( const std::string& aPath,
746 wxAuiPaneInfo aValue );
747template KICOMMON_API void JSON_SETTINGS::Set<KIGFX::CROSS_HAIR_MODE>( const std::string& aPath,
748 KIGFX::CROSS_HAIR_MODE aValue );
749
750
751void JSON_SETTINGS::registerMigration( int aOldSchemaVersion, int aNewSchemaVersion,
752 std::function<bool()> aMigrator )
753{
754 wxASSERT( aNewSchemaVersion > aOldSchemaVersion );
755 wxASSERT( aNewSchemaVersion <= m_schemaVersion );
756 m_migrators[aOldSchemaVersion] = std::make_pair( aNewSchemaVersion, aMigrator );
757}
758
759
761{
762 int filever = m_internals->Get<int>( "meta.version" );
763
764 while( filever < m_schemaVersion )
765 {
766 wxASSERT( m_migrators.count( filever ) > 0 );
767
768 if( !m_migrators.count( filever ) )
769 {
770 wxLogTrace( traceSettings, wxT( "Migrator missing for %s version %d!" ), typeid( *this ).name(),
771 filever );
772 return false;
773 }
774
775 std::pair<int, std::function<bool()>> pair = m_migrators.at( filever );
776
777 if( pair.second() )
778 {
779 wxLogTrace( traceSettings, wxT( "Migrated %s from %d to %d" ), typeid( *this ).name(), filever,
780 pair.first );
781 filever = pair.first;
782 m_internals->At( "meta.version" ) = filever;
783 }
784 else
785 {
786 wxLogTrace( traceSettings, wxT( "Migration failed for %s from %d to %d" ), typeid( *this ).name(),
787 filever, pair.first );
788 return false;
789 }
790 }
791
792 return true;
793}
794
795
796bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig )
797{
798 wxLogTrace( traceSettings, wxT( "MigrateFromLegacy() not implemented for %s" ), typeid( *this ).name() );
799 return false;
800}
801
802
803bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, wxString& aTarget )
804{
805 nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath );
806
807 if( aObj.contains( ptr ) && aObj.at( ptr ).is_string() )
808 {
809 aTarget = aObj.at( ptr ).get<wxString>();
810 return true;
811 }
812
813 return false;
814}
815
816
817bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, bool& aTarget )
818{
819 nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath );
820
821 if( aObj.contains( ptr ) && aObj.at( ptr ).is_boolean() )
822 {
823 aTarget = aObj.at( ptr ).get<bool>();
824 return true;
825 }
826
827 return false;
828}
829
830
831bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, int& aTarget )
832{
833 nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath );
834
835 if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_integer() )
836 {
837 aTarget = aObj.at( ptr ).get<int>();
838 return true;
839 }
840
841 return false;
842}
843
844
845bool JSON_SETTINGS::SetIfPresent( const nlohmann::json& aObj, const std::string& aPath, unsigned int& aTarget )
846{
847 nlohmann::json::json_pointer ptr = JSON_SETTINGS_INTERNALS::PointerFromString( aPath );
848
849 if( aObj.contains( ptr ) && aObj.at( ptr ).is_number_unsigned() )
850 {
851 aTarget = aObj.at( ptr ).get<unsigned int>();
852 return true;
853 }
854
855 return false;
856}
857
858
859template<typename ValueType>
860bool JSON_SETTINGS::fromLegacy( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest )
861{
862 ValueType val;
863
864 if( aConfig->Read( aKey, &val ) )
865 {
866 try
867 {
868 ( *m_internals )[aDest] = val;
869 }
870 catch( ... )
871 {
872 wxASSERT_MSG( false, wxT( "Could not write value in fromLegacy!" ) );
873 return false;
874 }
875
876 return true;
877 }
878
879 return false;
880}
881
882
883// Explicitly declare these because we only support a few types anyway, and it means we can keep
884// wxConfig detail out of the header file
885template
886KICOMMON_API bool JSON_SETTINGS::fromLegacy<int>( wxConfigBase*, const std::string&, const std::string& );
887
888template
889KICOMMON_API bool JSON_SETTINGS::fromLegacy<double>( wxConfigBase*, const std::string&, const std::string& );
890
891template
892KICOMMON_API bool JSON_SETTINGS::fromLegacy<bool>( wxConfigBase*, const std::string&, const std::string& );
893
894
895bool JSON_SETTINGS::fromLegacyString( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest )
896{
897 wxString str;
898
899 if( aConfig->Read( aKey, &str ) )
900 {
901 try
902 {
903 ( *m_internals )[aDest] = str.ToUTF8();
904 }
905 catch( ... )
906 {
907 wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyString!" ) );
908 return false;
909 }
910
911 return true;
912 }
913
914 return false;
915}
916
917
918bool JSON_SETTINGS::fromLegacyColor( wxConfigBase* aConfig, const std::string& aKey, const std::string& aDest )
919{
920 wxString str;
921
922 if( aConfig->Read( aKey, &str ) )
923 {
924 KIGFX::COLOR4D color;
925 color.SetFromWxString( str );
926
927 try
928 {
929 nlohmann::json js = nlohmann::json::array( { color.r, color.g, color.b, color.a } );
930 ( *m_internals )[aDest] = std::move( js );
931 }
932 catch( ... )
933 {
934 wxASSERT_MSG( false, wxT( "Could not write value in fromLegacyColor!" ) );
935 return false;
936 }
937
938 return true;
939 }
940
941 return false;
942}
943
944
946{
947 wxLogTrace( traceSettings, wxT( "AddNestedSettings %s" ), aSettings->GetFilename() );
948 m_nested_settings.push_back( aSettings );
949}
950
951
953{
954 if( !aSettings || !m_manager )
955 return;
956
957 auto it = std::find_if( m_nested_settings.begin(), m_nested_settings.end(),
958 [&aSettings]( const JSON_SETTINGS* aPtr )
959 {
960 return aPtr == aSettings;
961 } );
962
963 if( it != m_nested_settings.end() )
964 {
965 wxLogTrace( traceSettings, wxT( "Flush and release %s" ), ( *it )->GetFilename() );
966
967 // Flush the nested state into the parent and propagate genuine dirtiness so a later parent
968 // save persists it; the nested object is gone by then, so this is the parent's only signal.
969 // Default-fill of params absent from an older file no longer reports modified, so releasing
970 // an unchanged nested setting on editor close does not falsely dirty the parent (#24402).
971 m_modified |= ( *it )->SaveToFile();
972 m_nested_settings.erase( it );
973 }
974
975 aSettings->SetParent( nullptr );
976}
977
978
979// Specializations to allow conversion between wxString and std::string via JSON_SETTINGS API
980template<>
981std::optional<wxString> JSON_SETTINGS::Get( const std::string& aPath ) const
982{
983 if( std::optional<nlohmann::json> opt_json = GetJson( aPath ) )
984 return wxString( opt_json->get<std::string>().c_str(), wxConvUTF8 );
985
986 return std::nullopt;
987}
988
989
990template<>
991void JSON_SETTINGS::Set<wxString>( const std::string& aPath, wxString aVal )
992{
993 ( *m_internals )[aPath] = aVal.ToUTF8();
994}
995
996
997template<typename ResultType>
998ResultType JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey,
999 ResultType aDefault )
1000{
1001 ResultType ret = std::move( aDefault );
1002
1003 try
1004 {
1005 if( aJson.contains( aKey ) )
1006 ret = aJson.at( aKey ).get<ResultType>();
1007 }
1008 catch( ... )
1009 {
1010 }
1011
1012 return ret;
1013}
1014
1015
1016template
1017KICOMMON_API std::string JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey,
1018 std::string aDefault );
1019
1020
1021template
1022KICOMMON_API bool JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey, bool aDefault );
static nlohmann::json::json_pointer PointerFromString(std::string aPath)
Builds a JSON pointer based on a given string.
bool fromLegacyString(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy wxConfig string value to a given JSON pointer value.
bool fromLegacy(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy wxConfig value to a given JSON pointer value.
virtual wxString getFileExt() const
const nlohmann::json & originalJson() const
virtual std::map< std::string, nlohmann::json > GetFileHistories()
bool flushToStore()
Flushes nested settings and parameters into the JSON store.
void Set(const std::string &aPath, ValueType aVal)
Stores a value into the JSON document Will throw an exception if ValueType isn't something that the l...
std::optional< nlohmann::json > GetJson(const std::string &aPath) const
Fetches a JSON object that is a subset of this JSON_SETTINGS object, using a path of the form "key1....
wxString m_filename
The filename (not including path) of this settings file (inicode)
SETTINGS_MANAGER * m_manager
A pointer to the settings manager managing this file (may be null)
bool Contains(const std::string &aPath) const
bool m_fileSynced
True once the store has been synchronized with an on-disk file (loaded or saved)
bool LoadFromRawFile(const wxString &aPath)
bool m_isFutureFormat
Set to true if this settings is loaded from a file with a newer schema version than is known.
virtual ~JSON_SETTINGS()
std::vector< NESTED_SETTINGS * > m_nested_settings
Nested settings files that live inside this one, if any.
bool m_modified
True if the JSON data store has been written to since the last file write.
virtual bool LoadFromFile(const wxString &aDirectory="")
Loads the backing file from disk and then calls Load()
bool m_createIfDefault
Whether or not the backing store file should be created if all parameters are still at their default ...
bool m_writeFile
Whether or not the backing store file should be written.
static bool SetIfPresent(const nlohmann::json &aObj, const std::string &aPath, wxString &aTarget)
Sets the given string if the given key/path is present.
virtual void Load()
Updates the parameters of this object based on the current JSON document contents.
void ResetToDefaults()
Resets all parameters to default values.
bool fromLegacyColor(wxConfigBase *aConfig, const std::string &aKey, const std::string &aDest)
Translates a legacy COLOR4D stored in a wxConfig string to a given JSON pointer value.
wxString GetFullFilename() const
bool m_createIfMissing
Whether or not the backing store file should be created it if doesn't exist.
bool Migrate()
Migrates the schema of this settings from the version in the file to the latest version.
std::optional< ValueType > Get(const std::string &aPath) const
Fetches a value from within the JSON document.
bool m_lastSaveFailed
True if the most recent SaveToFile() had changes to write and could not write them.
std::vector< PARAM_BASE * > m_params
The list of parameters (owned by this object)
virtual bool MigrateFromLegacy(wxConfigBase *aLegacyConfig)
Migrates from wxConfig to JSON-based configuration.
const std::string FormatAsString()
void registerMigration(int aOldSchemaVersion, int aNewSchemaVersion, std::function< bool(void)> aMigrator)
Registers a migration from one schema version to another.
nlohmann::json & At(const std::string &aPath)
Wrappers for the underlying JSON API so that most consumers don't need json.hpp All of these function...
JSON_SETTINGS_INTERNALS * Internals()
static ResultType fetchOrDefault(const nlohmann::json &aJson, const std::string &aKey, ResultType aDefault=ResultType())
Helper to retrieve a value from a JSON object (dictionary) as a certain result type.
void ReleaseNestedSettings(NESTED_SETTINGS *aSettings)
Saves and frees a nested settings object, if it exists within this one.
JSON_SETTINGS(const wxString &aFilename, SETTINGS_LOC aLocation, int aSchemaVersion)
bool m_deleteLegacyAfterMigration
Whether or not to delete legacy file after migration.
std::unique_ptr< JSON_SETTINGS_INTERNALS > m_internals
friend class NESTED_SETTINGS
SETTINGS_LOC m_location
The location of this settings file (.
virtual bool SaveToFile(const wxString &aDirectory="", bool aForce=false)
Calls Store() and then writes the contents of the JSON document to a file.
virtual wxString getLegacyFileExt() const
bool m_resetParamsIfMissing
Whether or not to set parameters to their default value if missing from JSON on Load()
std::string formatFileContents()
Overlays the current store on the original file contents.
std::map< int, std::pair< int, std::function< bool()> > > m_migrators
A map of starting schema version to a pair of <ending version, migrator function>
int m_schemaVersion
Version of this settings schema.
wxString m_legacy_filename
The filename of the wxConfig legacy file (if different from m_filename)
void AddNestedSettings(NESTED_SETTINGS *aSettings)
Transfers ownership of a given NESTED_SETTINGS to this object.
virtual bool Store()
Stores the current parameters into the JSON document represented by this object Note: this doesn't do...
wxString GetFilename() const
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
double r
Red component.
Definition color4d.h:391
double g
Green component.
Definition color4d.h:392
bool SetFromWxString(const wxString &aColorString)
Set color values by parsing a string using wxColour::Set().
Definition color4d.cpp:127
double a
Alpha component.
Definition color4d.h:394
double b
Blue component.
Definition color4d.h:393
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
void SetParent(JSON_SETTINGS *aParent, bool aLoadFromFile=true)
#define ALLOW_EXCEPTIONS
#define IGNORE_COMMENTS
SETTINGS_LOC
#define traceSettings
#define KICOMMON_API
Definition kicommon.h:27
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
std::vector< FAB_LAYER_COLOR > dummy
Common grid settings, available to every frame.
std::string path