/* * This program source code file is part of KiCad, a free EDA CAD application. * * 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 2 * 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, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include wxString PANEL_DESIGN_BLOCK_CHOOSER::g_designBlockSearchString; PANEL_DESIGN_BLOCK_CHOOSER::PANEL_DESIGN_BLOCK_CHOOSER( EDA_DRAW_FRAME* aFrame, DESIGN_BLOCK_PANE* aParent, std::vector& aHistoryList, std::function aSelectHandler, TOOL_INTERACTIVE* aContextMenuTool ) : wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize ), m_dbl_click_timer( nullptr ), m_open_libs_timer( nullptr ), m_vsplitter( nullptr ), m_tree( nullptr ), m_preview( nullptr ), m_parent( aParent ), m_frame( aFrame ), m_selectHandler( std::move( aSelectHandler ) ), m_historyList( aHistoryList ) { DESIGN_BLOCK_LIB_TABLE* libs = m_frame->Prj().DesignBlockLibs(); // Load design block files: auto* progressReporter = new WX_PROGRESS_REPORTER( aParent, _( "Load Design Block Libraries" ), 1, PR_CAN_ABORT ); DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( libs, nullptr, progressReporter ); // Force immediate deletion of the WX_PROGRESS_REPORTER. Do not use Destroy(), or use // Destroy() followed by wxSafeYield() because on Windows, APP_PROGRESS_DIALOG and // WX_PROGRESS_REPORTER have some side effects on the event loop manager. For instance, a // subsequent call to ShowModal() or ShowQuasiModal() for a dialog following the use of a // WX_PROGRESS_REPORTER results in incorrect modal or quasi modal behavior. delete progressReporter; if( DESIGN_BLOCK_LIB_TABLE::GetGlobalList().GetErrorCount() ) displayErrors( aFrame ); m_adapter = DESIGN_BLOCK_TREE_MODEL_ADAPTER::Create( m_frame, libs, m_frame->config()->m_DesignBlockChooserPanel.tree, aContextMenuTool ); // ------------------------------------------------------------------------------------- // Construct the actual panel // wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL ); m_vsplitter = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_LIVE_UPDATE | wxSP_NOBORDER | wxSP_3DSASH ); // Avoid the splitter window being assigned as the parent to additional windows. m_vsplitter->SetExtraStyle( wxWS_EX_TRANSIENT ); wxPanel* treePanel = new wxPanel( m_vsplitter ); wxBoxSizer* treeSizer = new wxBoxSizer( wxVERTICAL ); treePanel->SetSizer( treeSizer ); m_detailsPanel = new wxPanel( m_vsplitter ); m_detailsSizer = new wxBoxSizer( wxVERTICAL ); m_detailsPanel->SetSizer( m_detailsSizer ); m_detailsPanel->Layout(); m_detailsSizer->Fit( m_detailsPanel ); m_vsplitter->SetSashGravity( 0.5 ); m_vsplitter->SetMinimumPaneSize( 20 ); m_vsplitter->SplitHorizontally( treePanel, m_detailsPanel ); sizer->Add( m_vsplitter, 1, wxEXPAND, 5 ); m_tree = new LIB_TREE( treePanel, wxT( "design_blocks" ), libs, m_adapter, LIB_TREE::FLAGS::ALL_WIDGETS, nullptr ); treeSizer->Add( m_tree, 1, wxEXPAND, 5 ); treePanel->Layout(); treeSizer->Fit( treePanel ); RefreshLibs(); m_adapter->FinishTreeInitialization(); m_tree->SetSearchString( g_designBlockSearchString ); m_dbl_click_timer = new wxTimer( this ); m_open_libs_timer = new wxTimer( this ); SetSizer( sizer ); Layout(); Bind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer, this, m_dbl_click_timer->GetId() ); Bind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onOpenLibsTimer, this, m_open_libs_timer->GetId() ); Bind( EVT_LIBITEM_CHOSEN, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen, this ); Bind( wxEVT_CHAR_HOOK, &PANEL_DESIGN_BLOCK_CHOOSER::OnChar, this ); // Open the user's previously opened libraries on timer expiration. // This is done on a timer because we need a gross hack to keep GTK from garbling the // display. Must be longer than the search debounce timer. m_open_libs_timer->StartOnce( 300 ); } PANEL_DESIGN_BLOCK_CHOOSER::~PANEL_DESIGN_BLOCK_CHOOSER() { Unbind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer, this ); Unbind( EVT_LIBITEM_SELECTED, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected, this ); Unbind( EVT_LIBITEM_CHOSEN, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen, this ); Unbind( wxEVT_CHAR_HOOK, &PANEL_DESIGN_BLOCK_CHOOSER::OnChar, this ); // Stop the timer during destruction early to avoid potential race conditions (that do happen) m_dbl_click_timer->Stop(); m_open_libs_timer->Stop(); delete m_dbl_click_timer; delete m_open_libs_timer; } void PANEL_DESIGN_BLOCK_CHOOSER::SaveSettings() { g_designBlockSearchString = m_tree->GetSearchString(); if( APP_SETTINGS_BASE* cfg = m_frame->config() ) { // Save any changes to column widths, etc. m_adapter->SaveSettings(); cfg->m_DesignBlockChooserPanel.width = GetParent()->GetSize().x; cfg->m_DesignBlockChooserPanel.height = GetParent()->GetSize().y; cfg->m_DesignBlockChooserPanel.sash_pos_v = m_vsplitter->GetSashPosition(); cfg->m_DesignBlockChooserPanel.sort_mode = m_tree->GetSortMode(); } } void PANEL_DESIGN_BLOCK_CHOOSER::ShowChangedLanguage() { if( m_tree ) m_tree->ShowChangedLanguage(); } void PANEL_DESIGN_BLOCK_CHOOSER::SetPreviewWidget( DESIGN_BLOCK_PREVIEW_WIDGET* aPreview ) { m_preview = aPreview; m_detailsSizer->Add( m_preview, 1, wxEXPAND, 5 ); Layout(); } void PANEL_DESIGN_BLOCK_CHOOSER::OnChar( wxKeyEvent& aEvent ) { if( aEvent.GetKeyCode() == WXK_ESCAPE ) { wxObject* eventSource = aEvent.GetEventObject(); if( wxTextCtrl* textCtrl = dynamic_cast( eventSource ) ) { // First escape cancels search string value if( textCtrl->GetValue() == m_tree->GetSearchString() && !m_tree->GetSearchString().IsEmpty() ) { m_tree->SetSearchString( wxEmptyString ); return; } } } else { aEvent.Skip(); } } void PANEL_DESIGN_BLOCK_CHOOSER::FinishSetup() { if( APP_SETTINGS_BASE* cfg = m_frame->config() ) { auto horizPixelsFromDU = [&]( int x ) -> int { wxSize sz( x, 0 ); return GetParent()->ConvertDialogToPixels( sz ).x; }; APP_SETTINGS_BASE::PANEL_DESIGN_BLOCK_CHOOSER& panelCfg = cfg->m_DesignBlockChooserPanel; int w = panelCfg.width > 40 ? panelCfg.width : horizPixelsFromDU( 440 ); int h = panelCfg.height > 40 ? panelCfg.height : horizPixelsFromDU( 340 ); GetParent()->SetSize( wxSize( w, h ) ); GetParent()->Layout(); // We specify the width of the right window (m_design_block_view_panel), because specify // the width of the left window does not work as expected when SetSashGravity() is called if( panelCfg.sash_pos_h < 0 ) panelCfg.sash_pos_h = horizPixelsFromDU( 220 ); if( panelCfg.sash_pos_v < 0 ) panelCfg.sash_pos_v = horizPixelsFromDU( 230 ); if( m_vsplitter ) m_vsplitter->SetSashPosition( panelCfg.sash_pos_v ); m_adapter->SetSortMode( (LIB_TREE_MODEL_ADAPTER::SORT_MODE) panelCfg.sort_mode ); } } void PANEL_DESIGN_BLOCK_CHOOSER::RefreshLibs( bool aProgress ) { // Unselect before syncing to avoid null reference in the adapter // if a selected item is removed during the sync m_tree->Unselect(); DESIGN_BLOCK_TREE_MODEL_ADAPTER* adapter = static_cast( m_adapter.get() ); // Clear all existing libraries then re-add adapter->ClearLibraries(); // Read the libraries from disk if they've changed DESIGN_BLOCK_LIB_TABLE* fpTable = m_frame->Prj().DesignBlockLibs(); adapter->SetLibTable( fpTable ); // Sync FOOTPRINT_INFO list to the libraries on disk if( aProgress ) { WX_PROGRESS_REPORTER progressReporter( this, _( "Update Design Block Libraries" ), 2, PR_CAN_ABORT ); DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( fpTable, nullptr, &progressReporter ); progressReporter.Show( false ); } else { DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( fpTable, nullptr, nullptr ); } rebuildHistoryNode(); if( !m_historyList.empty() ) adapter->SetPreselectNode( m_historyList[0], 0 ); adapter->AddLibraries( m_frame ); m_tree->Regenerate( true ); } void PANEL_DESIGN_BLOCK_CHOOSER::SetPreselect( const LIB_ID& aPreselect ) { m_adapter->SetPreselectNode( aPreselect, 0 ); } LIB_ID PANEL_DESIGN_BLOCK_CHOOSER::GetSelectedLibId( int* aUnit ) const { return m_tree->GetSelectedLibId( aUnit ); } void PANEL_DESIGN_BLOCK_CHOOSER::SelectLibId( const LIB_ID& aLibId ) { m_tree->CenterLibId( aLibId ); m_tree->SelectLibId( aLibId ); } void PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer( wxTimerEvent& aEvent ) { // Hack because of eaten MouseUp event. See PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen // for the beginning of this spaghetti noodle. wxMouseState state = wxGetMouseState(); if( state.LeftIsDown() ) { // Mouse hasn't been raised yet, so fire the timer again. Otherwise the // purpose of this timer is defeated. m_dbl_click_timer->StartOnce( PANEL_DESIGN_BLOCK_CHOOSER::DBLCLICK_DELAY ); } else { m_frame->GetCanvas()->SetFocus(); m_selectHandler(); addDesignBlockToHistory( m_tree->GetSelectedLibId() ); } } void PANEL_DESIGN_BLOCK_CHOOSER::onOpenLibsTimer( wxTimerEvent& aEvent ) { if( APP_SETTINGS_BASE* cfg = m_frame->config() ) m_adapter->OpenLibs( cfg->m_LibTree.open_libs ); // Bind this now se we don't spam the event queue with EVT_LIBITEM_SELECTED events during // the initial load. Bind( EVT_LIBITEM_SELECTED, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected, this ); } void PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected( wxCommandEvent& aEvent ) { if( GetSelectedLibId().IsValid() ) { std::unique_ptr designBlock( m_parent->GetDesignBlock( GetSelectedLibId(), true, true ) ); m_preview->DisplayDesignBlock( designBlock.get() ); } } void PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen( wxCommandEvent& aEvent ) { if( m_tree->GetSelectedLibId().IsValid() ) { // Got a selection. We can't just end the modal dialog here, because wx leaks some events // back to the parent window (in particular, the MouseUp following a double click). // // NOW, here's where it gets really fun. wxTreeListCtrl eats MouseUp. This isn't really // feasible to bypass without a fully custom wxDataViewCtrl implementation, and even then // might not be fully possible (docs are vague). To get around this, we use a one-shot // timer to schedule the dialog close. // // See PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer for the other end of this spaghetti noodle. m_dbl_click_timer->StartOnce( PANEL_DESIGN_BLOCK_CHOOSER::DBLCLICK_DELAY ); } } void PANEL_DESIGN_BLOCK_CHOOSER::addDesignBlockToHistory( const LIB_ID& aLibId ) { LIB_ID savedId = GetSelectedLibId(); m_tree->Unselect(); // Remove duplicates for( int i = (int) m_historyList.size() - 1; i >= 0; --i ) { if( m_historyList[i] == aLibId ) m_historyList.erase( m_historyList.begin() + i ); } // Add the new name at the beginning of the history list m_historyList.insert( m_historyList.begin(), aLibId ); // Remove extra names while( m_historyList.size() >= 8 ) m_historyList.pop_back(); rebuildHistoryNode(); m_tree->Regenerate( true ); SelectLibId( savedId ); } void PANEL_DESIGN_BLOCK_CHOOSER::rebuildHistoryNode() { m_adapter->RemoveGroup( true, false ); // Build the history list std::vector historyInfos; for( const LIB_ID& lib : m_historyList ) { LIB_TREE_ITEM* fp_info = DESIGN_BLOCK_LIB_TABLE::GetGlobalList().GetDesignBlockInfo( lib.GetLibNickname(), lib.GetLibItemName() ); // this can be null, for example, if the design block has been deleted from a library. if( fp_info != nullptr ) historyInfos.push_back( fp_info ); } m_adapter->DoAddLibrary( wxT( "-- " ) + _( "Recently Used" ) + wxT( " --" ), wxEmptyString, historyInfos, false, true ) .m_IsRecentlyUsedGroup = true; } void PANEL_DESIGN_BLOCK_CHOOSER::displayErrors( wxTopLevelWindow* aWindow ) { // @todo: go to a more HTML !! ? centric output, possibly with recommendations // for remedy of errors. Add numeric error codes to PARSE_ERROR, and switch on them for // remedies, etc. Full access is provided to everything in every exception! HTML_MESSAGE_BOX dlg( aWindow, _( "Load Error" ) ); dlg.MessageSet( _( "Errors were encountered loading design blocks:" ) ); wxString msg; while( std::unique_ptr error = DESIGN_BLOCK_LIB_TABLE::GetGlobalList().PopError() ) { wxString tmp = EscapeHTML( error->Problem() ); // Preserve new lines in error messages so queued errors don't run together. tmp.Replace( wxS( "\n" ), wxS( "
" ) ); msg += wxT( "

" ) + tmp + wxT( "

" ); } dlg.AddHTML_Text( msg ); dlg.ShowModal(); }