/* * This program source code file is part of KICAD, a free EDA CAD application. * * Copyright (C) 2020-2023 CERN * Copyright (C) 2020-2022 KiCad Developers, see AUTHORS.txt for contributors. * @author Maciej Suminski * * 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 . */ #include "properties_panel.h" #include #include #include #include #include #include #include #include #include #include #include // 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( 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 types; for( EDA_ITEM* item : aSelection ) types.insert( TYPE_HASH( *item ) ); wxCHECK( !types.empty(), /* void */ ); PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance(); std::set 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 groupDisplayOrder = propMgr.GetGroupDisplayOrder( *types.begin() ); std::set groups( groupDisplayOrder.begin(), groupDisplayOrder.end() ); std::set 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& 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 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 pgPropOrders; std::map> 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& 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( &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(); } void PROPERTIES_PANEL::onCharHook( wxKeyEvent& aEvent ) { if( aEvent.GetKeyCode() == WXK_TAB && !aEvent.ShiftDown() ) { m_grid->CommitChangesFromEditor(); 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(); }