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.

572 lines
17 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2017 Chris Pavlina <pavlina.chris@gmail.com>
  5. * Copyright (C) 2014 Henner Zeller <h.zeller@acm.org>
  6. * Copyright (C) 2014-2021 KiCad Developers, see AUTHORS.txt for contributors.
  7. *
  8. * This program is free software: you can redistribute it and/or modify it
  9. * under the terms of the GNU General Public License as published by the
  10. * Free Software Foundation, either version 3 of the License, or (at your
  11. * option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful, but
  14. * WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. * 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 <eda_base_frame.h>
  22. #include <eda_pattern_match.h>
  23. #include <kiface_i.h>
  24. #include <config_params.h>
  25. #include <lib_tree_model_adapter.h>
  26. #include <project/project_file.h>
  27. #include <settings/app_settings.h>
  28. #include <widgets/ui_common.h>
  29. #include <wx/tokenzr.h>
  30. #include <wx/wupdlock.h>
  31. #include <string_utils.h>
  32. #define PINNED_ITEMS_KEY wxT( "PinnedItems" )
  33. static const int kDataViewIndent = 20;
  34. wxDataViewItem LIB_TREE_MODEL_ADAPTER::ToItem( const LIB_TREE_NODE* aNode )
  35. {
  36. return wxDataViewItem( const_cast<void*>( static_cast<void const*>( aNode ) ) );
  37. }
  38. LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ToNode( wxDataViewItem aItem )
  39. {
  40. return static_cast<LIB_TREE_NODE*>( aItem.GetID() );
  41. }
  42. unsigned int LIB_TREE_MODEL_ADAPTER::IntoArray( const LIB_TREE_NODE& aNode,
  43. wxDataViewItemArray& aChildren )
  44. {
  45. unsigned int n = 0;
  46. for( std::unique_ptr<LIB_TREE_NODE> const& child: aNode.m_Children )
  47. {
  48. if( child->m_Score > 0 )
  49. {
  50. aChildren.Add( ToItem( &*child ) );
  51. ++n;
  52. }
  53. }
  54. return n;
  55. }
  56. LIB_TREE_MODEL_ADAPTER::LIB_TREE_MODEL_ADAPTER( EDA_BASE_FRAME* aParent,
  57. const wxString& aPinnedKey ) :
  58. m_parent( aParent ),
  59. m_filter( SYM_FILTER_NONE ),
  60. m_show_units( true ),
  61. m_preselect_unit( 0 ),
  62. m_freeze( 0 ),
  63. m_col_part( nullptr ),
  64. m_col_desc( nullptr ),
  65. m_widget( nullptr ),
  66. m_pinnedLibs(),
  67. m_pinnedKey( aPinnedKey )
  68. {
  69. // Default column widths
  70. m_colWidths[PART_COL] = 360;
  71. m_colWidths[DESC_COL] = 2000;
  72. APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings();
  73. m_colWidths[PART_COL] = cfg->m_LibTree.column_width;
  74. // Read the pinned entries from the project config
  75. PROJECT_FILE& project = m_parent->Kiway().Prj().GetProjectFile();
  76. std::vector<wxString>& entries = ( m_pinnedKey == "pinned_symbol_libs" ) ?
  77. project.m_PinnedSymbolLibs :
  78. project.m_PinnedFootprintLibs;
  79. for( const wxString& entry : entries )
  80. m_pinnedLibs.push_back( entry );
  81. }
  82. LIB_TREE_MODEL_ADAPTER::~LIB_TREE_MODEL_ADAPTER()
  83. {}
  84. void LIB_TREE_MODEL_ADAPTER::SaveColWidths()
  85. {
  86. if( m_widget )
  87. {
  88. APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings();
  89. cfg->m_LibTree.column_width = m_widget->GetColumn( PART_COL )->GetWidth();
  90. }
  91. }
  92. void LIB_TREE_MODEL_ADAPTER::SavePinnedItems()
  93. {
  94. PROJECT_FILE& project = m_parent->Kiway().Prj().GetProjectFile();
  95. std::vector<wxString>& entries = ( m_pinnedKey == "pinned_symbol_libs" ) ?
  96. project.m_PinnedSymbolLibs :
  97. project.m_PinnedFootprintLibs;
  98. entries.clear();
  99. m_pinnedLibs.clear();
  100. for( std::unique_ptr<LIB_TREE_NODE>& child: m_tree.m_Children )
  101. {
  102. if( child->m_Pinned )
  103. {
  104. m_pinnedLibs.push_back( child->m_LibId.GetLibNickname() );
  105. entries.push_back( child->m_LibId.GetLibNickname() );
  106. }
  107. }
  108. }
  109. void LIB_TREE_MODEL_ADAPTER::SetFilter( SYM_FILTER_TYPE aFilter )
  110. {
  111. m_filter = aFilter;
  112. }
  113. void LIB_TREE_MODEL_ADAPTER::ShowUnits( bool aShow )
  114. {
  115. m_show_units = aShow;
  116. }
  117. void LIB_TREE_MODEL_ADAPTER::SetPreselectNode( const LIB_ID& aLibId, int aUnit )
  118. {
  119. m_preselect_lib_id = aLibId;
  120. m_preselect_unit = aUnit;
  121. }
  122. LIB_TREE_NODE_LIB& LIB_TREE_MODEL_ADAPTER::DoAddLibraryNode( const wxString& aNodeName,
  123. const wxString& aDesc )
  124. {
  125. LIB_TREE_NODE_LIB& lib_node = m_tree.AddLib( aNodeName, aDesc );
  126. lib_node.m_Pinned = m_pinnedLibs.Index( lib_node.m_LibId.GetLibNickname() ) != wxNOT_FOUND;
  127. return lib_node;
  128. }
  129. void LIB_TREE_MODEL_ADAPTER::DoAddLibrary( const wxString& aNodeName, const wxString& aDesc,
  130. const std::vector<LIB_TREE_ITEM*>& aItemList,
  131. bool presorted )
  132. {
  133. LIB_TREE_NODE_LIB& lib_node = DoAddLibraryNode( aNodeName, aDesc );
  134. for( LIB_TREE_ITEM* item: aItemList )
  135. lib_node.AddItem( item );
  136. lib_node.AssignIntrinsicRanks( presorted );
  137. }
  138. void LIB_TREE_MODEL_ADAPTER::UpdateSearchString( const wxString& aSearch, bool aState )
  139. {
  140. {
  141. wxWindowUpdateLocker updateLock( m_widget );
  142. // Even with the updateLock, wxWidgets sometimes ties its knickers in a knot trying to
  143. // run a wxdataview_selection_changed_callback() on a row that has been deleted.
  144. // https://bugs.launchpad.net/kicad/+bug/1756255
  145. m_widget->UnselectAll();
  146. // This collapse is required before the call to "Freeze()" below. Once Freeze()
  147. // is called, GetParent() will return nullptr. While this works for some calls, it
  148. // segfaults when we have any expanded elements b/c the sub units in the tree don't
  149. // have explicit references that are maintained over a search
  150. // The tree will be expanded again below when we get our matches
  151. //
  152. // Also note that this cannot happen when we have deleted a symbol as GTK will also
  153. // iterate over the tree in this case and find a symbol that has an invalid link
  154. // and crash https://gitlab.com/kicad/code/kicad/-/issues/6910
  155. if( !aState && !aSearch.IsNull() && m_tree.m_Children.size() )
  156. {
  157. for( std::unique_ptr<LIB_TREE_NODE>& child: m_tree.m_Children )
  158. m_widget->Collapse( wxDataViewItem( &*child ) );
  159. }
  160. // DO NOT REMOVE THE FREEZE/THAW. This freeze/thaw is a flag for this model adapter
  161. // that tells it when it shouldn't trust any of the data in the model. When set, it will
  162. // not return invalid data to the UI, since this invalid data can cause crashes.
  163. // This is different than the update locker, which locks the UI aspects only.
  164. Freeze();
  165. BeforeReset();
  166. m_tree.ResetScore();
  167. for( std::unique_ptr<LIB_TREE_NODE>& child: m_tree.m_Children )
  168. {
  169. if( child->m_Pinned )
  170. child->m_Score *= 2;
  171. }
  172. wxStringTokenizer tokenizer( aSearch );
  173. while( tokenizer.HasMoreTokens() )
  174. {
  175. const wxString term = tokenizer.GetNextToken().Lower();
  176. EDA_COMBINED_MATCHER matcher( term );
  177. m_tree.UpdateScore( matcher );
  178. }
  179. m_tree.SortNodes();
  180. AfterReset();
  181. Thaw();
  182. }
  183. LIB_TREE_NODE* bestMatch = ShowResults();
  184. if( !bestMatch )
  185. bestMatch = ShowPreselect();
  186. if( !bestMatch )
  187. bestMatch = ShowSingleLibrary();
  188. if( bestMatch )
  189. {
  190. wxDataViewItem item = wxDataViewItem( bestMatch );
  191. m_widget->Select( item );
  192. // Make sure the *parent* item is visible. The selected item is the
  193. // first (shown) child of the parent. So it's always right below the parent,
  194. // and this way the user can also see what library the selected part belongs to,
  195. // without having a case where the selection is off the screen (unless the
  196. // window is a single row high, which is unlikely)
  197. //
  198. // This also happens to circumvent https://bugs.launchpad.net/kicad/+bug/1804400
  199. // which appears to be a GTK+3 bug.
  200. {
  201. wxDataViewItem parent = GetParent( item );
  202. if( parent.IsOk() )
  203. item = parent;
  204. }
  205. m_widget->EnsureVisible( item );
  206. }
  207. }
  208. void LIB_TREE_MODEL_ADAPTER::AttachTo( wxDataViewCtrl* aDataViewCtrl )
  209. {
  210. wxString partHead = _( "Item" );
  211. wxString descHead = _( "Description" );
  212. // The extent of the text doesn't take into account the space on either side
  213. // in the header, so artificially pad it
  214. wxSize partHeadMinWidth = KIUI::GetTextSize( partHead + "MMM", aDataViewCtrl );
  215. // Ensure the part column is wider than the smallest allowable width
  216. if( m_colWidths[PART_COL] < partHeadMinWidth.x )
  217. m_colWidths[PART_COL] = partHeadMinWidth.x;
  218. m_widget = aDataViewCtrl;
  219. aDataViewCtrl->SetIndent( kDataViewIndent );
  220. aDataViewCtrl->AssociateModel( this );
  221. aDataViewCtrl->ClearColumns();
  222. m_col_part = aDataViewCtrl->AppendTextColumn( partHead, PART_COL, wxDATAVIEW_CELL_INERT,
  223. m_colWidths[PART_COL] );
  224. m_col_desc = aDataViewCtrl->AppendTextColumn( descHead, DESC_COL, wxDATAVIEW_CELL_INERT,
  225. m_colWidths[DESC_COL] );
  226. m_col_part->SetMinWidth( partHeadMinWidth.x );
  227. }
  228. LIB_ID LIB_TREE_MODEL_ADAPTER::GetAliasFor( const wxDataViewItem& aSelection ) const
  229. {
  230. const LIB_TREE_NODE* node = ToNode( aSelection );
  231. LIB_ID emptyId;
  232. if( !node )
  233. return emptyId;
  234. return node->m_LibId;
  235. }
  236. int LIB_TREE_MODEL_ADAPTER::GetUnitFor( const wxDataViewItem& aSelection ) const
  237. {
  238. const LIB_TREE_NODE* node = ToNode( aSelection );
  239. return node ? node->m_Unit : 0;
  240. }
  241. LIB_TREE_NODE::TYPE LIB_TREE_MODEL_ADAPTER::GetTypeFor( const wxDataViewItem& aSelection ) const
  242. {
  243. const LIB_TREE_NODE* node = ToNode( aSelection );
  244. return node ? node->m_Type : LIB_TREE_NODE::INVALID;
  245. }
  246. LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::GetTreeNodeFor( const wxDataViewItem& aSelection ) const
  247. {
  248. return ToNode( aSelection );
  249. }
  250. int LIB_TREE_MODEL_ADAPTER::GetItemCount() const
  251. {
  252. int n = 0;
  253. for( const std::unique_ptr<LIB_TREE_NODE>& lib: m_tree.m_Children )
  254. n += lib->m_Children.size();
  255. return n;
  256. }
  257. wxDataViewItem LIB_TREE_MODEL_ADAPTER::FindItem( const LIB_ID& aLibId )
  258. {
  259. for( std::unique_ptr<LIB_TREE_NODE>& lib: m_tree.m_Children )
  260. {
  261. if( lib->m_Name != aLibId.GetLibNickname() )
  262. continue;
  263. // if part name is not specified, return the library node
  264. if( aLibId.GetLibItemName() == "" )
  265. return ToItem( lib.get() );
  266. for( std::unique_ptr<LIB_TREE_NODE>& alias: lib->m_Children )
  267. {
  268. if( alias->m_Name == aLibId.GetLibItemName() )
  269. return ToItem( alias.get() );
  270. }
  271. break; // could not find the part in the requested library
  272. }
  273. return wxDataViewItem();
  274. }
  275. unsigned int LIB_TREE_MODEL_ADAPTER::GetChildren( const wxDataViewItem& aItem,
  276. wxDataViewItemArray& aChildren ) const
  277. {
  278. const LIB_TREE_NODE* node = ( aItem.IsOk() ? ToNode( aItem ) : &m_tree );
  279. if( node->m_Type != LIB_TREE_NODE::TYPE::LIBID
  280. || ( m_show_units && node->m_Type == LIB_TREE_NODE::TYPE::LIBID ) )
  281. return IntoArray( *node, aChildren );
  282. else
  283. return 0;
  284. }
  285. void LIB_TREE_MODEL_ADAPTER::FinishTreeInitialization()
  286. {
  287. m_col_part->SetWidth( m_colWidths[PART_COL] );
  288. m_col_desc->SetWidth( m_colWidths[DESC_COL] );
  289. }
  290. void LIB_TREE_MODEL_ADAPTER::RefreshTree()
  291. {
  292. // Yes, this is an enormous hack. But it works on all platforms, it doesn't suffer
  293. // the On^2 sorting issues that ItemChanged() does on OSX, and it doesn't lose the
  294. // user's scroll position (which re-attaching or deleting/re-inserting columns does).
  295. static int walk = 1;
  296. int partWidth = m_col_part->GetWidth();
  297. int descWidth = m_col_desc->GetWidth();
  298. // Only use the widths read back if they are non-zero.
  299. // GTK returns the displayed width of the column, which is not calculated immediately
  300. if( descWidth > 0 )
  301. {
  302. m_colWidths[PART_COL] = partWidth;
  303. m_colWidths[DESC_COL] = descWidth;
  304. }
  305. m_colWidths[PART_COL] += walk;
  306. m_colWidths[DESC_COL] -= walk;
  307. m_col_part->SetWidth( m_colWidths[PART_COL] );
  308. m_col_desc->SetWidth( m_colWidths[DESC_COL] );
  309. walk = -walk;
  310. }
  311. bool LIB_TREE_MODEL_ADAPTER::HasContainerColumns( const wxDataViewItem& aItem ) const
  312. {
  313. return IsContainer( aItem );
  314. }
  315. bool LIB_TREE_MODEL_ADAPTER::IsContainer( const wxDataViewItem& aItem ) const
  316. {
  317. LIB_TREE_NODE* node = ToNode( aItem );
  318. return node ? node->m_Children.size() : true;
  319. }
  320. wxDataViewItem LIB_TREE_MODEL_ADAPTER::GetParent( const wxDataViewItem& aItem ) const
  321. {
  322. if( m_freeze )
  323. return ToItem( nullptr );
  324. LIB_TREE_NODE* node = ToNode( aItem );
  325. LIB_TREE_NODE* parent = node ? node->m_Parent : nullptr;
  326. // wxDataViewModel has no root node, but rather top-level elements have
  327. // an invalid (null) parent.
  328. if( !node || !parent || parent->m_Type == LIB_TREE_NODE::TYPE::ROOT )
  329. return ToItem( nullptr );
  330. else
  331. return ToItem( parent );
  332. }
  333. void LIB_TREE_MODEL_ADAPTER::GetValue( wxVariant& aVariant,
  334. const wxDataViewItem& aItem,
  335. unsigned int aCol ) const
  336. {
  337. if( IsFrozen() )
  338. {
  339. aVariant = wxEmptyString;
  340. return;
  341. }
  342. LIB_TREE_NODE* node = ToNode( aItem );
  343. wxASSERT( node );
  344. switch( aCol )
  345. {
  346. default: // column == -1 is used for default Compare function
  347. case 0:
  348. aVariant = UnescapeString( node->m_Name );
  349. break;
  350. case 1:
  351. aVariant = node->m_Desc;
  352. break;
  353. }
  354. }
  355. bool LIB_TREE_MODEL_ADAPTER::GetAttr( const wxDataViewItem& aItem,
  356. unsigned int aCol,
  357. wxDataViewItemAttr& aAttr ) const
  358. {
  359. if( IsFrozen() )
  360. return false;
  361. LIB_TREE_NODE* node = ToNode( aItem );
  362. wxASSERT( node );
  363. if( node->m_Type != LIB_TREE_NODE::LIBID )
  364. {
  365. // Currently only aliases are formatted at all
  366. return false;
  367. }
  368. if( !node->m_IsRoot && aCol == 0 )
  369. {
  370. // Names of non-root aliases are italicized
  371. aAttr.SetItalic( true );
  372. return true;
  373. }
  374. else
  375. {
  376. return false;
  377. }
  378. }
  379. void LIB_TREE_MODEL_ADAPTER::FindAndExpand( LIB_TREE_NODE& aNode,
  380. std::function<bool( const LIB_TREE_NODE* )> aFunc,
  381. LIB_TREE_NODE** aHighScore )
  382. {
  383. for( std::unique_ptr<LIB_TREE_NODE>& node: aNode.m_Children )
  384. {
  385. if( aFunc( &*node ) )
  386. {
  387. wxDataViewItem item = wxDataViewItem( &*node );
  388. m_widget->ExpandAncestors( item );
  389. if( !(*aHighScore) || node->m_Score > (*aHighScore)->m_Score )
  390. (*aHighScore) = &*node;
  391. }
  392. FindAndExpand( *node, aFunc, aHighScore );
  393. }
  394. }
  395. LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowResults()
  396. {
  397. LIB_TREE_NODE* highScore = nullptr;
  398. FindAndExpand( m_tree,
  399. []( LIB_TREE_NODE const* n )
  400. {
  401. // return leaf nodes with some level of matching
  402. return n->m_Type == LIB_TREE_NODE::TYPE::LIBID && n->m_Score > 1;
  403. },
  404. &highScore );
  405. return highScore;
  406. }
  407. LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowPreselect()
  408. {
  409. LIB_TREE_NODE* highScore = nullptr;
  410. if( !m_preselect_lib_id.IsValid() )
  411. return highScore;
  412. FindAndExpand( m_tree,
  413. [&]( LIB_TREE_NODE const* n )
  414. {
  415. if( n->m_Type == LIB_TREE_NODE::LIBID && ( n->m_Children.empty() ||
  416. !m_preselect_unit ) )
  417. return m_preselect_lib_id == n->m_LibId;
  418. else if( n->m_Type == LIB_TREE_NODE::UNIT && m_preselect_unit )
  419. return m_preselect_lib_id == n->m_Parent->m_LibId &&
  420. m_preselect_unit == n->m_Unit;
  421. else
  422. return false;
  423. },
  424. &highScore );
  425. return highScore;
  426. }
  427. LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowSingleLibrary()
  428. {
  429. LIB_TREE_NODE* highScore = nullptr;
  430. FindAndExpand( m_tree,
  431. []( LIB_TREE_NODE const* n )
  432. {
  433. return n->m_Type == LIB_TREE_NODE::TYPE::LIBID &&
  434. n->m_Parent->m_Parent->m_Children.size() == 1;
  435. },
  436. &highScore );
  437. return highScore;
  438. }