kicad-source/common/widgets/properties_panel.cpp
Seth Hillbrand 0b2d4d4879 Revise Copyright statement to align with TLF
Recommendation is to avoid using the year nomenclature as this
information is already encoded in the git repo.  Avoids needing to
repeatly update.

Also updates AUTHORS.txt from current repo with contributor names
2025-01-01 14:12:04 -08:00

479 lines
15 KiB
C++

/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2020-2023 CERN
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* @author Maciej Suminski <maciej.suminski@cern.ch>
*
* 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 "properties_panel.h"
#include <tool/selection.h>
#include <eda_base_frame.h>
#include <eda_item.h>
#include <import_export.h>
#include <pgm_base.h>
#include <properties/pg_cell_renderer.h>
#include <algorithm>
#include <set>
#include <wx/settings.h>
#include <wx/stattext.h>
#include <wx/propgrid/advprops.h>
// This is provided by wx >3.3.0
#if !wxCHECK_VERSION( 3, 3, 0 )
extern APIIMPORT wxPGGlobalVarsClass* wxPGGlobalVars;
#endif
PROPERTIES_PANEL::PROPERTIES_PANEL( wxWindow* aParent, EDA_BASE_FRAME* aFrame ) :
wxPanel( aParent ),
m_frame( aFrame ),
m_splitter_key_proportion( -1 )
{
wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
// on some platforms wxPGGlobalVars is initialized automatically,
// but others need an explicit init
if( !wxPGGlobalVars )
wxPGInitResourceModule();
// See https://gitlab.com/kicad/code/kicad/-/issues/12297
// and https://github.com/wxWidgets/wxWidgets/issues/11787
if( wxPGGlobalVars->m_mapEditorClasses.empty() )
{
wxPGEditor_TextCtrl = nullptr;
wxPGEditor_Choice = nullptr;
wxPGEditor_ComboBox = nullptr;
wxPGEditor_TextCtrlAndButton = nullptr;
wxPGEditor_CheckBox = nullptr;
wxPGEditor_ChoiceAndButton = nullptr;
wxPGEditor_SpinCtrl = nullptr;
wxPGEditor_DatePickerCtrl = nullptr;
}
if( !Pgm().m_PropertyGridInitialized )
{
delete wxPGGlobalVars->m_defaultRenderer;
wxPGGlobalVars->m_defaultRenderer = new PG_CELL_RENDERER();
Pgm().m_PropertyGridInitialized = true;
}
m_caption = new wxStaticText( this, wxID_ANY, _( "No objects selected" ) );
mainSizer->Add( m_caption, 0, wxALL | wxEXPAND, 5 );
m_grid = new wxPropertyGrid( this );
m_grid->SetUnspecifiedValueAppearance( wxPGCell( wxT( "<...>" ) ) );
m_grid->SetExtraStyle( wxPG_EX_HELP_AS_TOOLTIPS );
#if wxCHECK_VERSION( 3, 3, 0 )
m_grid->SetValidationFailureBehavior( wxPGVFBFlags::MarkCell );
#else
m_grid->SetValidationFailureBehavior( wxPG_VFB_MARK_CELL );
#endif
#if wxCHECK_VERSION( 3, 3, 0 )
m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_RETURN );
m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_NUMPAD_ENTER );
m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_DOWN );
m_grid->AddActionTrigger( wxPGKeyboardAction::PrevProperty, WXK_UP );
m_grid->AddActionTrigger( wxPGKeyboardAction::Edit, WXK_SPACE );
#else
m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_RETURN );
m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_NUMPAD_ENTER );
m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_DOWN );
m_grid->AddActionTrigger( wxPG_ACTION_PREV_PROPERTY, WXK_UP );
m_grid->AddActionTrigger( wxPG_ACTION_EDIT, WXK_SPACE );
#endif
m_grid->DedicateKey( WXK_RETURN );
m_grid->DedicateKey( WXK_NUMPAD_ENTER );
m_grid->DedicateKey( WXK_DOWN );
m_grid->DedicateKey( WXK_UP );
mainSizer->Add( m_grid, 1, wxEXPAND, 5 );
m_grid->SetCellDisabledTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
#ifdef __WXGTK__
// Needed for dark mode, on wx 3.0 at least.
m_grid->SetCaptionTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_CAPTIONTEXT ) );
#endif
SetFont( KIUI::GetDockedPaneFont( this ) );
SetSizer( mainSizer );
Layout();
m_grid->CenterSplitter();
Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( PROPERTIES_PANEL::onCharHook ), nullptr, this );
Connect( wxEVT_PG_CHANGED, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanged ), nullptr, this );
Connect( wxEVT_PG_CHANGING, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanging ), nullptr, this );
Connect( wxEVT_SHOW, wxShowEventHandler( PROPERTIES_PANEL::onShow ), nullptr, this );
Bind( wxEVT_PG_COL_END_DRAG,
[&]( wxPropertyGridEvent& )
{
m_splitter_key_proportion =
static_cast<float>( m_grid->GetSplitterPosition() ) / m_grid->GetSize().x;
} );
Bind( wxEVT_SIZE,
[&]( wxSizeEvent& aEvent )
{
CallAfter( [this]()
{
RecalculateSplitterPos();
} );
aEvent.Skip();
} );
m_frame->Bind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
}
PROPERTIES_PANEL::~PROPERTIES_PANEL()
{
m_frame->Unbind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
}
void PROPERTIES_PANEL::OnLanguageChanged( wxCommandEvent& aEvent )
{
if( m_grid->IsEditorFocused() )
m_grid->CommitChangesFromEditor();
m_grid->Clear();
m_displayed.clear();
UpdateData();
aEvent.Skip();
}
void PROPERTIES_PANEL::rebuildProperties( const SELECTION& aSelection )
{
auto reset =
[&]()
{
if( m_grid->IsEditorFocused() )
m_grid->CommitChangesFromEditor();
m_grid->Clear();
m_displayed.clear();
};
if( aSelection.Empty() )
{
m_caption->SetLabel( _( "No objects selected" ) );
reset();
return;
}
else if( aSelection.Size() == 1 )
{
m_caption->SetLabel( aSelection.Front()->GetFriendlyName() );
}
else
{
m_caption->SetLabel( wxString::Format( _( "%d objects selected" ), aSelection.Size() ) );
}
// Get all the selected types
std::set<TYPE_ID> types;
for( EDA_ITEM* item : aSelection )
types.insert( TYPE_HASH( *item ) );
wxCHECK( !types.empty(), /* void */ ); // already guarded above, but Coverity doesn't know that
PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
std::set<PROPERTY_BASE*> commonProps;
const PROPERTY_LIST& allProperties = propMgr.GetProperties( *types.begin() );
copy( allProperties.begin(), allProperties.end(), inserter( commonProps, commonProps.begin() ) );
PROPERTY_DISPLAY_ORDER displayOrder = propMgr.GetDisplayOrder( *types.begin() );
std::vector<wxString> groupDisplayOrder = propMgr.GetGroupDisplayOrder( *types.begin() );
std::set<wxString> groups( groupDisplayOrder.begin(), groupDisplayOrder.end() );
std::set<PROPERTY_BASE*> availableProps;
// Get all possible properties
for( const TYPE_ID& type : types )
{
const PROPERTY_LIST& itemProps = propMgr.GetProperties( type );
const PROPERTY_DISPLAY_ORDER& itemDisplayOrder = propMgr.GetDisplayOrder( type );
copy( itemDisplayOrder.begin(), itemDisplayOrder.end(),
inserter( displayOrder, displayOrder.begin() ) );
const std::vector<wxString>& itemGroups = propMgr.GetGroupDisplayOrder( type );
for( const wxString& group : itemGroups )
{
if( !groups.count( group ) )
{
groupDisplayOrder.emplace_back( group );
groups.insert( group );
}
}
for( auto it = commonProps.begin(); it != commonProps.end(); /* ++it in the loop */ )
{
if( !binary_search( itemProps.begin(), itemProps.end(), *it ) )
it = commonProps.erase( it );
else
++it;
}
}
EDA_ITEM* firstItem = aSelection.Front();
bool isLibraryEditor = m_frame->IsType( FRAME_FOOTPRINT_EDITOR )
|| m_frame->IsType( FRAME_SCH_SYMBOL_EDITOR );
bool isDesignEditor = m_frame->IsType( FRAME_PCB_EDITOR )
|| m_frame->IsType( FRAME_SCH );
// Find a set of properties that is common to all selected items
for( PROPERTY_BASE* property : commonProps )
{
if( property->IsHiddenFromPropertiesManager() )
continue;
if( isLibraryEditor && property->IsHiddenFromLibraryEditors() )
continue;
if( isDesignEditor && property->IsHiddenFromDesignEditors() )
continue;
if( propMgr.IsAvailableFor( TYPE_HASH( *firstItem ), property, firstItem ) )
availableProps.insert( property );
}
bool writeable = true;
std::set<PROPERTY_BASE*> existingProps;
for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
{
wxPGProperty* pgProp = it.GetProperty();
PROPERTY_BASE* property = propMgr.GetProperty( TYPE_HASH( *firstItem ), pgProp->GetName() );
// Switching item types? Property may no longer be valid
if( !property )
continue;
wxVariant commonVal;
extractValueAndWritability( aSelection, property, commonVal, writeable );
pgProp->SetValue( commonVal );
pgProp->Enable( writeable );
existingProps.insert( property );
}
if( !existingProps.empty() && existingProps == availableProps )
return;
// Some difference exists: start from scratch
reset();
std::map<wxPGProperty*, int> pgPropOrders;
std::map<wxString, std::vector<wxPGProperty*>> pgPropGroups;
for( PROPERTY_BASE* property : availableProps )
{
wxPGProperty* pgProp = createPGProperty( property );
wxVariant commonVal;
if( !extractValueAndWritability( aSelection, property, commonVal, writeable ) )
continue;
if( pgProp )
{
pgProp->SetValue( commonVal );
pgProp->Enable( writeable );
m_displayed.push_back( property );
wxASSERT( displayOrder.count( property ) );
pgPropOrders[pgProp] = displayOrder[property];
pgPropGroups[property->Group()].emplace_back( pgProp );
}
}
const wxString unspecifiedGroupCaption = _( "Basic Properties" );
for( const wxString& groupName : groupDisplayOrder )
{
if( !pgPropGroups.count( groupName ) )
continue;
std::vector<wxPGProperty*>& properties = pgPropGroups[groupName];
wxString groupCaption = wxGetTranslation( groupName );
auto groupItem = new wxPropertyCategory( groupName.IsEmpty() ? unspecifiedGroupCaption
: groupCaption );
m_grid->Append( groupItem );
std::sort( properties.begin(), properties.end(),
[&]( wxPGProperty*& aFirst, wxPGProperty*& aSecond )
{
return pgPropOrders[aFirst] < pgPropOrders[aSecond];
} );
for( wxPGProperty* property : properties )
m_grid->Append( property );
}
RecalculateSplitterPos();
}
bool PROPERTIES_PANEL::getItemValue( EDA_ITEM* aItem, PROPERTY_BASE* aProperty, wxVariant& aValue )
{
const wxAny& any = aItem->Get( aProperty );
bool converted = false;
if( aProperty->HasChoices() )
{
// handle enums as ints, since there are no default conversion functions for wxAny
int tmp;
converted = any.GetAs<int>( &tmp );
if( converted )
aValue = wxVariant( tmp );
}
if( !converted ) // all other types
converted = any.GetAs( &aValue );
if( !converted )
wxFAIL_MSG( wxS( "Could not convert wxAny to wxVariant" ) );
return converted;
}
bool PROPERTIES_PANEL::extractValueAndWritability( const SELECTION& aSelection,
PROPERTY_BASE* aProperty,
wxVariant& aValue, bool& aWritable )
{
PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
bool different = false;
wxVariant commonVal;
aWritable = true;
for( EDA_ITEM* item : aSelection )
{
if( !propMgr.IsAvailableFor( TYPE_HASH( *item ), aProperty, item ) )
return false;
if( aProperty->IsHiddenFromPropertiesManager() )
return false;
// If read-only for any of the selection, read-only for the whole selection.
if( !propMgr.IsWriteableFor( TYPE_HASH( *item ), aProperty, item ) )
aWritable = false;
wxVariant value;
if( getItemValue( item, aProperty, value ) )
{
// Null value indicates different property values between items
if( !different && !aValue.IsNull() && value != aValue )
{
different = true;
aValue.MakeNull();
}
else if( !different )
{
aValue = value;
}
}
else
{
// getItemValue returned false -- not available for this item
return false;
}
}
return true;
}
void PROPERTIES_PANEL::onShow( wxShowEvent& aEvent )
{
if( aEvent.IsShown() )
UpdateData();
aEvent.Skip();
}
void PROPERTIES_PANEL::onCharHook( wxKeyEvent& aEvent )
{
if( aEvent.GetKeyCode() == WXK_TAB && !aEvent.ShiftDown() && m_grid->IsAnyModified() )
{
m_grid->CommitChangesFromEditor();
// Pass the tab key on so the default property grid tab behavior is honored.
aEvent.Skip();
return;
}
if( aEvent.GetKeyCode() == WXK_SPACE )
{
if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
{
if( prop->GetValueType() == wxT( "bool" ) )
{
m_grid->SetPropertyValue( prop, !prop->GetValue().GetBool() );
return;
}
}
}
if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
{
m_grid->CommitChangesFromEditor();
/* don't skip this one; if we're not the last property we'll also go to the next row */
}
aEvent.Skip();
}
void PROPERTIES_PANEL::RecalculateSplitterPos()
{
if( m_splitter_key_proportion < 0 )
m_grid->CenterSplitter();
else
m_grid->SetSplitterPosition( m_splitter_key_proportion * m_grid->GetSize().x );
}
void PROPERTIES_PANEL::SetSplitterProportion( float aProportion )
{
m_splitter_key_proportion = aProportion;
RecalculateSplitterPos();
}