/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2017 Chris Pavlina * Copyright (C) 2014 Henner Zeller * Copyright (C) 2023 CERN * Copyright (C) 2014-2023 KiCad Developers, see AUTHORS.txt for contributors. * * 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 #include #include #include #include #include #include #include #include #include #include #include #include static const int kDataViewIndent = 20; class LIB_TREE_RENDERER : public wxDataViewCustomRenderer { public: LIB_TREE_RENDERER() : m_canvasItem( false ) {} wxSize GetSize() const override { return wxSize( GetOwner()->GetWidth(), GetTextExtent( m_text ).y + 2 ); } bool GetValue( wxVariant& aValue ) const override { aValue = m_text; return true; } bool SetValue( const wxVariant& aValue ) override { m_text = aValue.GetString(); return true; } void SetAttr( const wxDataViewItemAttr& aAttr ) override { // Use strikethrough as a proxy for is-canvas-item m_canvasItem = aAttr.GetStrikethrough(); wxDataViewItemAttr realAttr = aAttr; realAttr.SetStrikethrough( false ); wxDataViewCustomRenderer::SetAttr( realAttr ); } bool Render( wxRect aRect, wxDC *dc, int aState ) override { RenderBackground( dc, aRect ); if( m_canvasItem ) { wxPoint points[6]; points[0] = aRect.GetTopLeft(); points[1] = aRect.GetTopRight() + wxPoint( -4, 0 ); points[2] = aRect.GetTopRight() + wxPoint( 0, aRect.GetHeight() / 2 ); points[3] = aRect.GetBottomRight() + wxPoint( -4, 1 ); points[4] = aRect.GetBottomLeft() + wxPoint( 0, 1 ); points[5] = aRect.GetTopLeft(); dc->SetPen( KIPLATFORM::UI::IsDarkTheme() ? *wxWHITE_PEN : *wxBLACK_PEN ); dc->DrawLines( 6, points ); } // We should be able to pass wxDATAVIEW_CELL_SELECTED into RenderText() and have it do // the right thing -- but it picks wxSYS_COLOUR_HIGHLIGHTTEXT on MacOS and GTK (instead // of wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT). if( aState & wxDATAVIEW_CELL_SELECTED ) dc->SetTextForeground( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT ) ); aRect.Deflate( 1 ); RenderText( m_text, 0, aRect, dc, 0 ); return true; } private: bool m_canvasItem; wxString m_text; }; wxDataViewItem LIB_TREE_MODEL_ADAPTER::ToItem( const LIB_TREE_NODE* aNode ) { return wxDataViewItem( const_cast( static_cast( aNode ) ) ); } LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ToNode( wxDataViewItem aItem ) { return static_cast( aItem.GetID() ); } LIB_TREE_MODEL_ADAPTER::LIB_TREE_MODEL_ADAPTER( EDA_BASE_FRAME* aParent, const wxString& aPinnedKey ) : m_widget( nullptr ), m_parent( aParent ), m_sort_mode( BEST_MATCH ), m_show_units( true ), m_preselect_unit( 0 ), m_freeze( 0 ), m_filter( nullptr ) { // Default column widths. Do not translate these names. m_colWidths[ _HKI( "Item" ) ] = 300; m_colWidths[ _HKI( "Description" ) ] = 600; m_availableColumns = { _HKI( "Item" ), _HKI( "Description" ) }; APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings(); for( const std::pair& pair : cfg->m_LibTree.column_widths ) m_colWidths[pair.first] = pair.second; m_shownColumns = cfg->m_LibTree.columns; if( m_shownColumns.empty() ) m_shownColumns = { _HKI( "Item" ), _HKI( "Description" ) }; if( m_shownColumns[0] != _HKI( "Item" ) ) m_shownColumns.insert( m_shownColumns.begin(), _HKI( "Item" ) ); } LIB_TREE_MODEL_ADAPTER::~LIB_TREE_MODEL_ADAPTER() {} std::vector LIB_TREE_MODEL_ADAPTER::GetOpenLibs() const { std::vector openLibs; wxDataViewItem rootItem( nullptr ); wxDataViewItemArray children; GetChildren( rootItem, children ); for( const wxDataViewItem& child : children ) { if( m_widget->IsExpanded( child ) ) openLibs.emplace_back( ToNode( child )->m_LibId.GetLibNickname().wx_str() ); } return openLibs; } void LIB_TREE_MODEL_ADAPTER::OpenLibs( const std::vector& aLibs ) { wxWindowUpdateLocker updateLock( m_widget ); for( const wxString& lib : aLibs ) { wxDataViewItem item = FindItem( LIB_ID( lib, wxEmptyString ) ); if( item.IsOk() ) m_widget->Expand( item ); } } void LIB_TREE_MODEL_ADAPTER::SaveSettings() { if( m_widget ) { APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings(); cfg->m_LibTree.columns = GetShownColumns(); cfg->m_LibTree.column_widths.clear(); for( const std::pair& pair : m_colNameMap ) cfg->m_LibTree.column_widths[pair.first] = pair.second->GetWidth(); cfg->m_LibTree.open_libs = GetOpenLibs(); } } void LIB_TREE_MODEL_ADAPTER::ShowUnits( bool aShow ) { m_show_units = aShow; } void LIB_TREE_MODEL_ADAPTER::SetPreselectNode( const LIB_ID& aLibId, int aUnit ) { m_preselect_lib_id = aLibId; m_preselect_unit = aUnit; } LIB_TREE_NODE_LIBRARY& LIB_TREE_MODEL_ADAPTER::DoAddLibraryNode( const wxString& aNodeName, const wxString& aDesc, bool pinned ) { LIB_TREE_NODE_LIBRARY& lib_node = m_tree.AddLib( aNodeName, aDesc ); lib_node.m_Pinned = pinned; return lib_node; } void LIB_TREE_MODEL_ADAPTER::DoAddLibrary( const wxString& aNodeName, const wxString& aDesc, const std::vector& aItemList, bool pinned, bool presorted ) { LIB_TREE_NODE_LIBRARY& lib_node = DoAddLibraryNode( aNodeName, aDesc, pinned ); for( LIB_TREE_ITEM* item: aItemList ) lib_node.AddItem( item ); lib_node.AssignIntrinsicRanks( presorted ); } void LIB_TREE_MODEL_ADAPTER::UpdateSearchString( const wxString& aSearch, bool aState ) { { wxWindowUpdateLocker updateLock( m_widget ); // Even with the updateLock, wxWidgets sometimes ties its knickers in a knot trying to // run a wxdataview_selection_changed_callback() on a row that has been deleted. // https://bugs.launchpad.net/kicad/+bug/1756255 m_widget->UnselectAll(); // This collapse is required before the call to "Freeze()" below. Once Freeze() // is called, GetParent() will return nullptr. While this works for some calls, it // segfaults when we have any expanded elements b/c the sub units in the tree don't // have explicit references that are maintained over a search // The tree will be expanded again below when we get our matches // // Also note that this cannot happen when we have deleted a symbol as GTK will also // iterate over the tree in this case and find a symbol that has an invalid link // and crash https://gitlab.com/kicad/code/kicad/-/issues/6910 if( !aState && !aSearch.IsNull() && m_tree.m_Children.size() ) { for( std::unique_ptr& child: m_tree.m_Children ) m_widget->Collapse( wxDataViewItem( &*child ) ); } // DO NOT REMOVE THE FREEZE/THAW. This freeze/thaw is a flag for this model adapter // that tells it when it shouldn't trust any of the data in the model. When set, it will // not return invalid data to the UI, since this invalid data can cause crashes. // This is different than the update locker, which locks the UI aspects only. Freeze(); BeforeReset(); m_tree.ResetScore(); wxStringTokenizer tokenizer( aSearch ); bool firstTerm = true; while( tokenizer.HasMoreTokens() ) { // First search for the full token, in case it appears in a search string wxString term = tokenizer.GetNextToken().Lower(); EDA_COMBINED_MATCHER termMatcher( term, CTX_LIBITEM ); m_tree.UpdateScore( &termMatcher, wxEmptyString, firstTerm ? m_filter : nullptr ); firstTerm = false; if( term.Contains( ":" ) ) { // Next search for the library:item_name wxString lib = term.BeforeFirst( ':' ); wxString itemName = term.AfterFirst( ':' ); EDA_COMBINED_MATCHER itemNameMatcher( itemName, CTX_LIBITEM ); m_tree.UpdateScore( &itemNameMatcher, lib, nullptr ); } else { // In case the full token happens to match a library name m_tree.UpdateScore( nullptr, '*' + term + '*', nullptr ); } } if( firstTerm ) { // No terms processed; just run the filter m_tree.UpdateScore( nullptr, wxEmptyString, m_filter ); } m_tree.SortNodes( m_sort_mode == BEST_MATCH ); AfterReset(); Thaw(); } const LIB_TREE_NODE* firstMatch = ShowResults(); if( firstMatch ) { wxDataViewItem item = ToItem( firstMatch ); m_widget->Select( item ); // Make sure the *parent* item is visible. The selected item is the first (shown) child // of the parent. So it's always right below the parent, and this way the user can also // see what library the selected part belongs to, without having a case where the selection // is off the screen (unless the window is a single row high, which is unlikely). // // This also happens to circumvent https://bugs.launchpad.net/kicad/+bug/1804400 which // appears to be a GTK+3 bug. { wxDataViewItem parent = GetParent( item ); if( parent.IsOk() ) m_widget->EnsureVisible( parent ); } m_widget->EnsureVisible( item ); } } void LIB_TREE_MODEL_ADAPTER::AttachTo( wxDataViewCtrl* aDataViewCtrl ) { m_widget = aDataViewCtrl; aDataViewCtrl->SetIndent( kDataViewIndent ); aDataViewCtrl->AssociateModel( this ); recreateColumns(); } void LIB_TREE_MODEL_ADAPTER::recreateColumns() { m_widget->ClearColumns(); m_columns.clear(); m_colIdxMap.clear(); m_colNameMap.clear(); // The Item column is always shown doAddColumn( wxT( "Item" ) ); for( const wxString& colName : m_shownColumns ) { if( !m_colNameMap.count( colName ) ) doAddColumn( colName, colName == wxT( "Description" ) ); } } void LIB_TREE_MODEL_ADAPTER::resortTree() { Freeze(); BeforeReset(); m_tree.SortNodes( m_sort_mode == BEST_MATCH ); AfterReset(); Thaw(); } void LIB_TREE_MODEL_ADAPTER::PinLibrary( LIB_TREE_NODE* aTreeNode ) { m_parent->Prj().PinLibrary( aTreeNode->m_LibId.GetLibNickname(), isSymbolModel() ); aTreeNode->m_Pinned = true; resortTree(); m_widget->EnsureVisible( ToItem( aTreeNode ) ); } void LIB_TREE_MODEL_ADAPTER::UnpinLibrary( LIB_TREE_NODE* aTreeNode ) { m_parent->Prj().UnpinLibrary( aTreeNode->m_LibId.GetLibNickname(), isSymbolModel() ); aTreeNode->m_Pinned = false; resortTree(); // Keep focus at top when unpinning } wxDataViewColumn* LIB_TREE_MODEL_ADAPTER::doAddColumn( const wxString& aHeader, bool aTranslate ) { wxString translatedHeader = aTranslate ? wxGetTranslation( aHeader ) : aHeader; // The extent of the text doesn't take into account the space on either side // in the header, so artificially pad it wxSize headerMinWidth = KIUI::GetTextSize( translatedHeader + wxT( "MMM" ), m_widget ); if( !m_colWidths.count( aHeader ) || m_colWidths[aHeader] < headerMinWidth.x ) m_colWidths[aHeader] = headerMinWidth.x; int index = (int) m_columns.size(); wxDataViewColumn* col = new wxDataViewColumn( translatedHeader, new LIB_TREE_RENDERER(), index, m_colWidths[aHeader], wxALIGN_NOT, wxDATAVIEW_CELL_INERT | wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE ); m_widget->AppendColumn( col ); col->SetMinWidth( headerMinWidth.x ); m_columns.emplace_back( col ); m_colNameMap[aHeader] = col; m_colIdxMap[m_columns.size() - 1] = aHeader; return col; } void LIB_TREE_MODEL_ADAPTER::addColumnIfNecessary( const wxString& aHeader ) { if( m_colNameMap.count( aHeader ) ) return; // Columns will be created later m_colNameMap[aHeader] = nullptr; m_availableColumns.emplace_back( aHeader ); } void LIB_TREE_MODEL_ADAPTER::SetShownColumns( const std::vector& aColumnNames ) { bool recreate = m_shownColumns != aColumnNames; m_shownColumns = aColumnNames; if( recreate && m_widget ) recreateColumns(); } LIB_ID LIB_TREE_MODEL_ADAPTER::GetAliasFor( const wxDataViewItem& aSelection ) const { const LIB_TREE_NODE* node = ToNode( aSelection ); return node ? node->m_LibId : LIB_ID(); } int LIB_TREE_MODEL_ADAPTER::GetUnitFor( const wxDataViewItem& aSelection ) const { const LIB_TREE_NODE* node = ToNode( aSelection ); return node ? node->m_Unit : 0; } LIB_TREE_NODE::TYPE LIB_TREE_MODEL_ADAPTER::GetTypeFor( const wxDataViewItem& aSelection ) const { const LIB_TREE_NODE* node = ToNode( aSelection ); return node ? node->m_Type : LIB_TREE_NODE::INVALID; } LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::GetTreeNodeFor( const wxDataViewItem& aSelection ) const { return ToNode( aSelection ); } int LIB_TREE_MODEL_ADAPTER::GetItemCount() const { int n = 0; for( const std::unique_ptr& lib: m_tree.m_Children ) n += lib->m_Children.size(); return n; } wxDataViewItem LIB_TREE_MODEL_ADAPTER::FindItem( const LIB_ID& aLibId ) { for( std::unique_ptr& lib: m_tree.m_Children ) { if( lib->m_Name != aLibId.GetLibNickname() ) continue; // if part name is not specified, return the library node if( aLibId.GetLibItemName() == "" ) return ToItem( lib.get() ); for( std::unique_ptr& alias: lib->m_Children ) { if( alias->m_Name == aLibId.GetLibItemName() ) return ToItem( alias.get() ); } break; // could not find the part in the requested library } return wxDataViewItem(); } wxDataViewItem LIB_TREE_MODEL_ADAPTER::GetCurrentDataViewItem() { return FindItem( m_preselect_lib_id ); } unsigned int LIB_TREE_MODEL_ADAPTER::GetChildren( const wxDataViewItem& aItem, wxDataViewItemArray& aChildren ) const { const LIB_TREE_NODE* node = ( aItem.IsOk() ? ToNode( aItem ) : &m_tree ); unsigned int count = 0; if( node->m_Type == LIB_TREE_NODE::TYPE::ROOT || node->m_Type == LIB_TREE_NODE::LIBRARY || ( m_show_units && node->m_Type == LIB_TREE_NODE::TYPE::ITEM ) ) { for( std::unique_ptr const& child: node->m_Children ) { if( child->m_Score > 0 ) { aChildren.Add( ToItem( &*child ) ); ++count; } } } return count; } void LIB_TREE_MODEL_ADAPTER::FinishTreeInitialization() { wxDataViewColumn* col = nullptr; size_t idx = 0; int totalWidth = 0; wxString header; for( ; idx < m_columns.size() - 1; idx++ ) { wxASSERT( m_colIdxMap.count( idx ) ); col = m_columns[idx]; header = m_colIdxMap[idx]; wxASSERT( m_colWidths.count( header ) ); col->SetWidth( m_colWidths[header] ); totalWidth += col->GetWidth(); } int remainingWidth = m_widget->GetSize().x - totalWidth; header = m_columns[idx]->GetTitle(); m_columns[idx]->SetWidth( std::max( m_colWidths[header], remainingWidth ) ); } void LIB_TREE_MODEL_ADAPTER::RefreshTree() { // Yes, this is an enormous hack. But it works on all platforms, it doesn't suffer // the On^2 sorting issues that ItemChanged() does on OSX, and it doesn't lose the // user's scroll position (which re-attaching or deleting/re-inserting columns does). static int walk = 1; std::vector widths; for( const wxDataViewColumn* col : m_columns ) widths.emplace_back( col->GetWidth() ); wxASSERT( widths.size() ); // Only use the widths read back if they are non-zero. // GTK returns the displayed width of the column, which is not calculated immediately if( widths[0] > 0 ) { size_t i = 0; for( const auto& [ colName, colPtr ] : m_colNameMap ) m_colWidths[ colName ] = widths[i++]; } auto colIt = m_colWidths.begin(); colIt->second += walk; colIt++; if( colIt != m_colWidths.end() ) colIt->second -= walk; for( const auto& [ colName, colPtr ] : m_colNameMap ) { if( colPtr == m_columns[0] ) continue; wxASSERT( m_colWidths.count( colName ) ); colPtr->SetWidth( m_colWidths[ colName ] ); } walk = -walk; } bool LIB_TREE_MODEL_ADAPTER::HasContainerColumns( const wxDataViewItem& aItem ) const { return IsContainer( aItem ); } bool LIB_TREE_MODEL_ADAPTER::IsContainer( const wxDataViewItem& aItem ) const { LIB_TREE_NODE* node = ToNode( aItem ); return node ? node->m_Children.size() : true; } wxDataViewItem LIB_TREE_MODEL_ADAPTER::GetParent( const wxDataViewItem& aItem ) const { if( m_freeze ) return ToItem( nullptr ); LIB_TREE_NODE* node = ToNode( aItem ); LIB_TREE_NODE* parent = node ? node->m_Parent : nullptr; // wxDataViewModel has no root node, but rather top-level elements have // an invalid (null) parent. if( !node || !parent || parent->m_Type == LIB_TREE_NODE::TYPE::ROOT ) return ToItem( nullptr ); else return ToItem( parent ); } void LIB_TREE_MODEL_ADAPTER::GetValue( wxVariant& aVariant, const wxDataViewItem& aItem, unsigned int aCol ) const { if( IsFrozen() ) { aVariant = wxEmptyString; return; } LIB_TREE_NODE* node = ToNode( aItem ); wxCHECK( node, /* void */ ); wxString valueStr; switch( aCol ) { case NAME_COL: if( node->m_Pinned ) valueStr = GetPinningSymbol() + UnescapeString( node->m_Name ); else valueStr = UnescapeString( node->m_Name ); break; default: if( m_colIdxMap.count( aCol ) ) { const wxString& key = m_colIdxMap.at( aCol ); if( node->m_Fields.count( key ) ) valueStr = UnescapeString( node->m_Fields.at( key ) ); else if( key == wxT( "Description" ) ) valueStr = UnescapeString( node->m_Desc ); else valueStr = wxEmptyString; } break; } valueStr.Replace( wxS( "\n" ), wxS( " " ) ); // Clear line breaks aVariant = valueStr; } bool LIB_TREE_MODEL_ADAPTER::GetAttr( const wxDataViewItem& aItem, unsigned int aCol, wxDataViewItemAttr& aAttr ) const { if( IsFrozen() ) return false; LIB_TREE_NODE* node = ToNode( aItem ); wxCHECK( node, false ); if( node->m_Type == LIB_TREE_NODE::ITEM ) { if( !node->m_IsRoot && aCol == 0 ) { // Names of non-root aliases are italicized aAttr.SetItalic( true ); return true; } } return false; } void recursiveDescent( LIB_TREE_NODE& aNode, const std::function& f ) { for( std::unique_ptr& node: aNode.m_Children ) { int r = f( node.get() ); if( r == 0 ) break; else if( r == -1 ) continue; recursiveDescent( *node, f ); } } const LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowResults() { const LIB_TREE_NODE* firstMatch = nullptr; // Expand parents of leaf nodes with some level of matching recursiveDescent( m_tree, [&]( const LIB_TREE_NODE* n ) { if( n->m_Type == LIB_TREE_NODE::TYPE::ITEM && n->m_Score > 1 ) { if( !firstMatch ) firstMatch = n; else if( n->m_Score > firstMatch->m_Score ) firstMatch = n; m_widget->ExpandAncestors( ToItem( n ) ); } return 1; // keep going to expand ancestors of all found items } ); // If no matches, find and show the preselect node if( !firstMatch && m_preselect_lib_id.IsValid() ) { recursiveDescent( m_tree, [&]( const LIB_TREE_NODE* n ) { // Don't match the recent and already placed libraries if( n->m_Name.StartsWith( "-- " ) ) return -1; // Skip this node and its children if( n->m_Type == LIB_TREE_NODE::ITEM && ( n->m_Children.empty() || !m_preselect_unit ) && m_preselect_lib_id == n->m_LibId ) { firstMatch = n; m_widget->ExpandAncestors( ToItem( n ) ); return 0; } else if( n->m_Type == LIB_TREE_NODE::UNIT && ( m_preselect_unit && m_preselect_unit == n->m_Unit ) && m_preselect_lib_id == n->m_Parent->m_LibId ) { firstMatch = n; m_widget->ExpandAncestors( ToItem( n ) ); return 0; } return 1; } ); } // If still no matches expand a single library if there is only one if( !firstMatch ) { int libraries = 0; for( const std::unique_ptr& child : m_tree.m_Children ) { if( !child->m_Name.StartsWith( "-- " ) ) libraries++; } if( libraries != 1 ) return nullptr; recursiveDescent( m_tree, [&]( const LIB_TREE_NODE* n ) { if( n->m_Type == LIB_TREE_NODE::TYPE::ITEM ) { firstMatch = n; m_widget->ExpandAncestors( ToItem( n ) ); return 0; } return 1; } ); } return firstMatch; }