/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2021 Andrew Lutsenko, anlutsenko at gmail dot com * Copyright The 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 "panel_packages_view.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GRID_CELL_MARGIN 4 std::unordered_map PANEL_PACKAGES_VIEW::STATUS_ENUM_TO_STR = { { PVS_INVALID, wxS( "invalid" ) }, { PVS_STABLE, wxS( "stable" ) }, { PVS_TESTING, wxS( "testing" ) }, { PVS_DEVELOPMENT, wxS( "development" ) }, { PVS_DEPRECATED, wxS( "deprecated" ) } }; PANEL_PACKAGES_VIEW::PANEL_PACKAGES_VIEW( wxWindow* parent, std::shared_ptr aPcm, const ActionCallback& aActionCallback, const PinCallback& aPinCallback ) : PANEL_PACKAGES_VIEW_BASE( parent ), m_actionCallback( aActionCallback ), m_pinCallback( aPinCallback ), m_pcm( aPcm ) { // Replace wxFormBuilder's sash initializer with one which will respect m_initialSashPos. m_splitter1->Disconnect( wxEVT_IDLE, wxIdleEventHandler( PANEL_PACKAGES_VIEW_BASE::m_splitter1OnIdle ), NULL, this ); m_splitter1->Connect( wxEVT_IDLE, wxIdleEventHandler( PANEL_PACKAGES_VIEW::SetSashOnIdle ), NULL, this ); m_splitter1->SetPaneMinimums( FromDIP( 350 ), FromDIP( 450 ) ); #ifdef __WXGTK__ // wxSearchCtrl vertical height is not calculated correctly on some GTK setups // See https://gitlab.com/kicad/code/kicad/-/issues/9019 m_searchCtrl->SetMinSize( wxSize( -1, GetTextExtent( wxT( "qb" ) ).y + 10 ) ); #endif m_searchCtrl->Bind( wxEVT_TEXT, &PANEL_PACKAGES_VIEW::OnSearchTextChanged, this ); m_searchCtrl->SetDescriptiveText( _( "Filter" ) ); m_panelList->SetBorders( false, true, false, false ); m_gridVersions->PushEventHandler( new GRID_TRICKS( m_gridVersions ) ); for( int col = 0; col < m_gridVersions->GetNumberCols(); col++ ) { const wxString& heading = m_gridVersions->GetColLabelValue( col ); int headingWidth = GetTextExtent( heading ).x + 2 * GRID_CELL_MARGIN; // Set the minimal width to the column label size. m_gridVersions->SetColMinimalWidth( col, headingWidth ); m_gridVersions->SetColSize( col, m_gridVersions->GetVisibleWidth( col ) ); } // Most likely should be changed to wxGridSelectNone once WxWidgets>=3.1.5 is mandatory. m_gridVersions->SetSelectionMode( WX_GRID::wxGridSelectRows ); wxColor background = wxStaticText::GetClassDefaultAttributes().colBg; m_panelList->SetBackgroundColour( background ); m_packageListWindow->SetBackgroundColour( background ); m_infoScrollWindow->SetBackgroundColour( background ); m_infoScrollWindow->EnableScrolling( false, true ); ClearData(); } PANEL_PACKAGES_VIEW::~PANEL_PACKAGES_VIEW() { m_splitter1->Disconnect( wxEVT_IDLE, wxIdleEventHandler( PANEL_PACKAGES_VIEW::SetSashOnIdle ), NULL, this ); COMMON_SETTINGS* cfg = Pgm().GetCommonSettings(); cfg->m_PackageManager.sash_pos = m_splitter1->GetSashPosition(); m_gridVersions->PopEventHandler( true ); } void PANEL_PACKAGES_VIEW::ClearData() { unsetPackageDetails(); m_currentSelected = nullptr; m_updateablePackages.clear(); m_packagePanels.clear(); m_packageInitialOrder.clear(); m_packageListWindow->GetSizer()->Clear( true ); // Delete panels m_packageListWindow->GetSizer()->FitInside( m_packageListWindow ); m_packageListWindow->Layout(); } void PANEL_PACKAGES_VIEW::SetData( const std::vector& aPackageData ) { ClearData(); for( const PACKAGE_VIEW_DATA& data : aPackageData ) { PANEL_PACKAGE* package_panel = new PANEL_PACKAGE( m_packageListWindow, m_actionCallback, m_pinCallback, data ); package_panel->SetSelectCallback( [package_panel, this] () { if( m_currentSelected && m_currentSelected != package_panel ) m_currentSelected->SetSelected( false ); package_panel->SetSelected( true ); m_currentSelected = package_panel; setPackageDetails( package_panel->GetPackageData() ); Layout(); } ); m_packagePanels.insert( { data.package.identifier, package_panel } ); m_packageInitialOrder.push_back( data.package.identifier ); if( data.state == PPS_UPDATE_AVAILABLE && !data.pinned ) m_updateablePackages.insert( data.package.identifier ); } updatePackageList(); updateCommonState(); } void PANEL_PACKAGES_VIEW::setPackageDetails( const PACKAGE_VIEW_DATA& aPackageData ) { const PCM_PACKAGE& package = aPackageData.package; // Details wxString details; details << wxT( "
" ) + package.name + wxT( "
" ); auto format_desc = []( const wxString& text ) -> wxString { wxString result; bool inURL = false; wxString url; for( unsigned i = 0; i < text.length(); ++i ) { wxUniChar c = text[i]; if( inURL ) { if( c == ' ' || c == '\n') { result += wxString::Format( wxT( "%s" ), url, url ); inURL = false; if( c == '\n' ) result += wxT( "

" ); else result += c; } else { url += c; } } else if( text.Mid( i, 5 ) == wxT( "http:" ) || text.Mid( i, 6 ) == wxT( "https:" ) ) { url = c; inURL = true; } else if( c == '\n' ) { result += wxT( "

" ); } else { result += c; } } if( inURL ) result += wxString::Format( wxT( "%s" ), url, url ); return result; }; wxString desc = package.description_full; details << wxT( "

" ) + format_desc( desc ) + wxT( "

" ); details << wxT( "

" ) + _( "Metadata" ) + wxT( "

" ); details << wxT( "
    " ); details << wxT( "
  • " ) + _( "Package identifier: " ) + package.identifier + wxT( "
  • " ); details << wxT( "
  • " ) + _( "License: " ) + package.license + wxT( "
  • " ); if( package.tags.size() > 0 ) { wxString tags_str; for( const std::string& tag : package.tags ) { if( !tags_str.IsEmpty() ) tags_str += ", "; tags_str += tag; } details << wxT( "
  • " ) + _( "Tags: " ) + tags_str + wxT( "
  • " ); } auto format_entry = []( const std::pair& entry ) -> wxString { wxString name = entry.first; wxString url = EscapeHTML( entry.second ); if( name == wxT( "email" ) ) return wxString::Format( wxT( "%s" ), url, url ); else if( url.StartsWith( wxT( "http:" ) ) || url.StartsWith( wxT( "https:" ) ) ) return wxString::Format( wxT( "%s" ), url, url ); else return entry.second; }; auto write_contact = [&]( const wxString& type, const PCM_CONTACT& contact ) { details << wxT( "
  • " ) + type + wxT( ": " ) + contact.name + wxT( "
      " ); for( const std::pair& entry : contact.contact ) { details << wxT( "
    • " ); details << entry.first << wxT( ": " ) + format_entry( entry ); details << wxT( "
    • " ); } details << wxT( "
    " ); }; write_contact( _( "Author" ), package.author ); if( package.maintainer ) write_contact( _( "Maintainer" ), *package.maintainer ); if( package.resources.size() > 0 ) { details << wxT( "
  • " ) + _( "Resources" ) + wxT( "
      " ); for( const std::pair& entry : package.resources ) { details << wxT( "
    • " ); details << entry.first << wxT( ": " ); details << format_entry( entry ) + wxT( "
    • " ); } details << wxT( "
    " ); } details << wxT( "
" ); m_infoText->SetPage( details ); wxSizeEvent dummy; OnSizeInfoBox( dummy ); // Versions table m_gridVersions->Freeze(); m_gridVersions->ClearRows(); int row = 0; wxString current_version; if( aPackageData.state == PPS_INSTALLED || aPackageData.state == PPS_UPDATE_AVAILABLE ) current_version = m_pcm->GetInstalledPackageVersion( package.identifier ); wxFont bold_font = m_gridVersions->GetDefaultCellFont().Bold(); for( const PACKAGE_VERSION& version : package.versions ) { if( !version.compatible && !m_showAllVersions->IsChecked() ) continue; m_gridVersions->InsertRows( row ); m_gridVersions->SetCellValue( row, COL_VERSION, version.version ); m_gridVersions->SetCellValue( row, COL_DOWNLOAD_SIZE, toHumanReadableSize( version.download_size ) ); m_gridVersions->SetCellValue( row, COL_INSTALL_SIZE, toHumanReadableSize( version.install_size ) ); m_gridVersions->SetCellValue( row, COL_COMPATIBILITY, version.compatible ? wxT( "\u2714" ) : wxEmptyString ); m_gridVersions->SetCellValue( row, COL_STATUS, STATUS_ENUM_TO_STR.at( version.status ) ); m_gridVersions->SetCellAlignment( row, COL_COMPATIBILITY, wxALIGN_CENTER, wxALIGN_CENTER ); if( current_version == version.version ) { for( int col = 0; col < m_gridVersions->GetNumberCols(); col++ ) m_gridVersions->SetCellFont( row, col, bold_font ); } row++; } for( int col = 0; col < m_gridVersions->GetNumberCols(); col++ ) { // Set the width to see the full contents m_gridVersions->SetColSize( col, m_gridVersions->GetVisibleWidth( col ) ); } // Autoselect preferred or installed version if( m_gridVersions->GetNumberRows() >= 1 ) { wxString version = m_currentSelected->GetPackageData().current_version; if( version.IsEmpty() ) version = m_currentSelected->GetPreferredVersion(); if( !version.IsEmpty() ) { for( int i = 0; i < m_gridVersions->GetNumberRows(); i++ ) { if( m_gridVersions->GetCellValue( i, COL_VERSION ) == version ) { m_gridVersions->SelectRow( i ); m_gridVersions->SetGridCursor( i, COL_VERSION ); break; } } } else { // Fall back to first row. m_gridVersions->SelectRow( 0 ); } } m_gridVersions->Thaw(); updateDetailsButtons(); m_infoText->Show( true ); m_sizerVersions->Show( true ); m_sizerVersions->Layout(); wxSize size = m_infoScrollWindow->GetTargetWindow()->GetBestVirtualSize(); m_infoScrollWindow->SetVirtualSize( size ); } void PANEL_PACKAGES_VIEW::unsetPackageDetails() { m_infoText->SetPage( wxEmptyString ); m_infoText->Show( false ); m_sizerVersions->Show( false ); wxSize size = m_infoScrollWindow->GetTargetWindow()->GetBestVirtualSize(); m_infoScrollWindow->SetVirtualSize( size ); // Clean up grid just so we don't keep stale info around (it's already been hidden). m_gridVersions->Freeze(); m_gridVersions->ClearRows(); m_gridVersions->Thaw(); } wxString PANEL_PACKAGES_VIEW::toHumanReadableSize( const std::optional size ) const { if( !size ) return wxT( "-" ); uint64_t b = *size; if( b >= 1024 * 1024 ) return wxString::Format( wxT( "%.1f MB" ), b / 1000.0 / 1000.0 ); if( b >= 1024 ) return wxString::Format( wxT( "%lld kB" ), b / 1000 ); return wxString::Format( wxT( "%lld B" ), b ); } bool PANEL_PACKAGES_VIEW::canDownload() const { if( !m_currentSelected ) return false; return m_gridVersions->GetNumberRows() == 1 || m_gridVersions->GetSelectedRows().size() == 1; } bool PANEL_PACKAGES_VIEW::canRunAction() const { if( !m_currentSelected ) return false; const PACKAGE_VIEW_DATA& packageData = m_currentSelected->GetPackageData(); switch( packageData.state ) { case PPS_PENDING_INSTALL: case PPS_PENDING_UNINSTALL: case PPS_PENDING_UPDATE: return false; default: break; } return m_gridVersions->GetNumberRows() == 1 || m_gridVersions->GetSelectedRows().size() == 1; } void PANEL_PACKAGES_VIEW::SetPackageState( const wxString& aPackageId, const PCM_PACKAGE_STATE aState, const bool aPinned ) { auto it = m_packagePanels.find( aPackageId ); if( it != m_packagePanels.end() ) { it->second->SetState( aState, aPinned ); if( m_currentSelected && m_currentSelected == it->second ) { wxMouseEvent dummy; m_currentSelected->OnClick( dummy ); } if( aState == PPS_UPDATE_AVAILABLE && !aPinned ) m_updateablePackages.insert( aPackageId ); else m_updateablePackages.erase( aPackageId ); updateCommonState(); } } void PANEL_PACKAGES_VIEW::OnVersionsCellClicked( wxGridEvent& event ) { m_gridVersions->ClearSelection(); m_gridVersions->SelectRow( event.GetRow() ); updateDetailsButtons(); } void PANEL_PACKAGES_VIEW::OnDownloadVersionClicked( wxCommandEvent& event ) { if( !canDownload() ) { wxBell(); return; } if( m_gridVersions->GetNumberRows() == 1 ) m_gridVersions->SelectRow( 0 ); const wxArrayInt selectedRows = m_gridVersions->GetSelectedRows(); wxString version = m_gridVersions->GetCellValue( selectedRows[0], COL_VERSION ); const PCM_PACKAGE& package = m_currentSelected->GetPackageData().package; auto ver_it = std::find_if( package.versions.begin(), package.versions.end(), [&]( const PACKAGE_VERSION& ver ) { return ver.version == version; } ); wxASSERT_MSG( ver_it != package.versions.end(), "Could not find package version" ); if( !ver_it->download_url ) { wxMessageBox( _( "Package download url is not specified" ), _( "Error downloading package" ), wxICON_INFORMATION | wxOK, wxGetTopLevelParent( this ) ); return; } const wxString& url = *ver_it->download_url; KICAD_SETTINGS* cfg = GetAppSettings( "kicad" ); wxWindow* topLevelParent = wxGetTopLevelParent( this ); wxFileDialog dialog( topLevelParent, _( "Save Package" ), cfg->m_PcmLastDownloadDir, wxString::Format( wxT( "%s_v%s.zip" ), package.identifier, version ), wxT( "ZIP files (*.zip)|*.zip" ), wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); if( dialog.ShowModal() == wxID_CANCEL ) return; wxString path = dialog.GetPath(); cfg->m_PcmLastDownloadDir = wxPathOnly( path ); std::ofstream output( path.ToUTF8(), std::ios_base::binary ); WX_PROGRESS_REPORTER reporter( this, _( "Download Package" ), 1, PR_CAN_ABORT ); bool success = m_pcm->DownloadToStream( url, &output, &reporter, 0 ); output.close(); if( success ) { if( ver_it->download_sha256 ) { std::ifstream stream( path.ToUTF8(), std::ios_base::binary ); bool matches = m_pcm->VerifyHash( stream, *ver_it->download_sha256 ); stream.close(); if( !matches && wxMessageBox( _( "Integrity of the downloaded package could not be verified, hash " "does not match. Are you sure you want to keep this file?" ), _( "Keep downloaded file" ), wxICON_EXCLAMATION | wxYES_NO, wxGetTopLevelParent( this ) ) == wxNO ) { wxRemoveFile( path ); } } } else { if( wxFileExists( path ) ) wxRemoveFile( path ); } } void PANEL_PACKAGES_VIEW::OnVersionActionClicked( wxCommandEvent& event ) { if( !canRunAction() ) { wxBell(); return; } PCM_PACKAGE_ACTION action = getAction(); if( action == PPA_UNINSTALL ) { m_actionCallback( m_currentSelected->GetPackageData(), PPA_UNINSTALL, m_currentSelected->GetPackageData().current_version ); return; } if( m_gridVersions->GetNumberRows() == 1 ) m_gridVersions->SelectRow( 0 ); const wxArrayInt selectedRows = m_gridVersions->GetSelectedRows(); wxString version = m_gridVersions->GetCellValue( selectedRows[0], COL_VERSION ); const PCM_PACKAGE& package = m_currentSelected->GetPackageData().package; auto ver_it = std::find_if( package.versions.begin(), package.versions.end(), [&]( const PACKAGE_VERSION& ver ) { return ver.version == version; } ); wxCHECK_RET( ver_it != package.versions.end(), "Could not find package version" ); if( !ver_it->compatible && wxMessageBox( _( "This package version is incompatible with your KiCad version or " "platform. Are you sure you want to install it anyway?" ), _( "Install package" ), wxICON_EXCLAMATION | wxYES_NO, wxGetTopLevelParent( this ) ) == wxNO ) { return; } m_actionCallback( m_currentSelected->GetPackageData(), action, version ); } void PANEL_PACKAGES_VIEW::OnShowAllVersionsClicked( wxCommandEvent& event ) { if( m_currentSelected ) { wxMouseEvent dummy; m_currentSelected->OnClick( dummy ); } updateDetailsButtons(); } void PANEL_PACKAGES_VIEW::OnSearchTextChanged( wxCommandEvent& event ) { unsetPackageDetails(); if( m_currentSelected ) m_currentSelected->SetSelected( false ); m_currentSelected = nullptr; updatePackageList(); } void PANEL_PACKAGES_VIEW::updatePackageList() { // Sort by descending rank, ascending index std::vector> package_ranks; const wxString search_term = m_searchCtrl->GetValue().Trim(); for( size_t index = 0; index < m_packageInitialOrder.size(); index++ ) { int rank = 1; const PCM_PACKAGE& pkg = m_packagePanels[m_packageInitialOrder[index]]->GetPackageData().package; if( search_term.size() > 2 ) rank = m_pcm->GetPackageSearchRank( pkg, search_term ); // Packages with no versions are delisted and should not be shown if( pkg.versions.size() == 0 ) rank = 0; package_ranks.emplace_back( rank, index ); } std::sort( package_ranks.begin(), package_ranks.end(), []( const std::pair& a, const std::pair& b ) { return a.first > b.first || ( a.first == b.first && a.second < b.second ); } ); // Rearrange panels, hide ones with 0 rank wxSizer* sizer = m_packageListWindow->GetSizer(); sizer->Clear( false ); // Don't delete panels for( const std::pair& pair : package_ranks ) { PANEL_PACKAGE* panel = m_packagePanels[m_packageInitialOrder[pair.second]]; if( pair.first > 0 ) { sizer->Add( panel, 0, wxEXPAND ); panel->Show(); if( !m_currentSelected ) { wxMouseEvent dummy; panel->OnClick( dummy ); } } else { panel->Hide(); } } m_packageListWindow->FitInside(); m_packageListWindow->SetScrollRate( 0, 15 ); m_packageListWindow->SendSizeEvent( wxSEND_EVENT_POST ); } void PANEL_PACKAGES_VIEW::updateDetailsButtons() { m_buttonDownload->Enable( canDownload() ); if( canRunAction() ) { m_buttonAction->Enable(); PCM_PACKAGE_ACTION action = getAction(); switch( action ) { case PPA_INSTALL: m_buttonAction->SetLabel( _( "Install" ) ); break; case PPA_UNINSTALL: m_buttonAction->SetLabel( _( "Uninstall" ) ); break; case PPA_UPDATE: m_buttonAction->SetLabel( _( "Update" ) ); break; } } else { m_buttonAction->Disable(); m_buttonAction->SetLabel( _( "Pending" ) ); } } PCM_PACKAGE_ACTION PANEL_PACKAGES_VIEW::getAction() const { wxASSERT_MSG( m_gridVersions->GetNumberRows() == 1 || m_gridVersions->GetSelectedRows().size() == 1, wxT( "getAction() called with ambiguous version selection" ) ); int selected_row = 0; if( m_gridVersions->GetSelectedRows().size() == 1 ) selected_row = m_gridVersions->GetSelectedRows()[0]; wxString version = m_gridVersions->GetCellValue( selected_row, COL_VERSION ); const PACKAGE_VIEW_DATA& package = m_currentSelected->GetPackageData(); switch( package.state ) { case PPS_AVAILABLE: case PPS_UNAVAILABLE: return PPA_INSTALL; // Only action for not installed package is to install it case PPS_INSTALLED: case PPS_UPDATE_AVAILABLE: if( version == package.current_version ) return PPA_UNINSTALL; else return PPA_UPDATE; default: return PPA_INSTALL; // For pending states return value does not matter as button will be disabled } } void PANEL_PACKAGES_VIEW::OnSizeInfoBox( wxSizeEvent& aEvent ) { wxSize infoSize = KIPLATFORM::UI::GetUnobscuredSize( m_infoText->GetParent() ); infoSize.x -= 10; m_infoText->SetMinSize( infoSize ); m_infoText->SetMaxSize( infoSize ); m_infoText->SetSize( infoSize ); m_infoText->Layout(); infoSize.y = m_infoText->GetInternalRepresentation()->GetHeight() + 12; m_infoText->SetMinSize( infoSize ); m_infoText->SetMaxSize( infoSize ); m_infoText->SetSize( infoSize ); m_infoText->Layout(); Refresh(); } void PANEL_PACKAGES_VIEW::OnURLClicked( wxHtmlLinkEvent& aEvent ) { const wxHtmlLinkInfo& info = aEvent.GetLinkInfo(); ::wxLaunchDefaultBrowser( info.GetHref() ); } void PANEL_PACKAGES_VIEW::OnInfoMouseWheel( wxMouseEvent& event ) { // Transfer scrolling from the info window to its parent scroll window m_infoScrollWindow->HandleOnMouseWheel( event ); } void PANEL_PACKAGES_VIEW::SetSashOnIdle( wxIdleEvent& aEvent ) { COMMON_SETTINGS* cfg = Pgm().GetCommonSettings(); m_splitter1->SetSashPosition( cfg->m_PackageManager.sash_pos ); m_packageListWindow->FitInside(); m_splitter1->Disconnect( wxEVT_IDLE, wxIdleEventHandler( PANEL_PACKAGES_VIEW::SetSashOnIdle ), nullptr, this ); } void PANEL_PACKAGES_VIEW::updateCommonState() { m_buttonUpdateAll->Enable( m_updateablePackages.size() > 0 ); } void PANEL_PACKAGES_VIEW::OnUpdateAllClicked( wxCommandEvent& event ) { // The map will be modified by the callback so we copy the list here std::vector packages; std::copy( m_updateablePackages.begin(), m_updateablePackages.end(), std::back_inserter( packages ) ); for( const wxString& pkg_id : packages ) { auto it = m_packagePanels.find( pkg_id ); if( it != m_packagePanels.end() ) { const PACKAGE_VIEW_DATA& data = it->second->GetPackageData(); m_actionCallback( data, PPA_UPDATE, data.update_version ); } } }