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.

446 lines
15 KiB

11 months ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public License
  8. * as published by the Free Software Foundation; either version 2
  9. * of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, you may find one here:
  18. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  19. * or you may search the http://www.gnu.org website for the version 2 license,
  20. * or you may write to the Free Software Foundation, Inc.,
  21. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  22. */
  23. #include <pgm_base.h>
  24. #include <design_block.h>
  25. #include <design_block_pane.h>
  26. #include <design_block_lib_table.h>
  27. #include <panel_design_block_chooser.h>
  28. #include <design_block_preview_widget.h>
  29. #include <kiface_base.h>
  30. #include <kiway_holder.h>
  31. #include <eda_draw_frame.h>
  32. #include <widgets/lib_tree.h>
  33. #include <settings/settings_manager.h>
  34. #include <project/project_file.h>
  35. #include <dialogs/html_message_box.h>
  36. #include <settings/app_settings.h>
  37. #include <string_utils.h>
  38. #include <wx/log.h>
  39. #include <wx/panel.h>
  40. #include <wx/sizer.h>
  41. #include <wx/splitter.h>
  42. #include <wx/timer.h>
  43. #include <wx/wxhtml.h>
  44. #include <wx/msgdlg.h>
  45. #include <widgets/wx_progress_reporters.h>
  46. wxString PANEL_DESIGN_BLOCK_CHOOSER::g_designBlockSearchString;
  47. PANEL_DESIGN_BLOCK_CHOOSER::PANEL_DESIGN_BLOCK_CHOOSER( EDA_DRAW_FRAME* aFrame, DESIGN_BLOCK_PANE* aParent,
  48. std::vector<LIB_ID>& aHistoryList,
  49. std::function<void()> aSelectHandler,
  50. TOOL_INTERACTIVE* aContextMenuTool ) :
  51. wxPanel( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize ),
  52. m_dbl_click_timer( nullptr ),
  53. m_open_libs_timer( nullptr ),
  54. m_vsplitter( nullptr ),
  55. m_tree( nullptr ),
  56. m_preview( nullptr ),
  57. m_parent( aParent ),
  58. m_frame( aFrame ),
  59. m_selectHandler( std::move( aSelectHandler ) ),
  60. m_historyList( aHistoryList )
  61. {
  62. DESIGN_BLOCK_LIB_TABLE* libs = m_frame->Prj().DesignBlockLibs();
  63. // Load design block files:
  64. auto* progressReporter = new WX_PROGRESS_REPORTER( aParent, _( "Load Design Block Libraries" ), 1,
  65. PR_CAN_ABORT );
  66. DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( libs, nullptr, progressReporter );
  67. // Force immediate deletion of the WX_PROGRESS_REPORTER. Do not use Destroy(), or use
  68. // Destroy() followed by wxSafeYield() because on Windows, APP_PROGRESS_DIALOG and
  69. // WX_PROGRESS_REPORTER have some side effects on the event loop manager. For instance, a
  70. // subsequent call to ShowModal() or ShowQuasiModal() for a dialog following the use of a
  71. // WX_PROGRESS_REPORTER results in incorrect modal or quasi modal behavior.
  72. delete progressReporter;
  73. if( DESIGN_BLOCK_LIB_TABLE::GetGlobalList().GetErrorCount() )
  74. displayErrors( aFrame );
  75. m_adapter = DESIGN_BLOCK_TREE_MODEL_ADAPTER::Create(
  76. m_frame, libs, m_frame->config()->m_DesignBlockChooserPanel.tree, aContextMenuTool );
  77. // -------------------------------------------------------------------------------------
  78. // Construct the actual panel
  79. //
  80. wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
  81. m_vsplitter = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
  82. wxSP_LIVE_UPDATE | wxSP_NOBORDER | wxSP_3DSASH );
  83. // Avoid the splitter window being assigned as the parent to additional windows.
  84. m_vsplitter->SetExtraStyle( wxWS_EX_TRANSIENT );
  85. wxPanel* treePanel = new wxPanel( m_vsplitter );
  86. wxBoxSizer* treeSizer = new wxBoxSizer( wxVERTICAL );
  87. treePanel->SetSizer( treeSizer );
  88. m_detailsPanel = new wxPanel( m_vsplitter );
  89. m_detailsSizer = new wxBoxSizer( wxVERTICAL );
  90. m_detailsPanel->SetSizer( m_detailsSizer );
  91. m_detailsPanel->Layout();
  92. m_detailsSizer->Fit( m_detailsPanel );
  93. m_vsplitter->SetSashGravity( 0.5 );
  94. m_vsplitter->SetMinimumPaneSize( 20 );
  95. m_vsplitter->SplitHorizontally( treePanel, m_detailsPanel );
  96. sizer->Add( m_vsplitter, 1, wxEXPAND, 5 );
  97. m_tree = new LIB_TREE( treePanel, wxT( "design_blocks" ), libs, m_adapter, LIB_TREE::FLAGS::ALL_WIDGETS, nullptr );
  98. treeSizer->Add( m_tree, 1, wxEXPAND, 5 );
  99. treePanel->Layout();
  100. treeSizer->Fit( treePanel );
  101. RefreshLibs();
  102. m_adapter->FinishTreeInitialization();
  103. m_tree->SetSearchString( g_designBlockSearchString );
  104. m_dbl_click_timer = new wxTimer( this );
  105. m_open_libs_timer = new wxTimer( this );
  106. SetSizer( sizer );
  107. Layout();
  108. Bind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer, this, m_dbl_click_timer->GetId() );
  109. Bind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onOpenLibsTimer, this, m_open_libs_timer->GetId() );
  110. Bind( EVT_LIBITEM_CHOSEN, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen, this );
  111. Bind( wxEVT_CHAR_HOOK, &PANEL_DESIGN_BLOCK_CHOOSER::OnChar, this );
  112. // Open the user's previously opened libraries on timer expiration.
  113. // This is done on a timer because we need a gross hack to keep GTK from garbling the
  114. // display. Must be longer than the search debounce timer.
  115. m_open_libs_timer->StartOnce( 300 );
  116. }
  117. PANEL_DESIGN_BLOCK_CHOOSER::~PANEL_DESIGN_BLOCK_CHOOSER()
  118. {
  119. Unbind( wxEVT_TIMER, &PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer, this );
  120. Unbind( EVT_LIBITEM_SELECTED, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected, this );
  121. Unbind( EVT_LIBITEM_CHOSEN, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen, this );
  122. Unbind( wxEVT_CHAR_HOOK, &PANEL_DESIGN_BLOCK_CHOOSER::OnChar, this );
  123. // Stop the timer during destruction early to avoid potential race conditions (that do happen)
  124. m_dbl_click_timer->Stop();
  125. m_open_libs_timer->Stop();
  126. delete m_dbl_click_timer;
  127. delete m_open_libs_timer;
  128. }
  129. void PANEL_DESIGN_BLOCK_CHOOSER::SaveSettings()
  130. {
  131. g_designBlockSearchString = m_tree->GetSearchString();
  132. if( APP_SETTINGS_BASE* cfg = m_frame->config() )
  133. {
  134. // Save any changes to column widths, etc.
  135. m_adapter->SaveSettings();
  136. cfg->m_DesignBlockChooserPanel.width = GetParent()->GetSize().x;
  137. cfg->m_DesignBlockChooserPanel.height = GetParent()->GetSize().y;
  138. cfg->m_DesignBlockChooserPanel.sash_pos_v = m_vsplitter->GetSashPosition();
  139. cfg->m_DesignBlockChooserPanel.sort_mode = m_tree->GetSortMode();
  140. }
  141. }
  142. void PANEL_DESIGN_BLOCK_CHOOSER::ShowChangedLanguage()
  143. {
  144. if( m_tree )
  145. m_tree->ShowChangedLanguage();
  146. }
  147. void PANEL_DESIGN_BLOCK_CHOOSER::SetPreviewWidget( DESIGN_BLOCK_PREVIEW_WIDGET* aPreview )
  148. {
  149. m_preview = aPreview;
  150. m_detailsSizer->Add( m_preview, 1, wxEXPAND, 5 );
  151. Layout();
  152. }
  153. void PANEL_DESIGN_BLOCK_CHOOSER::OnChar( wxKeyEvent& aEvent )
  154. {
  155. if( aEvent.GetKeyCode() == WXK_ESCAPE )
  156. {
  157. wxObject* eventSource = aEvent.GetEventObject();
  158. if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( eventSource ) )
  159. {
  160. // First escape cancels search string value
  161. if( textCtrl->GetValue() == m_tree->GetSearchString() && !m_tree->GetSearchString().IsEmpty() )
  162. {
  163. m_tree->SetSearchString( wxEmptyString );
  164. return;
  165. }
  166. }
  167. }
  168. else
  169. {
  170. aEvent.Skip();
  171. }
  172. }
  173. void PANEL_DESIGN_BLOCK_CHOOSER::FinishSetup()
  174. {
  175. if( APP_SETTINGS_BASE* cfg = m_frame->config() )
  176. {
  177. auto horizPixelsFromDU =
  178. [&]( int x ) -> int
  179. {
  180. wxSize sz( x, 0 );
  181. return GetParent()->ConvertDialogToPixels( sz ).x;
  182. };
  183. APP_SETTINGS_BASE::PANEL_DESIGN_BLOCK_CHOOSER& panelCfg = cfg->m_DesignBlockChooserPanel;
  184. int w = panelCfg.width > 40 ? panelCfg.width : horizPixelsFromDU( 440 );
  185. int h = panelCfg.height > 40 ? panelCfg.height : horizPixelsFromDU( 340 );
  186. GetParent()->SetSize( wxSize( w, h ) );
  187. GetParent()->Layout();
  188. // We specify the width of the right window (m_design_block_view_panel), because specify
  189. // the width of the left window does not work as expected when SetSashGravity() is called
  190. if( panelCfg.sash_pos_h < 0 )
  191. panelCfg.sash_pos_h = horizPixelsFromDU( 220 );
  192. if( panelCfg.sash_pos_v < 0 )
  193. panelCfg.sash_pos_v = horizPixelsFromDU( 230 );
  194. if( m_vsplitter )
  195. m_vsplitter->SetSashPosition( panelCfg.sash_pos_v );
  196. m_adapter->SetSortMode( (LIB_TREE_MODEL_ADAPTER::SORT_MODE) panelCfg.sort_mode );
  197. }
  198. }
  199. void PANEL_DESIGN_BLOCK_CHOOSER::RefreshLibs( bool aProgress )
  200. {
  201. // Unselect before syncing to avoid null reference in the adapter
  202. // if a selected item is removed during the sync
  203. m_tree->Unselect();
  204. DESIGN_BLOCK_TREE_MODEL_ADAPTER* adapter = static_cast<DESIGN_BLOCK_TREE_MODEL_ADAPTER*>( m_adapter.get() );
  205. // Clear all existing libraries then re-add
  206. adapter->ClearLibraries();
  207. // Read the libraries from disk if they've changed
  208. DESIGN_BLOCK_LIB_TABLE* fpTable = m_frame->Prj().DesignBlockLibs();
  209. adapter->SetLibTable( fpTable );
  210. // Sync FOOTPRINT_INFO list to the libraries on disk
  211. if( aProgress )
  212. {
  213. WX_PROGRESS_REPORTER progressReporter( this, _( "Update Design Block Libraries" ), 2,
  214. PR_CAN_ABORT );
  215. DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( fpTable, nullptr, &progressReporter );
  216. progressReporter.Show( false );
  217. }
  218. else
  219. {
  220. DESIGN_BLOCK_LIB_TABLE::GetGlobalList().ReadDesignBlockFiles( fpTable, nullptr, nullptr );
  221. }
  222. rebuildHistoryNode();
  223. if( !m_historyList.empty() )
  224. adapter->SetPreselectNode( m_historyList[0], 0 );
  225. adapter->AddLibraries( m_frame );
  226. m_tree->Regenerate( true );
  227. }
  228. void PANEL_DESIGN_BLOCK_CHOOSER::SetPreselect( const LIB_ID& aPreselect )
  229. {
  230. m_adapter->SetPreselectNode( aPreselect, 0 );
  231. }
  232. LIB_ID PANEL_DESIGN_BLOCK_CHOOSER::GetSelectedLibId( int* aUnit ) const
  233. {
  234. return m_tree->GetSelectedLibId( aUnit );
  235. }
  236. void PANEL_DESIGN_BLOCK_CHOOSER::SelectLibId( const LIB_ID& aLibId )
  237. {
  238. m_tree->CenterLibId( aLibId );
  239. m_tree->SelectLibId( aLibId );
  240. }
  241. void PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer( wxTimerEvent& aEvent )
  242. {
  243. // Hack because of eaten MouseUp event. See PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen
  244. // for the beginning of this spaghetti noodle.
  245. wxMouseState state = wxGetMouseState();
  246. if( state.LeftIsDown() )
  247. {
  248. // Mouse hasn't been raised yet, so fire the timer again. Otherwise the
  249. // purpose of this timer is defeated.
  250. m_dbl_click_timer->StartOnce( PANEL_DESIGN_BLOCK_CHOOSER::DBLCLICK_DELAY );
  251. }
  252. else
  253. {
  254. m_frame->GetCanvas()->SetFocus();
  255. m_selectHandler();
  256. addDesignBlockToHistory( m_tree->GetSelectedLibId() );
  257. }
  258. }
  259. void PANEL_DESIGN_BLOCK_CHOOSER::onOpenLibsTimer( wxTimerEvent& aEvent )
  260. {
  261. if( APP_SETTINGS_BASE* cfg = m_frame->config() )
  262. m_adapter->OpenLibs( cfg->m_LibTree.open_libs );
  263. // Bind this now se we don't spam the event queue with EVT_LIBITEM_SELECTED events during
  264. // the initial load.
  265. Bind( EVT_LIBITEM_SELECTED, &PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected, this );
  266. }
  267. void PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockSelected( wxCommandEvent& aEvent )
  268. {
  269. if( GetSelectedLibId().IsValid() )
  270. m_preview->DisplayDesignBlock( m_parent->GetDesignBlock( GetSelectedLibId(), true, true ) );
  271. }
  272. void PANEL_DESIGN_BLOCK_CHOOSER::onDesignBlockChosen( wxCommandEvent& aEvent )
  273. {
  274. if( m_tree->GetSelectedLibId().IsValid() )
  275. {
  276. // Got a selection. We can't just end the modal dialog here, because wx leaks some events
  277. // back to the parent window (in particular, the MouseUp following a double click).
  278. //
  279. // NOW, here's where it gets really fun. wxTreeListCtrl eats MouseUp. This isn't really
  280. // feasible to bypass without a fully custom wxDataViewCtrl implementation, and even then
  281. // might not be fully possible (docs are vague). To get around this, we use a one-shot
  282. // timer to schedule the dialog close.
  283. //
  284. // See PANEL_DESIGN_BLOCK_CHOOSER::onCloseTimer for the other end of this spaghetti noodle.
  285. m_dbl_click_timer->StartOnce( PANEL_DESIGN_BLOCK_CHOOSER::DBLCLICK_DELAY );
  286. }
  287. }
  288. void PANEL_DESIGN_BLOCK_CHOOSER::addDesignBlockToHistory( const LIB_ID& aLibId )
  289. {
  290. LIB_ID savedId = GetSelectedLibId();
  291. m_tree->Unselect();
  292. // Remove duplicates
  293. for( int i = (int) m_historyList.size() - 1; i >= 0; --i )
  294. {
  295. if( m_historyList[i] == aLibId )
  296. m_historyList.erase( m_historyList.begin() + i );
  297. }
  298. // Add the new name at the beginning of the history list
  299. m_historyList.insert( m_historyList.begin(), aLibId );
  300. // Remove extra names
  301. while( m_historyList.size() >= 8 )
  302. m_historyList.pop_back();
  303. rebuildHistoryNode();
  304. m_tree->Regenerate( true );
  305. SelectLibId( savedId );
  306. }
  307. void PANEL_DESIGN_BLOCK_CHOOSER::rebuildHistoryNode()
  308. {
  309. m_adapter->RemoveGroup( true, false );
  310. // Build the history list
  311. std::vector<LIB_TREE_ITEM*> historyInfos;
  312. for( const LIB_ID& lib : m_historyList )
  313. {
  314. LIB_TREE_ITEM* fp_info = DESIGN_BLOCK_LIB_TABLE::GetGlobalList().GetDesignBlockInfo( lib.GetLibNickname(),
  315. lib.GetLibItemName() );
  316. // this can be null, for example, if the design block has been deleted from a library.
  317. if( fp_info != nullptr )
  318. historyInfos.push_back( fp_info );
  319. }
  320. m_adapter->DoAddLibrary( wxT( "-- " ) + _( "Recently Used" ) + wxT( " --" ), wxEmptyString,
  321. historyInfos, false, true )
  322. .m_IsRecentlyUsedGroup = true;
  323. }
  324. void PANEL_DESIGN_BLOCK_CHOOSER::displayErrors( wxTopLevelWindow* aWindow )
  325. {
  326. // @todo: go to a more HTML !<table>! ? centric output, possibly with recommendations
  327. // for remedy of errors. Add numeric error codes to PARSE_ERROR, and switch on them for
  328. // remedies, etc. Full access is provided to everything in every exception!
  329. HTML_MESSAGE_BOX dlg( aWindow, _( "Load Error" ) );
  330. dlg.MessageSet( _( "Errors were encountered loading design blocks:" ) );
  331. wxString msg;
  332. while( std::unique_ptr<IO_ERROR> error = DESIGN_BLOCK_LIB_TABLE::GetGlobalList().PopError() )
  333. {
  334. wxString tmp = EscapeHTML( error->Problem() );
  335. // Preserve new lines in error messages so queued errors don't run together.
  336. tmp.Replace( wxS( "\n" ), wxS( "<BR>" ) );
  337. msg += wxT( "<p>" ) + tmp + wxT( "</p>" );
  338. }
  339. dlg.AddHTML_Text( msg );
  340. dlg.ShowModal();
  341. }