kicad-source/common/project/project_local_settings.cpp
Seth Hillbrand e379e91081 Store hierarchy expansion/collapse state
Defaults to fully expanded but stores the names for nodes that are
collapsed in project local settings.  Because project local settings are
generally changed by modifying views or selection filters, these should
be saved on close rather than only when the project is saved

Fixes https://gitlab.com/kicad/code/kicad/-/issues/19276
2025-08-04 08:34:09 -07:00

593 lines
24 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2020 CERN
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* @author Jon Evans <jon@craftyjon.com>
*
* 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 <lset.h>
#include <project.h>
#include <project/project_local_settings.h>
#include <settings/layer_settings_utils.h>
#include <settings/json_settings_internals.h>
#include <settings/parameters.h>
const int projectLocalSettingsVersion = 5;
PROJECT_LOCAL_SETTINGS::PROJECT_LOCAL_SETTINGS( PROJECT* aProject, const wxString& aFilename ) :
JSON_SETTINGS( aFilename, SETTINGS_LOC::PROJECT, projectLocalSettingsVersion,
/* aCreateIfMissing = */ true, /* aCreateIfDefault = */ false,
/* aWriteFile = */ true ),
// clang-format off: suggestion is less readable.
m_ActiveLayer( UNDEFINED_LAYER ),
m_ContrastModeDisplay( HIGH_CONTRAST_MODE::NORMAL ),
m_NetColorMode( NET_COLOR_MODE::RATSNEST ),
m_AutoTrackWidth( true ),
m_ZoneDisplayMode( ZONE_DISPLAY_MODE::SHOW_FILLED ),
m_PrototypeZoneFill( false ),
m_TrackOpacity( 1.0 ),
m_ViaOpacity( 1.0 ),
m_PadOpacity( 1.0 ),
m_ZoneOpacity( 0.6 ),
m_ShapeOpacity( 1.0 ),
m_ImageOpacity( 0.6 ),
m_PcbSelectionFilter(),
m_project( aProject ),
m_wasMigrated( false )
// clang-format on: suggestion is less readable.
{
// Keep old files around
m_deleteLegacyAfterMigration = false;
m_params.emplace_back( new PARAM_LAMBDA<std::string>( "board.visible_layers",
[&]() -> std::string
{
return m_VisibleLayers.FmtHex();
},
[&]( const std::string& aString )
{
m_VisibleLayers.ParseHex( aString );
},
LSET::AllLayersMask().FmtHex() ) );
m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "board.visible_items",
[&]() -> nlohmann::json
{
nlohmann::json ret = nlohmann::json::array();
for( GAL_LAYER_ID l : m_VisibleItems.Seq() )
{
if( std::optional<VISIBILITY_LAYER> vl = VisibilityLayerFromRenderLayer( l ) )
ret.push_back( VisibilityLayerToString( *vl ) );
}
// Explicit marker to tell apart a wiped-out array from the user hiding everything
if( ret.empty() )
ret.push_back( "none" );
return ret;
},
[&]( const nlohmann::json& aVal )
{
if( !aVal.is_array() || aVal.empty() )
{
m_VisibleItems |= UserVisbilityLayers();
return;
}
m_VisibleItems &= ~UserVisbilityLayers();
GAL_SET visible;
bool none = false;
for( const nlohmann::json& entry : aVal )
{
try
{
std::string vs = entry.get<std::string>();
if( std::optional<GAL_LAYER_ID> l = RenderLayerFromVisbilityString( vs ) )
visible.set( *l );
else if( vs == "none" )
none = true;
}
catch( ... )
{
// Unknown entry (possibly the settings file was re-saved by an old version
// of kicad that used numeric entries, or is a future format)
}
}
// Restore corrupted state
if( !visible.any() && !none )
m_VisibleItems |= UserVisbilityLayers();
else
m_VisibleItems |= UserVisbilityLayers() & visible;
},
{} ) );
m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "board.selection_filter",
[&]() -> nlohmann::json
{
nlohmann::json ret;
ret["lockedItems"] = m_PcbSelectionFilter.lockedItems;
ret["footprints"] = m_PcbSelectionFilter.footprints;
ret["text"] = m_PcbSelectionFilter.text;
ret["tracks"] = m_PcbSelectionFilter.tracks;
ret["vias"] = m_PcbSelectionFilter.vias;
ret["pads"] = m_PcbSelectionFilter.pads;
ret["graphics"] = m_PcbSelectionFilter.graphics;
ret["zones"] = m_PcbSelectionFilter.zones;
ret["keepouts"] = m_PcbSelectionFilter.keepouts;
ret["dimensions"] = m_PcbSelectionFilter.dimensions;
ret["otherItems"] = m_PcbSelectionFilter.otherItems;
return ret;
},
[&]( const nlohmann::json& aVal )
{
if( aVal.empty() || !aVal.is_object() )
return;
SetIfPresent( aVal, "lockedItems", m_PcbSelectionFilter.lockedItems );
SetIfPresent( aVal, "footprints", m_PcbSelectionFilter.footprints );
SetIfPresent( aVal, "text", m_PcbSelectionFilter.text );
SetIfPresent( aVal, "tracks", m_PcbSelectionFilter.tracks );
SetIfPresent( aVal, "vias", m_PcbSelectionFilter.vias );
SetIfPresent( aVal, "pads", m_PcbSelectionFilter.pads );
SetIfPresent( aVal, "graphics", m_PcbSelectionFilter.graphics );
SetIfPresent( aVal, "zones", m_PcbSelectionFilter.zones );
SetIfPresent( aVal, "keepouts", m_PcbSelectionFilter.keepouts );
SetIfPresent( aVal, "dimensions", m_PcbSelectionFilter.dimensions );
SetIfPresent( aVal, "otherItems", m_PcbSelectionFilter.otherItems );
},
{
{ "lockedItems", false },
{ "footprints", true },
{ "text", true },
{ "tracks", true },
{ "vias", true },
{ "pads", true },
{ "graphics", true },
{ "zones", true },
{ "keepouts", true },
{ "dimensions", true },
{ "otherItems", true }
} ) );
m_params.emplace_back( new PARAM_ENUM<PCB_LAYER_ID>( "board.active_layer",
&m_ActiveLayer, F_Cu, PCBNEW_LAYER_ID_START, F_Fab ) );
m_params.emplace_back( new PARAM<wxString>( "board.active_layer_preset",
&m_ActiveLayerPreset, "" ) );
m_params.emplace_back( new PARAM_ENUM<HIGH_CONTRAST_MODE>( "board.high_contrast_mode",
&m_ContrastModeDisplay, HIGH_CONTRAST_MODE::NORMAL,
HIGH_CONTRAST_MODE::NORMAL, HIGH_CONTRAST_MODE::HIDDEN ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.tracks", &m_TrackOpacity, 1.0 ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.vias", &m_ViaOpacity, 1.0 ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.pads", &m_PadOpacity, 1.0 ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.zones", &m_ZoneOpacity, 0.6 ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.images", &m_ImageOpacity, 0.6 ) );
m_params.emplace_back( new PARAM<double>( "board.opacity.shapes", &m_ShapeOpacity, 1.0 ) );
m_params.emplace_back( new PARAM_LIST<wxString>( "board.hidden_nets", &m_HiddenNets, {} ) );
m_params.emplace_back( new PARAM_SET<wxString>( "board.hidden_netclasses",
&m_HiddenNetclasses, {} ) );
m_params.emplace_back( new PARAM_ENUM<NET_COLOR_MODE>( "board.net_color_mode",
&m_NetColorMode, NET_COLOR_MODE::RATSNEST, NET_COLOR_MODE::OFF,
NET_COLOR_MODE::ALL ) );
m_params.emplace_back( new PARAM<bool>( "board.auto_track_width",
&m_AutoTrackWidth, true ) );
m_params.emplace_back( new PARAM_ENUM<ZONE_DISPLAY_MODE>( "board.zone_display_mode",
&m_ZoneDisplayMode,
ZONE_DISPLAY_MODE::SHOW_FILLED, ZONE_DISPLAY_MODE::SHOW_FILLED,
ZONE_DISPLAY_MODE::SHOW_TRIANGULATION ) );
m_params.emplace_back(
new PARAM<bool>( "board.prototype_zone_fills", &m_PrototypeZoneFill, false ) );
m_params.emplace_back( new PARAM<wxString>( "git.repo_username", &m_GitRepoUsername, "" ) );
m_params.emplace_back( new PARAM<wxString>( "git.repo_type", &m_GitRepoType, "" ) );
m_params.emplace_back( new PARAM<wxString>( "git.ssh_key", &m_GitSSHKey, "" ) );
m_params.emplace_back( new PARAM<wxString>( "net_inspector_panel.filter_text",
&m_NetInspectorPanel.filter_text, "" ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.filter_by_net_name",
&m_NetInspectorPanel.filter_by_net_name, true ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.filter_by_netclass",
&m_NetInspectorPanel.filter_by_netclass, true ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.group_by_netclass",
&m_NetInspectorPanel.group_by_netclass, false ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.group_by_constraint",
&m_NetInspectorPanel.group_by_constraint, false ) );
m_params.emplace_back( new PARAM_LIST<wxString>( "net_inspector_panel.custom_group_rules",
&m_NetInspectorPanel.custom_group_rules,
{} ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.show_zero_pad_nets",
&m_NetInspectorPanel.show_zero_pad_nets, false ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.show_unconnected_nets",
&m_NetInspectorPanel.show_unconnected_nets, false ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.show_time_domain_details",
&m_NetInspectorPanel.show_time_domain_details, false ) );
m_params.emplace_back( new PARAM<int>( "net_inspector_panel.sorting_column",
&m_NetInspectorPanel.sorting_column, -1 ) );
m_params.emplace_back( new PARAM<bool>( "net_inspector_panel.sort_ascending",
&m_NetInspectorPanel.sort_order_asc, true ) );
m_params.emplace_back( new PARAM_LIST<int>( "net_inspector_panel.col_order",
&m_NetInspectorPanel.col_order, {} ) );
m_params.emplace_back( new PARAM_LIST<int>( "net_inspector_panel.col_widths",
&m_NetInspectorPanel.col_widths, {} ) );
m_params.emplace_back( new PARAM_LIST<bool>( "net_inspector_panel.col_hidden",
&m_NetInspectorPanel.col_hidden, {} ) );
m_params.emplace_back( new PARAM_LIST<wxString>( "net_inspector_panel.expanded_rows",
&m_NetInspectorPanel.expanded_rows, {} ) );
m_params.emplace_back( new PARAM_LIST<wxString>( "open_jobsets", &m_OpenJobSets, {} ) );
m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "project.files",
[&]() -> nlohmann::json
{
nlohmann::json ret = nlohmann::json::array();
for( PROJECT_FILE_STATE& fileState : m_files )
{
nlohmann::json file;
file["name"] = fileState.fileName;
file["open"] = fileState.open;
nlohmann::json window;
window["maximized"] = fileState.window.maximized;
window["size_x"] = fileState.window.size_x;
window["size_y"] = fileState.window.size_y;
window["pos_x"] = fileState.window.pos_x;
window["pos_y"] = fileState.window.pos_y;
window["display"] = fileState.window.display;
file["window"] = window;
ret.push_back( file );
}
return ret;
},
[&]( const nlohmann::json& aVal )
{
if( !aVal.is_array() || aVal.empty() )
return;
m_files.clear();
for( const nlohmann::json& file : aVal )
{
PROJECT_FILE_STATE fileState;
try
{
SetIfPresent( file, "name", fileState.fileName );
SetIfPresent( file, "open", fileState.open );
SetIfPresent( file, "window.size_x", fileState.window.size_x );
SetIfPresent( file, "window.size_y", fileState.window.size_y );
SetIfPresent( file, "window.pos_x", fileState.window.pos_x );
SetIfPresent( file, "window.pos_y", fileState.window.pos_y );
SetIfPresent( file, "window.maximized", fileState.window.maximized );
SetIfPresent( file, "window.display", fileState.window.display );
m_files.push_back( fileState );
}
catch( ... )
{
// Non-integer or out of range entry in the array; ignore
}
}
},
{
} ) );
m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "schematic.selection_filter",
[&]() -> nlohmann::json
{
nlohmann::json ret;
ret["lockedItems"] = m_SchSelectionFilter.lockedItems;
ret["symbols"] = m_SchSelectionFilter.symbols;
ret["text"] = m_SchSelectionFilter.text;
ret["wires"] = m_SchSelectionFilter.wires;
ret["labels"] = m_SchSelectionFilter.labels;
ret["pins"] = m_SchSelectionFilter.pins;
ret["graphics"] = m_SchSelectionFilter.graphics;
ret["images"] = m_SchSelectionFilter.images;
ret["ruleAreas"] = m_SchSelectionFilter.ruleAreas;
ret["otherItems"] = m_SchSelectionFilter.otherItems;
return ret;
},
[&]( const nlohmann::json& aVal )
{
if( aVal.empty() || !aVal.is_object() )
return;
SetIfPresent( aVal, "lockedItems", m_SchSelectionFilter.lockedItems );
SetIfPresent( aVal, "symbols", m_SchSelectionFilter.symbols );
SetIfPresent( aVal, "text", m_SchSelectionFilter.text );
SetIfPresent( aVal, "wires", m_SchSelectionFilter.wires );
SetIfPresent( aVal, "labels", m_SchSelectionFilter.labels );
SetIfPresent( aVal, "pins", m_SchSelectionFilter.pins );
SetIfPresent( aVal, "graphics", m_SchSelectionFilter.graphics );
SetIfPresent( aVal, "images", m_SchSelectionFilter.images );
SetIfPresent( aVal, "ruleAreas", m_SchSelectionFilter.ruleAreas );
SetIfPresent( aVal, "otherItems", m_SchSelectionFilter.otherItems );
},
{
{ "lockedItems", false },
{ "symbols", true },
{ "text", true },
{ "wires", true },
{ "labels", true },
{ "pins", true },
{ "graphics", true },
{ "images", true },
{ "ruleAreas", true },
{ "otherItems", true }
} ) );
m_params.emplace_back( new PARAM_LIST<wxString>( "schematic.hierarchy_collapsed",
&m_SchHierarchyCollapsed, {} ) );
registerMigration( 1, 2,
[&]()
{
/**
* Schema version 1 to 2:
* LAYER_PADS and LAYER_ZONES added to visibility controls
*/
std::string ptr( "board.visible_items" );
if( Contains( ptr ) )
{
if( At( ptr ).is_array() )
{
At( ptr ).push_back( LAYER_PADS - GAL_LAYER_ID_START );
At( ptr ).push_back( LAYER_ZONES - GAL_LAYER_ID_START );
}
else
{
At( "board" ).erase( "visible_items" );
}
m_wasMigrated = true;
}
return true;
} );
registerMigration( 2, 3,
[&]()
{
/**
* Schema version 2 to 3:
* Fix issue with object visibility not migrating from legacy, which required
* remapping of GAL_LAYER_ID to match the legacy bitmask ordering.
*/
/// Stores a mapping from old to new enum offset
const std::map<int, int> offsets = {
{ 22, 34 }, // LAYER_PAD_HOLEWALLS
{ 23, 22 }, // LAYER_VIA_HOLES
{ 24, 35 }, // LAYER_VIA_HOLEWALLS
{ 25, 23 }, // LAYER_DRC_ERROR
{ 26, 36 }, // LAYER_DRC_WARNING
{ 27, 37 }, // LAYER_DRC_EXCLUSION
{ 28, 38 }, // LAYER_MARKER_SHADOWS
{ 29, 24 }, // LAYER_DRAWINGSHEET
{ 30, 25 }, // LAYER_GP_OVERLAY
{ 31, 26 }, // LAYER_SELECT_OVERLAY
{ 32, 27 }, // LAYER_PCB_BACKGROUND
{ 33, 28 }, // LAYER_CURSOR
{ 34, 29 }, // LAYER_AUX_ITEM
{ 35, 30 }, // LAYER_DRAW_BITMAPS
{ 39, 32 }, // LAYER_PADS
{ 40, 33 }, // LAYER_ZONES
};
std::string ptr( "board.visible_items" );
if( Contains( ptr ) && At( ptr ).is_array() )
{
nlohmann::json visible = nlohmann::json::array();
for( const nlohmann::json& val : At( ptr ) )
{
try
{
int layer = val.get<int>();
if( offsets.count( layer ) )
visible.push_back( offsets.at( layer ) );
else
visible.push_back( layer );
}
catch( ... )
{
// skip invalid value
}
}
At( "board" )["visible_items"] = visible;
m_wasMigrated = true;
}
return true;
} );
registerMigration( 3, 4,
[&]()
{
// Schema version 3 to 4: LAYER_FILLED_SHAPES added to visibility controls
std::string ptr( "board.visible_items" );
if( Contains( ptr ) )
{
if( At( ptr ).is_array() && !At( ptr ).empty() )
At( ptr ).push_back( LAYER_FILLED_SHAPES - GAL_LAYER_ID_START );
else
At( "board" ).erase( "visible_items" );
m_wasMigrated = true;
}
return true;
} );
registerMigration( 4, 5,
[&]()
{
// Schema version 5: use named render layers
std::string ptr( "board.visible_items" );
if( Contains( ptr ) && At( ptr ).is_array() )
{
std::vector<std::string> newLayers;
for( nlohmann::json& entry : At( ptr ) )
{
if( !entry.is_number_integer() )
continue;
if( std::optional<VISIBILITY_LAYER> vl =
VisibilityLayerFromRenderLayer( GAL_LAYER_ID_START + entry.get<int>() ) )
{
newLayers.emplace_back( VisibilityLayerToString( *vl ) );
}
}
At( ptr ) = newLayers;
m_wasMigrated = true;
}
return true;
} );
}
bool PROJECT_LOCAL_SETTINGS::MigrateFromLegacy( wxConfigBase* aLegacyConfig )
{
/**
* The normal legacy migration code won't be used for this because the only legacy
* information stored here was stored in board files, so we do that migration when loading
* the board.
*/
return true;
}
bool PROJECT_LOCAL_SETTINGS::SaveToFile( const wxString& aDirectory, bool aForce )
{
wxASSERT( m_project );
Set( "meta.filename",
m_project->GetProjectName() + "." + FILEEXT::ProjectLocalSettingsFileExtension );
// Even if parameters were not modified, we should resave after migration
bool force = aForce || m_wasMigrated;
// If we're actually going ahead and doing the save, the flag that keeps code from doing the
// save should be cleared at this point.
m_wasMigrated = false;
return JSON_SETTINGS::SaveToFile( aDirectory, force );
}
bool PROJECT_LOCAL_SETTINGS::SaveAs( const wxString& aDirectory, const wxString& aFile )
{
Set( "meta.filename", aFile + "." + FILEEXT::ProjectLocalSettingsFileExtension );
SetFilename( aFile );
// If we're actually going ahead and doing the save, the flag that keeps code from doing the
// save should be cleared at this point.
m_wasMigrated = false;
return JSON_SETTINGS::SaveToFile( aDirectory, true );
}
const PROJECT_FILE_STATE* PROJECT_LOCAL_SETTINGS::GetFileState( const wxString& aFileName )
{
auto it = std::find_if( m_files.begin(), m_files.end(),
[&aFileName]( const PROJECT_FILE_STATE &a )
{
return a.fileName == aFileName;
} );
if( it != m_files.end() )
{
return &( *it );
}
return nullptr;
}
void PROJECT_LOCAL_SETTINGS::SaveFileState( const wxString& aFileName,
const WINDOW_SETTINGS* aWindowCfg, bool aOpen )
{
auto it = std::find_if( m_files.begin(), m_files.end(),
[&aFileName]( const PROJECT_FILE_STATE& a )
{
return a.fileName == aFileName;
} );
if( it == m_files.end() )
{
PROJECT_FILE_STATE fileState;
fileState.fileName = aFileName;
fileState.open = false;
fileState.window.maximized = false;
fileState.window.size_x = -1;
fileState.window.size_y = -1;
fileState.window.pos_x = -1;
fileState.window.pos_y = -1;
fileState.window.display = 0;
m_files.push_back( fileState );
it = m_files.end() - 1;
}
( *it ).window = aWindowCfg->state;
( *it ).open = aOpen;
}
void PROJECT_LOCAL_SETTINGS::ClearFileState()
{
m_files.clear();
}