You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

442 lines
14 KiB

3 years ago
  1. /*
  2. * This program source code file is part of KICAD, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2020 CERN
  5. * Copyright (C) 2020-2022 KiCad Developers, see AUTHORS.txt for contributors.
  6. * @author Maciej Suminski <maciej.suminski@cern.ch>
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 3
  11. * of the License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. #include "properties_panel.h"
  22. #include <tool/selection.h>
  23. #include <eda_base_frame.h>
  24. #include <eda_item.h>
  25. #include <import_export.h>
  26. #include <properties/pg_cell_renderer.h>
  27. #include <algorithm>
  28. #include <set>
  29. #include <wx/settings.h>
  30. #include <wx/stattext.h>
  31. #include <wx/propgrid/advprops.h>
  32. // This is provided by wx >3.3.0
  33. #if !wxCHECK_VERSION( 3, 3, 0 )
  34. extern APIIMPORT wxPGGlobalVarsClass* wxPGGlobalVars;
  35. #endif
  36. PROPERTIES_PANEL::PROPERTIES_PANEL( wxWindow* aParent, EDA_BASE_FRAME* aFrame ) :
  37. wxPanel( aParent ),
  38. m_frame( aFrame ),
  39. m_splitter_key_proportion( -1 )
  40. {
  41. wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
  42. // on some platforms wxPGGlobalVars is initialized automatically,
  43. // but others need an explicit init
  44. if( !wxPGGlobalVars )
  45. wxPGInitResourceModule();
  46. // See https://gitlab.com/kicad/code/kicad/-/issues/12297
  47. // and https://github.com/wxWidgets/wxWidgets/issues/11787
  48. if( wxPGGlobalVars->m_mapEditorClasses.empty() )
  49. {
  50. wxPGEditor_TextCtrl = nullptr;
  51. wxPGEditor_Choice = nullptr;
  52. wxPGEditor_ComboBox = nullptr;
  53. wxPGEditor_TextCtrlAndButton = nullptr;
  54. wxPGEditor_CheckBox = nullptr;
  55. wxPGEditor_ChoiceAndButton = nullptr;
  56. wxPGEditor_SpinCtrl = nullptr;
  57. wxPGEditor_DatePickerCtrl = nullptr;
  58. }
  59. if( !dynamic_cast<PG_CELL_RENDERER*>( wxPGGlobalVars->m_defaultRenderer ) )
  60. {
  61. delete wxPGGlobalVars->m_defaultRenderer;
  62. wxPGGlobalVars->m_defaultRenderer = new PG_CELL_RENDERER();
  63. }
  64. m_caption = new wxStaticText( this, wxID_ANY, _( "No objects selected" ) );
  65. mainSizer->Add( m_caption, 0, wxALL | wxEXPAND, 5 );
  66. m_grid = new wxPropertyGrid( this, wxID_ANY, wxDefaultPosition, wxSize( 300, 400 ),
  67. wxPG_DEFAULT_STYLE );
  68. m_grid->SetUnspecifiedValueAppearance( wxPGCell( wxT( "<...>" ) ) );
  69. m_grid->SetExtraStyle( wxPG_EX_HELP_AS_TOOLTIPS );
  70. #if wxCHECK_VERSION( 3, 3, 0 )
  71. m_grid->SetValidationFailureBehavior( wxPGVFBFlags::MarkCell );
  72. #else
  73. m_grid->SetValidationFailureBehavior( wxPG_VFB_MARK_CELL );
  74. #endif
  75. m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_RETURN );
  76. m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_NUMPAD_ENTER );
  77. m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_DOWN );
  78. m_grid->AddActionTrigger( wxPG_ACTION_PREV_PROPERTY, WXK_UP );
  79. m_grid->AddActionTrigger( wxPG_ACTION_EDIT, WXK_SPACE );
  80. m_grid->DedicateKey( WXK_RETURN );
  81. m_grid->DedicateKey( WXK_NUMPAD_ENTER );
  82. m_grid->DedicateKey( WXK_DOWN );
  83. m_grid->DedicateKey( WXK_UP );
  84. mainSizer->Add( m_grid, 1, wxEXPAND, 5 );
  85. m_grid->SetCellDisabledTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
  86. #ifdef __WXGTK__
  87. // Needed for dark mode, on wx 3.0 at least.
  88. m_grid->SetCaptionTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_CAPTIONTEXT ) );
  89. #endif
  90. SetFont( KIUI::GetDockedPaneFont( this ) );
  91. SetSizer( mainSizer );
  92. Layout();
  93. m_grid->CenterSplitter();
  94. Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( PROPERTIES_PANEL::onCharHook ), nullptr, this );
  95. Connect( wxEVT_PG_CHANGED, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanged ), nullptr, this );
  96. Connect( wxEVT_PG_CHANGING, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanging ), nullptr, this );
  97. Connect( wxEVT_SHOW, wxShowEventHandler( PROPERTIES_PANEL::onShow ), nullptr, this );
  98. Bind( wxEVT_PG_COL_END_DRAG,
  99. [&]( wxPropertyGridEvent& )
  100. {
  101. m_splitter_key_proportion =
  102. static_cast<float>( m_grid->GetSplitterPosition() ) / m_grid->GetSize().x;
  103. } );
  104. Bind( wxEVT_SIZE,
  105. [&]( wxSizeEvent& aEvent )
  106. {
  107. CallAfter( [&]()
  108. {
  109. RecalculateSplitterPos();
  110. } );
  111. aEvent.Skip();
  112. } );
  113. }
  114. void PROPERTIES_PANEL::OnLanguageChanged()
  115. {
  116. UpdateData();
  117. }
  118. void PROPERTIES_PANEL::rebuildProperties( const SELECTION& aSelection )
  119. {
  120. auto reset =
  121. [&]()
  122. {
  123. if( m_grid->IsEditorFocused() )
  124. m_grid->CommitChangesFromEditor();
  125. m_grid->Clear();
  126. m_displayed.clear();
  127. };
  128. if( aSelection.Empty() )
  129. {
  130. m_caption->SetLabel( _( "No objects selected" ) );
  131. reset();
  132. return;
  133. }
  134. // Get all the selected types
  135. std::set<TYPE_ID> types;
  136. for( EDA_ITEM* item : aSelection )
  137. types.insert( TYPE_HASH( *item ) );
  138. wxCHECK( !types.empty(), /* void */ );
  139. PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
  140. propMgr.SetUnits( m_frame->GetUserUnits() );
  141. propMgr.SetTransforms( &m_frame->GetOriginTransforms() );
  142. std::set<PROPERTY_BASE*> commonProps;
  143. const PROPERTY_LIST& allProperties = propMgr.GetProperties( *types.begin() );
  144. copy( allProperties.begin(), allProperties.end(), inserter( commonProps, commonProps.begin() ) );
  145. PROPERTY_DISPLAY_ORDER displayOrder = propMgr.GetDisplayOrder( *types.begin() );
  146. std::vector<wxString> groupDisplayOrder = propMgr.GetGroupDisplayOrder( *types.begin() );
  147. std::set<wxString> groups( groupDisplayOrder.begin(), groupDisplayOrder.end() );
  148. std::set<PROPERTY_BASE*> availableProps;
  149. // Get all possible properties
  150. for( const TYPE_ID& type : types )
  151. {
  152. const PROPERTY_LIST& itemProps = propMgr.GetProperties( type );
  153. const PROPERTY_DISPLAY_ORDER& itemDisplayOrder = propMgr.GetDisplayOrder( type );
  154. copy( itemDisplayOrder.begin(), itemDisplayOrder.end(),
  155. inserter( displayOrder, displayOrder.begin() ) );
  156. const std::vector<wxString>& itemGroups = propMgr.GetGroupDisplayOrder( type );
  157. for( const wxString& group : itemGroups )
  158. {
  159. if( !groups.count( group ) )
  160. {
  161. groupDisplayOrder.emplace_back( group );
  162. groups.insert( group );
  163. }
  164. }
  165. for( auto it = commonProps.begin(); it != commonProps.end(); /* ++it in the loop */ )
  166. {
  167. if( !binary_search( itemProps.begin(), itemProps.end(), *it ) )
  168. it = commonProps.erase( it );
  169. else
  170. ++it;
  171. }
  172. }
  173. EDA_ITEM* firstItem = aSelection.Front();
  174. bool isFootprintEditor = m_frame->IsType( FRAME_FOOTPRINT_EDITOR );
  175. // Find a set of properties that is common to all selected items
  176. for( PROPERTY_BASE* property : commonProps )
  177. {
  178. if( property->IsHiddenFromPropertiesManager() )
  179. continue;
  180. if( isFootprintEditor && property->IsHiddenFromLibraryEditors() )
  181. continue;
  182. if( propMgr.IsAvailableFor( TYPE_HASH( *firstItem ), property, firstItem ) )
  183. availableProps.insert( property );
  184. }
  185. bool writeable = true;
  186. std::set<PROPERTY_BASE*> existingProps;
  187. for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
  188. {
  189. wxPGProperty* pgProp = it.GetProperty();
  190. PROPERTY_BASE* property = propMgr.GetProperty( TYPE_HASH( *firstItem ), pgProp->GetName() );
  191. // Switching item types? Property may no longer be valid
  192. if( !property )
  193. continue;
  194. wxVariant commonVal;
  195. extractValueAndWritability( aSelection, property, commonVal, writeable );
  196. pgProp->SetValue( commonVal );
  197. pgProp->Enable( writeable );
  198. existingProps.insert( property );
  199. }
  200. if( existingProps == availableProps )
  201. return;
  202. // Some difference exists: start from scratch
  203. reset();
  204. std::map<wxPGProperty*, int> pgPropOrders;
  205. std::map<wxString, std::vector<wxPGProperty*>> pgPropGroups;
  206. for( PROPERTY_BASE* property : availableProps )
  207. {
  208. wxPGProperty* pgProp = createPGProperty( property );
  209. wxVariant commonVal;
  210. if( !extractValueAndWritability( aSelection, property, commonVal, writeable ) )
  211. continue;
  212. if( pgProp )
  213. {
  214. pgProp->SetValue( commonVal );
  215. pgProp->Enable( writeable );
  216. m_displayed.push_back( property );
  217. wxASSERT( displayOrder.count( property ) );
  218. pgPropOrders[pgProp] = displayOrder[property];
  219. pgPropGroups[property->Group()].emplace_back( pgProp );
  220. }
  221. }
  222. if( aSelection.Size() > 1 )
  223. {
  224. m_caption->SetLabel( wxString::Format( _( "%d objects selected" ), aSelection.Size() ) );
  225. }
  226. else
  227. {
  228. m_caption->SetLabel( aSelection.Front()->GetFriendlyName() );
  229. }
  230. const wxString unspecifiedGroupCaption = _( "Basic Properties" );
  231. for( const wxString& groupName : groupDisplayOrder )
  232. {
  233. if( !pgPropGroups.count( groupName ) )
  234. continue;
  235. std::vector<wxPGProperty*>& properties = pgPropGroups[groupName];
  236. wxString groupCaption = wxGetTranslation( groupName );
  237. auto groupItem = new wxPropertyCategory( groupName.IsEmpty() ? unspecifiedGroupCaption
  238. : groupCaption );
  239. m_grid->Append( groupItem );
  240. std::sort( properties.begin(), properties.end(),
  241. [&]( wxPGProperty*& aFirst, wxPGProperty*& aSecond )
  242. {
  243. return pgPropOrders[aFirst] < pgPropOrders[aSecond];
  244. } );
  245. for( wxPGProperty* property : properties )
  246. m_grid->Append( property );
  247. }
  248. RecalculateSplitterPos();
  249. }
  250. bool PROPERTIES_PANEL::getItemValue( EDA_ITEM* aItem, PROPERTY_BASE* aProperty, wxVariant& aValue )
  251. {
  252. const wxAny& any = aItem->Get( aProperty );
  253. bool converted = false;
  254. if( aProperty->HasChoices() )
  255. {
  256. // handle enums as ints, since there are no default conversion functions for wxAny
  257. int tmp;
  258. converted = any.GetAs<int>( &tmp );
  259. if( converted )
  260. aValue = wxVariant( tmp );
  261. }
  262. if( !converted ) // all other types
  263. converted = any.GetAs( &aValue );
  264. if( !converted )
  265. wxFAIL_MSG( wxS( "Could not convert wxAny to wxVariant" ) );
  266. return converted;
  267. }
  268. bool PROPERTIES_PANEL::extractValueAndWritability( const SELECTION& aSelection,
  269. PROPERTY_BASE* aProperty,
  270. wxVariant& aValue, bool& aWritable )
  271. {
  272. PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
  273. propMgr.SetUnits( m_frame->GetUserUnits() );
  274. propMgr.SetTransforms( &m_frame->GetOriginTransforms() );
  275. bool different = false;
  276. wxVariant commonVal;
  277. aWritable = true;
  278. for( EDA_ITEM* item : aSelection )
  279. {
  280. if( !propMgr.IsAvailableFor( TYPE_HASH( *item ), aProperty, item ) )
  281. return false;
  282. // If read-only for any of the selection, read-only for the whole selection.
  283. if( !aProperty->Writeable( item ) )
  284. aWritable = false;
  285. wxVariant value;
  286. if( getItemValue( item, aProperty, value ) )
  287. {
  288. // Null value indicates different property values between items
  289. if( !different && !aValue.IsNull() && value != aValue )
  290. {
  291. different = true;
  292. aValue.MakeNull();
  293. }
  294. else if( !different )
  295. {
  296. aValue = value;
  297. }
  298. }
  299. else
  300. {
  301. // getItemValue returned false -- not available for this item
  302. return false;
  303. }
  304. }
  305. return true;
  306. }
  307. void PROPERTIES_PANEL::onShow( wxShowEvent& aEvent )
  308. {
  309. if( aEvent.IsShown() )
  310. UpdateData();
  311. }
  312. void PROPERTIES_PANEL::onCharHook( wxKeyEvent& aEvent )
  313. {
  314. if( aEvent.GetKeyCode() == WXK_TAB && !aEvent.ShiftDown() )
  315. {
  316. m_grid->CommitChangesFromEditor();
  317. return;
  318. }
  319. if( aEvent.GetKeyCode() == WXK_SPACE )
  320. {
  321. if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
  322. {
  323. if( prop->GetValueType() == wxT( "bool" ) )
  324. {
  325. m_grid->SetPropertyValue( prop, !prop->GetValue().GetBool() );
  326. return;
  327. }
  328. }
  329. }
  330. if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
  331. {
  332. m_grid->CommitChangesFromEditor();
  333. /* don't skip this one; if we're not the last property we'll also go to the next row */
  334. }
  335. aEvent.Skip();
  336. }
  337. void PROPERTIES_PANEL::RecalculateSplitterPos()
  338. {
  339. if( m_splitter_key_proportion < 0 )
  340. m_grid->CenterSplitter();
  341. else
  342. m_grid->SetSplitterPosition( m_splitter_key_proportion * m_grid->GetSize().x );
  343. }
  344. void PROPERTIES_PANEL::SetSplitterProportion( float aProportion )
  345. {
  346. m_splitter_key_proportion = aProportion;
  347. RecalculateSplitterPos();
  348. }