KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_template_selector.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2012 Brian Sidebotham <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
22#include <bitmaps.h>
23#include <kiplatform/ui.h>
24#include <pgm_base.h>
28#include <widgets/ui_common.h>
29#include <algorithm>
30#include <wx_filename.h>
31#include <wx/dir.h>
32#include <wx/dirdlg.h>
33#include <wx/settings.h>
34#include <wx/bitmap.h>
35#include <wx/image.h>
36#include <wx/math.h>
37#include <wx/menu.h>
38#include <wx/textdlg.h>
39#include <wx/textfile.h>
40#include <wx/regex.h>
41#include <confirm.h>
44
45
46static const wxChar* traceTemplateSelector = wxT( "KICAD_TEMPLATE_SELECTOR" );
47
48
50 const wxString& aPath, const wxString& aTitle,
51 const wxBitmap& aIcon ) :
52 wxPanel( aParent, wxID_ANY ),
53 m_dialog( aDialog ),
54 m_templatePath( aPath )
55{
56 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
57
58 wxBoxSizer* sizer = new wxBoxSizer( wxHORIZONTAL );
59
60 wxStaticBitmap* icon = new wxStaticBitmap( this, wxID_ANY, aIcon );
61 icon->SetMinSize( FromDIP( wxSize( 16, 16 ) ) );
62 sizer->Add( icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxTOP | wxBOTTOM, 6 );
63
64 wxStaticText* label = new wxStaticText( this, wxID_ANY, aTitle );
65 label->SetFont( KIUI::GetInfoFont( this ) );
66 sizer->Add( label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 6 );
67
68 SetSizer( sizer );
69 Layout();
70
71 Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
72 Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
73 Bind( wxEVT_ENTER_WINDOW, &TEMPLATE_MRU_WIDGET::OnEnter, this );
74 Bind( wxEVT_LEAVE_WINDOW, &TEMPLATE_MRU_WIDGET::OnLeave, this );
75
76 icon->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
77 icon->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
78 label->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
79 label->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
80}
81
82
83void TEMPLATE_MRU_WIDGET::OnClick( wxMouseEvent& event )
84{
85 // Just select the template in the list without showing the preview pane
86 m_dialog->SelectTemplateByPath( m_templatePath, true );
87 event.Skip();
88}
89
90
91void TEMPLATE_MRU_WIDGET::OnDoubleClick( wxMouseEvent& event )
92{
93 m_dialog->SelectTemplateByPath( m_templatePath );
94 m_dialog->EndModal( wxID_OK );
95 event.Skip();
96}
97
98
99void TEMPLATE_MRU_WIDGET::OnEnter( wxMouseEvent& event )
100{
101 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ).ChangeLightness( 150 ) );
102 Refresh();
103 event.Skip();
104}
105
106
107void TEMPLATE_MRU_WIDGET::OnLeave( wxMouseEvent& event )
108{
109 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
110 Refresh();
111 event.Skip();
112}
113
114
116 TEMPLATE_WIDGET_BASE( aParent )
117{
118 m_parent = aParent;
119 m_dialog = aDialog;
120 m_isUserTemplate = false;
121
122 // Set a small minimum size to allow the dialog to shrink
123 // The actual size will be determined by the parent sizer
124 SetMinSize( FromDIP( wxSize( 200, -1 ) ) );
125
126 m_bitmapIcon->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
127 m_titleLabel->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
128 m_descLabel->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
129
130 Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
131
132 m_bitmapIcon->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
133 m_titleLabel->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
134 m_descLabel->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
135 Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
136
137 m_bitmapIcon->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
138 m_titleLabel->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
139 m_descLabel->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
140 Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
141
142 // Set description text to use a smaller font
143 m_descLabel->SetFont( KIUI::GetInfoFont( this ) );
144
145#if wxCHECK_VERSION( 3, 3, 2 )
146 m_descLabel->SetWindowStyle( wxST_WRAP );
147#endif
148
149 // Bind size event for dynamic text wrapping
150 Bind( wxEVT_SIZE, &TEMPLATE_WIDGET::OnSize, this );
151
152 Unselect();
153
154 m_currTemplate = nullptr;
155}
156
157
159{
160 m_dialog->SetWidget( this );
161 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
162 m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
163 m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
164 m_selected = true;
165 Refresh();
166}
167
168
170{
171 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
172 m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
173 m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
174 m_selected = true;
175 Refresh();
176}
177
178
180{
181 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
182 m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
183 m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
184 m_selected = false;
185 Refresh();
186}
187
188
190{
191 m_currTemplate = aTemplate;
192 m_titleLabel->SetFont( KIUI::GetInfoFont( this ).Bold() );
193 m_titleLabel->SetLabel( *aTemplate->GetTitle() );
194
195 // Generate bitmap bundle from template icon bitmap (64x64)
196 // so wxStaticBitmap can size itself to the closest match
197 const std::vector<int> c_bitmapSizes = { 48, 64, 96, 128 };
198
199 wxBitmapBundle bundle;
200 wxVector<wxBitmap> bitmaps;
201 wxBitmap* icon = aTemplate->GetIcon();
202
203 if( icon && icon->IsOk() )
204 bundle = *icon;
205 else
206 bundle = KiBitmapBundleDef( BITMAPS::icon_kicad, c_bitmapSizes[0] );
207
208 wxSize defSize = bundle.GetDefaultSize();
209
210 for( const int genSize : c_bitmapSizes )
211 {
212 double scale = std::min( (double) genSize / defSize.GetWidth(),
213 (double) genSize / defSize.GetHeight() );
214
215 wxSize scaledSize( wxRound( defSize.x * scale ),
216 wxRound( defSize.y * scale ) );
217
218 wxBitmap scaled = bundle.GetBitmap( scaledSize );
219 scaled.SetScaleFactor( 1.0 );
220
221 bitmaps.push_back( scaled );
222 }
223
224 m_bitmapIcon->SetBitmap( wxBitmapBundle::FromBitmaps( bitmaps ) );
225}
226
227
228void TEMPLATE_WIDGET::SetDescription( const wxString& aDescription )
229{
230 m_description = aDescription;
231
232 // Truncate long descriptions to approximately 3 lines worth
233 wxString displayDesc = aDescription;
234
235 if( displayDesc.Length() > 120 )
236 displayDesc = displayDesc.Left( 120 ) + wxS( "..." );
237
238 m_descLabel->SetLabel( displayDesc );
239}
240
241
242void TEMPLATE_WIDGET::OnSize( wxSizeEvent& event )
243{
244 event.Skip();
245
246 // wx 3.3.2+ can wrap automatically
247#if !wxCHECK_VERSION( 3, 3, 2 )
248 // Calculate available width for description text (widget width minus icon and padding)
249 int wrapWidth = GetClientSize().GetWidth() - 48 - 20; // 48px icon + 20px padding
250
251 if( wrapWidth > 100 )
252 {
253 // Recalculate the truncated description from the original stored description
254 // to avoid cumulative wrapping artifacts
255 wxString displayDesc = m_description;
256
257 if( displayDesc.Length() > 120 )
258 displayDesc = displayDesc.Left( 120 ) + wxS( "..." );
259
260 m_descLabel->SetLabel( displayDesc );
261 m_descLabel->Wrap( wrapWidth );
262 Layout();
263 }
264#endif
265}
266
267
268void TEMPLATE_WIDGET::OnMouse( wxMouseEvent& event )
269{
270 Select();
271 event.Skip();
272}
273
274
275void TEMPLATE_WIDGET::OnDoubleClick( wxMouseEvent& event )
276{
277 Select();
278 m_dialog->EndModal( wxID_OK );
279 event.Skip();
280}
281
282
283void TEMPLATE_WIDGET::onRightClick( wxMouseEvent& event )
284{
285 if( !m_currTemplate )
286 {
287 event.Skip();
288 return;
289 }
290
291 wxMenu menu;
292 menu.Append( wxID_EDIT, m_isUserTemplate ? _( "Edit Template" ) : _( "Open Template (Read-Only)" ) );
293 menu.Append( wxID_OPEN, _( "Open Template Folder" ) );
294 menu.Append( wxID_COPY, _( "Duplicate Template" ) );
295
296 menu.Bind( wxEVT_COMMAND_MENU_SELECTED,
297 [this]( wxCommandEvent& evt )
298 {
299 if( evt.GetId() == wxID_EDIT )
300 onEditTemplate( evt );
301 if( evt.GetId() == wxID_OPEN )
302 onOpenFolder( evt );
303 else if( evt.GetId() == wxID_COPY )
304 onDuplicateTemplate( evt );
305 } );
306
307 PopupMenu( &menu );
308}
309
310
311void TEMPLATE_WIDGET::onEditTemplate( wxCommandEvent& event )
312{
313 if( !m_currTemplate )
314 return;
315
316 wxFileName templatePath = m_currTemplate->GetHtmlFile();
317 templatePath.RemoveLastDir();
318
319 wxDir dir( templatePath.GetPath() );
320
321 if( !dir.IsOpened() )
322 {
323 DisplayErrorMessage( m_dialog, _( "Could not open template directory." ) );
324 return;
325 }
326
327 wxString filename;
328 bool found = dir.GetFirst( &filename, "*.kicad_pro", wxDIR_FILES );
329
330 if( !found )
331 {
332 DisplayErrorMessage( m_dialog, _( "No project file found in template directory." ) );
333 return;
334 }
335
336 wxFileName projectFile( templatePath.GetPath(), filename );
337
338 m_dialog->SetProjectToEdit( projectFile.GetFullPath() );
339
340 m_dialog->EndModal( wxID_APPLY );
341}
342
343
344void TEMPLATE_WIDGET::onOpenFolder( wxCommandEvent& event )
345{
346 if( !m_currTemplate )
347 return;
348
349 wxFileName templatePath = m_currTemplate->GetHtmlFile();
350 templatePath.RemoveLastDir();
351
352 if( !wxLaunchDefaultApplication( templatePath.GetPath() ) )
353 DisplayError( this, wxString::Format( _( "Failed to open '%s'." ), templatePath.GetPath() ) );
354}
355
356
357void TEMPLATE_WIDGET::onDuplicateTemplate( wxCommandEvent& event )
358{
359 if( !m_currTemplate )
360 return;
361
362 wxFileName templatePath = m_currTemplate->GetHtmlFile();
363 templatePath.RemoveLastDir();
364 wxString srcTemplatePath = templatePath.GetPath();
365 wxString srcTemplateName = m_currTemplate->GetPrjDirName();
366
367 wxTextEntryDialog nameDlg( m_dialog, _( "Enter name for the new template:" ), _( "Duplicate Template" ),
368 srcTemplateName + _( "_copy" ) );
369
370 if( nameDlg.ShowModal() != wxID_OK )
371 return;
372
373 wxString newTemplateName = nameDlg.GetValue();
374
375 if( newTemplateName.IsEmpty() )
376 {
377 DisplayErrorMessage( m_dialog, _( "Template name cannot be empty." ) );
378 return;
379 }
380
381 wxString userTemplatesPath = m_dialog->GetUserTemplatesPath();
382
383 if( userTemplatesPath.IsEmpty() )
384 {
385 DisplayErrorMessage( m_dialog, _( "Could not find user templates directory." ) );
386 return;
387 }
388
389 wxFileName destPath( userTemplatesPath, wxEmptyString );
390 destPath.AppendDir( newTemplateName );
391 wxString newTemplatePath = destPath.GetPath();
392
393 if( destPath.DirExists() )
394 {
395 DisplayErrorMessage( m_dialog, wxString::Format( _( "Directory '%s' already exists." ), newTemplatePath ) );
396 return;
397 }
398
399 if( !destPath.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
400 {
401 DisplayErrorMessage( m_dialog, wxString::Format( _( "Could not create directory '%s'." ), newTemplatePath ) );
402 return;
403 }
404
405 wxDir sourceDir( srcTemplatePath );
406
407 if( !sourceDir.IsOpened() )
408 {
409 DisplayErrorMessage( m_dialog, _( "Could not open source template directory." ) );
410 return;
411 }
412
413 PROJECT_TREE_TRAVERSER traverser( nullptr, srcTemplatePath, srcTemplateName, newTemplatePath, newTemplateName );
414
415 sourceDir.Traverse( traverser );
416
417 if( !traverser.GetErrors().empty() )
418 {
419 DisplayErrorMessage( m_dialog, traverser.GetErrors() );
420 return;
421 }
422
423 wxFileName metaHtmlFile( newTemplatePath, "info.html" );
424 metaHtmlFile.AppendDir( "meta" );
425
426 if( metaHtmlFile.FileExists() )
427 {
428 wxTextFile htmlFile( metaHtmlFile.GetFullPath() );
429
430 if( htmlFile.Open() )
431 {
432 bool modified = false;
433
434 for( size_t i = 0; i < htmlFile.GetLineCount(); i++ )
435 {
436 wxString line = htmlFile.GetLine( i );
437
438 if( line.Contains( wxT( "<title>" ) ) && line.Contains( wxT( "</title>" ) ) )
439 {
440 int titleStart = line.Find( wxT( "<title>" ) );
441 int titleEnd = line.Find( wxT( "</title>" ) );
442
443 if( titleStart != wxNOT_FOUND && titleEnd != wxNOT_FOUND && titleEnd > titleStart )
444 {
445 wxString before = line.Left( titleStart + 7 );
446 wxString after = line.Mid( titleEnd );
447 line = before + newTemplateName + after;
448 htmlFile[i] = line;
449 modified = true;
450 }
451 }
452 }
453
454 if( modified )
455 htmlFile.Write();
456
457 htmlFile.Close();
458 }
459 }
460
461 // The file copy triggers OnFileSystemEvent which starts a refresh timer. If we show a
462 // modal info dialog here, the timer fires during the modal event loop and destroys this
463 // TEMPLATE_WIDGET while we're still on its call stack. Defer both the message and the
464 // refresh to run after the current event handler returns.
466
467 dlg->CallAfter(
468 [dlg, newTemplatePath]()
469 {
470 DisplayInfoMessage( dlg, wxString::Format( _( "Template duplicated successfully to '%s'." ),
471 newTemplatePath ) );
472 dlg->RefreshTemplateList();
473 } );
474}
475
476
477DIALOG_TEMPLATE_SELECTOR::DIALOG_TEMPLATE_SELECTOR( wxWindow* aParent, const wxPoint& aPos,
478 const wxSize& aSize, const wxString& aUserTemplatesPath,
479 const wxString& aSystemTemplatesPath,
480 const wxString& aDefaultTemplatesPath,
481 const std::vector<wxString>& aRecentTemplates ) :
482 DIALOG_TEMPLATE_SELECTOR_BASE( aParent, wxID_ANY, _( "Project Template Selector" ), aPos, aSize ),
484 m_selectedWidget( nullptr ),
485 m_selectedTemplate( nullptr ),
486 m_userTemplatesPath( aUserTemplatesPath ),
487 m_systemTemplatesPath( aSystemTemplatesPath ),
488 m_defaultTemplatesPath( aDefaultTemplatesPath ),
489 m_recentTemplates( aRecentTemplates ),
490 m_searchTimer( this ),
491 m_refreshTimer( this ),
492 m_watcher( nullptr ),
493 m_webviewPanel( nullptr ),
494 m_loadingExternalHtml( false ),
496{
497 // The base class now provides the UI structure via wxFormBuilder.
498 // Configure the scrolled windows.
499 m_scrolledMRU->SetScrollRate( 0, 25 );
500 m_scrolledTemplates->SetScrollRate( 0, 25 );
501 m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
502
503 m_scrolledTemplates->SetMinSize( FromDIP( wxSize( 200, 200 ) ) );
504
505 // Configure the search control
506 m_searchCtrl->ShowSearchButton( true );
507 m_searchCtrl->ShowCancelButton( true );
508
509 Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnSearchTimer, this, m_searchTimer.GetId() );
510 Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnRefreshTimer, this, m_refreshTimer.GetId() );
511 Bind( wxEVT_FSWATCHER, &DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent, this );
512 Bind( wxEVT_SYS_COLOUR_CHANGED, &DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged, this );
514
515 // Set initial filter based on saved setting
517
518 if( settings )
519 {
520 int filterChoice = settings->m_TemplateFilterChoice;
521
522 if( filterChoice >= 0 && filterChoice < static_cast<int>( m_filterChoice->GetCount() ) )
523 m_filterChoice->SetSelection( filterChoice );
524 else
525 m_filterChoice->SetSelection( 0 );
526 }
527 else
528 {
529 m_filterChoice->SetSelection( 0 );
530 }
531
532 BuildMRUList();
535
536 // Auto-select the most recently used template if available
537 if( !m_recentTemplates.empty() )
539
541
542 m_sdbSizerOK->SetDefault();
543
545}
546
547
549{
550 m_searchTimer.Stop();
551 m_refreshTimer.Stop();
552
553 if( m_watcher )
554 {
555 m_watcher->RemoveAll();
556 m_watcher->SetOwner( nullptr );
557 delete m_watcher;
558 m_watcher = nullptr;
559 }
560}
561
562
564{
565 if( m_splitter->IsSplit() )
566 return;
567
568 int sashPos = m_previewSashPos;
569
570 // Fall back to a third of the available width the first time the preview is shown
571 if( sashPos <= 0 )
572 {
573 int width = m_splitter->GetClientSize().GetWidth();
574 sashPos = width > 0 ? width / 3 : FromDIP( 300 );
575 }
576
577 m_splitter->SplitVertically( m_panelTemplates, m_panelPreview, sashPos );
578}
579
580
582{
583 m_state = aState;
584
585 switch( aState )
586 {
588 m_panelMRU->Show();
589 m_btnBack->Enable( false );
590
591 if( m_splitter->IsSplit() )
592 {
593 // Remember the sash so reselecting a template restores the user's layout
594 m_previewSashPos = m_splitter->GetSashPosition();
595 m_splitter->Unsplit( m_panelPreview );
596 }
597
598 break;
599
601 m_panelMRU->Hide();
602 m_btnBack->Enable( true );
604 break;
605
607 m_panelMRU->Show();
608 m_btnBack->Enable( true );
610 break;
611 }
612
613 Layout();
614}
615
616
618{
619 // Clear existing MRU widgets
620 for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
621 {
622 m_sizerMRU->Detach( widget );
623 widget->Destroy();
624 }
625
626 m_mruWidgets.clear();
627
628 for( const wxString& path : m_recentTemplates )
629 {
630 wxFileName templateDir;
631 templateDir.AssignDir( path );
632
633 if( !templateDir.DirExists() )
634 continue;
635
636 PROJECT_TEMPLATE templ( path );
637
638 wxString* title = templ.GetTitle();
639 wxBitmap* icon = templ.GetIcon();
640
641 wxBitmap scaledIcon;
642
643 if( icon && icon->IsOk() )
644 {
645 wxImage img = icon->ConvertToImage();
646 img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
647 scaledIcon = wxBitmap( img );
648 }
649 else
650 {
651 scaledIcon = KiBitmap( BITMAPS::icon_kicad );
652 wxImage img = scaledIcon.ConvertToImage();
653 img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
654 scaledIcon = wxBitmap( img );
655 }
656
657 wxString displayTitle = title ? *title : templateDir.GetDirs().Last();
658
659 TEMPLATE_MRU_WIDGET* mruWidget = new TEMPLATE_MRU_WIDGET( m_scrolledMRU, this, path, displayTitle,
660 scaledIcon );
661 m_sizerMRU->Add( mruWidget, 0, wxEXPAND | wxBOTTOM, 2 );
662 m_mruWidgets.push_back( mruWidget );
663 }
664
665 m_scrolledMRU->FitInside();
666 m_scrolledMRU->Layout();
667 m_panelMRU->Layout();
668}
669
670
672{
673 wxLogTrace( traceTemplateSelector, "BuildTemplateList() called" );
674
675 // Clear existing template widgets
676 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
677 {
678 m_sizerTemplateList->Detach( widget );
679 widget->Destroy();
680 }
681
682 m_templateWidgets.clear();
683 m_templates.clear();
684
685 auto scanDirectory =
686 [this]( const wxString& aPath, bool aIsUser )
687 {
688 if( aPath.IsEmpty() )
689 return;
690
691 wxDir dir;
692
693 if( !dir.Open( aPath ) )
694 return;
695
696 wxLogTrace( traceTemplateSelector, "Scanning directory: %s (user=%d)", aPath, aIsUser );
697
698 if( dir.HasSubDirs( "meta" ) )
699 {
700 auto templ = std::make_unique<PROJECT_TEMPLATE>( aPath );
701 wxFileName htmlFile = templ->GetHtmlFile();
702 wxString description = ExtractDescription( htmlFile );
703
705 widget->SetTemplate( templ.get() );
706 widget->SetDescription( description );
707 widget->SetIsUserTemplate( aIsUser );
708
709 m_templates.push_back( std::move( templ ) );
710 m_templateWidgets.push_back( widget );
711 }
712 else
713 {
714 wxString subName;
715 bool cont = dir.GetFirst( &subName, wxEmptyString, wxDIR_DIRS );
716
717 while( cont )
718 {
719 wxString subFull = aPath + wxFileName::GetPathSeparator() + subName;
720
721 // Only treat subdirectories that contain a meta directory as templates.
722 // Directories without meta (e.g. regular project directories the user
723 // placed in the template path) are silently skipped.
724 wxFileName metaTest;
725 metaTest.AssignDir( subFull );
726 metaTest.AppendDir( METADIR );
727
728 if( metaTest.DirExists() )
729 {
730 auto templ = std::make_unique<PROJECT_TEMPLATE>( subFull );
731 wxFileName htmlFile = templ->GetHtmlFile();
732 wxString description = templ->GetError().IsEmpty() ? ExtractDescription( htmlFile ) : templ->GetError();
733
734 TEMPLATE_WIDGET* widget =
736 widget->SetTemplate( templ.get() );
737 widget->SetDescription( description );
738 widget->SetIsUserTemplate( aIsUser );
739
740 m_templates.push_back( std::move( templ ) );
741 m_templateWidgets.push_back( widget );
742 }
743
744 cont = dir.GetNext( &subName );
745 }
746 }
747 };
748
749 scanDirectory( m_userTemplatesPath, true );
750 scanDirectory( m_systemTemplatesPath, false );
751
752 // The built-in "default" template lives in the stable default user templates path. It is
753 // always scanned so that the default remains available even when KICAD_USER_TEMPLATE_DIR
754 // points at a custom location. Treated as a built-in, not a user template.
755 scanDirectory( m_defaultTemplatesPath, false );
756
757 // Sort alphabetically with "Default" first
758 std::sort( m_templateWidgets.begin(), m_templateWidgets.end(),
760 {
761 wxString titleA = *a->GetTemplate()->GetTitle();
762 wxString titleB = *b->GetTemplate()->GetTitle();
763 int cmp = titleA.CmpNoCase( titleB );
764
765 if( cmp == 0 )
766 return false;
767
768 if( titleA.CmpNoCase( "default" ) == 0 )
769 return true;
770 else if( titleB.CmpNoCase( "default" ) == 0 )
771 return false;
772
773 return cmp < 0;
774 } );
775
776 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
777 {
778 m_sizerTemplateList->Add( widget, 0, wxEXPAND | wxBOTTOM, 8 );
779 }
780
781 ApplyFilter();
782
783 // Force initial sizing of widgets after layout
784 CallAfter(
785 [this]()
786 {
787 wxSizeEvent evt( m_scrolledTemplates->GetSize() );
789 } );
790
791 wxLogTrace( traceTemplateSelector, "BuildTemplateList() found %zu templates", m_templateWidgets.size() );
792}
793
794
796{
797 int filterChoice = m_filterChoice->GetSelection();
798 wxString searchText = m_searchCtrl->GetValue().Lower();
799
800 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
801 {
802 bool matchesFilter = true;
803
804 if( filterChoice == 1 && !widget->IsUserTemplate() )
805 matchesFilter = false;
806 else if( filterChoice == 2 && widget->IsUserTemplate() )
807 matchesFilter = false;
808
809 bool matchesSearch = true;
810
811 if( !searchText.IsEmpty() )
812 {
813 wxString title = widget->GetTemplate()->GetTitle()->Lower();
814 wxString description = widget->GetDescription().Lower();
815
816 matchesSearch = title.Contains( searchText ) || description.Contains( searchText );
817 }
818
819 widget->Show( matchesFilter && matchesSearch );
820 }
821
822 m_scrolledTemplates->FitInside();
823 m_scrolledTemplates->Layout();
824 Layout();
825}
826
827
828void DIALOG_TEMPLATE_SELECTOR::OnSearchCtrl( wxCommandEvent& event )
829{
830 m_searchTimer.Stop();
831 m_searchTimer.StartOnce( 200 );
832}
833
834
836{
837 m_searchCtrl->Clear();
838 ApplyFilter();
839}
840
841
842void DIALOG_TEMPLATE_SELECTOR::OnFilterChanged( wxCommandEvent& event )
843{
845
846 if( settings )
847 settings->m_TemplateFilterChoice = m_filterChoice->GetSelection();
848
849 ApplyFilter();
850}
851
852
854{
855 ApplyFilter();
856}
857
858
860{
861 wxString selectedPath;
862
863 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
864 {
865 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
866 htmlFile.RemoveLastDir();
867 selectedPath = htmlFile.GetPath();
868 }
869
870 // Clear pointers before destroying widgets to avoid dangling pointers
871 m_selectedWidget = nullptr;
872 m_selectedTemplate = nullptr;
873
875
876 if( !selectedPath.IsEmpty() )
877 SelectTemplateByPath( selectedPath );
878}
879
880
881void DIALOG_TEMPLATE_SELECTOR::OnBackClicked( wxCommandEvent& event )
882{
883 if( m_selectedWidget )
884 m_selectedWidget->Unselect();
885
886 m_selectedWidget = nullptr;
887 m_selectedTemplate = nullptr;
888
891}
892
893
895{
896 if( m_selectedWidget != nullptr && m_selectedWidget != aWidget )
897 m_selectedWidget->Unselect();
898
899 m_selectedWidget = aWidget;
900 m_selectedTemplate = aWidget ? aWidget->GetTemplate() : nullptr;
901
903
906}
907
908
910{
911 SelectTemplateByPath( aPath, false );
912}
913
914
915void DIALOG_TEMPLATE_SELECTOR::SelectTemplateByPath( const wxString& aPath, bool aKeepMRUVisible )
916{
917 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
918 {
919 if( widget->GetTemplate() )
920 {
921 wxFileName htmlFile = widget->GetTemplate()->GetHtmlFile();
922 htmlFile.RemoveLastDir();
923
924 if( htmlFile.GetPath() == aPath )
925 {
926 if( aKeepMRUVisible )
927 {
928 // Select the widget but keep MRU visible, don't show preview
929 if( m_selectedWidget != nullptr && m_selectedWidget != widget )
930 m_selectedWidget->Unselect();
931
932 m_selectedWidget = widget;
933 m_selectedTemplate = widget->GetTemplate();
934
935 widget->SelectWithoutStateChange();
936
937 // Scroll the widget into view
938 int y = 0;
939 int scrollRate = 0;
940 widget->GetPosition( nullptr, &y );
941 m_scrolledTemplates->GetScrollPixelsPerUnit( nullptr, &scrollRate );
942
943 if( scrollRate > 0 )
944 m_scrolledTemplates->Scroll( -1, y / scrollRate );
945 }
946 else
947 {
948 widget->Select();
949 }
950
951 return;
952 }
953 }
954 }
955
956 wxLogTrace( traceTemplateSelector, "SelectTemplateByPath: template not found at %s", aPath );
957}
958
959
961{
962 if( m_webviewPanel )
963 return;
964
965 // Create the WEBVIEW_PANEL lazily to avoid WebKit JavaScript VM initialization issues
966 // when the dialog is opened from a coroutine context.
967 m_webviewPanel = new WEBVIEW_PANEL( m_webviewPlaceholder, wxID_ANY, wxDefaultPosition, wxDefaultSize,
968 wxTAB_TRAVERSAL );
969
970 wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
971 sizer->Add( m_webviewPanel, 1, wxEXPAND );
972 m_webviewPlaceholder->SetSizer( sizer );
973 m_webviewPlaceholder->Layout();
974
975 m_webviewPanel->BindLoadedEvent();
976
977 if( m_webviewPanel->GetWebView() )
978 {
979 Bind( wxEVT_WEBVIEW_LOADED, &DIALOG_TEMPLATE_SELECTOR::OnWebViewLoaded, this,
980 m_webviewPanel->GetWebView()->GetId() );
981 }
982}
983
984
986{
987 if( !aTemplate )
988 return;
989
991
992 wxFileName htmlFile = aTemplate->GetHtmlFile();
993
994 if( htmlFile.FileExists() && htmlFile.IsFileReadable() )
995 {
997 wxString url = wxFileName::FileNameToURL( htmlFile );
998 m_webviewPanel->LoadURL( url );
999 }
1000 else
1001 {
1002 m_loadingExternalHtml = false;
1003 wxString html = GetTemplateInfoHtml( *aTemplate->GetTitle(), KIPLATFORM::UI::IsDarkTheme() );
1004 m_webviewPanel->SetPage( html );
1005 }
1006}
1007
1008
1015
1016
1018{
1019 if( m_watcher )
1020 {
1021 m_watcher->RemoveAll();
1022 delete m_watcher;
1023 m_watcher = nullptr;
1024 }
1025
1027 m_watcher->SetOwner( this );
1028
1029 wxLogNull logNo;
1030
1031 auto watchDir =
1032 [this]( const wxString& aPath, const char* aLabel )
1033 {
1034 if( aPath.IsEmpty() )
1035 return;
1036
1037 wxFileName dir;
1038 dir.AssignDir( aPath );
1039
1040 if( dir.DirExists() )
1041 {
1042 m_watcher->Add( dir );
1043 wxLogTrace( traceTemplateSelector, "Watching %s templates: %s", aLabel, aPath );
1044 }
1045 };
1046
1047 watchDir( m_userTemplatesPath, "user" );
1048 watchDir( m_systemTemplatesPath, "system" );
1049 watchDir( m_defaultTemplatesPath, "default" );
1050}
1051
1052
1053void DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent( wxFileSystemWatcherEvent& event )
1054{
1055 if( !m_watcher )
1056 return;
1057
1058 wxLogTrace( traceTemplateSelector, "File system event detected" );
1059
1060 // wxFileSystemWatcher may fire events from a worker thread on some platforms.
1061 // Use CallAfter to marshal timer operations to the main thread.
1062 CallAfter(
1063 [this]()
1064 {
1065 m_refreshTimer.Stop();
1066 m_refreshTimer.StartOnce( 500 );
1067 } );
1068}
1069
1070
1071void DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged( wxSysColourChangedEvent& event )
1072{
1073 event.Skip();
1074
1075 // Update background colors for the templates panel
1076 m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1077
1078 // Update all template widgets
1079 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1080 {
1081 if( widget->IsSelected() )
1082 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
1083 else
1084 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1085
1086 widget->Refresh();
1087 }
1088
1089 // Update all MRU widgets
1090 for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
1091 {
1092 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1093 widget->Refresh();
1094 }
1095
1096 m_scrolledTemplates->Refresh();
1097 m_scrolledMRU->Refresh();
1098}
1099
1100
1102{
1103 event.Skip();
1104
1105 // Get the client width of the scrolled window
1106 int clientWidth = m_scrolledTemplates->GetClientSize().GetWidth();
1107
1108 if( clientWidth <= 0 )
1109 return;
1110
1111 // Force each widget to rewrap its text at the new width
1112 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1113 {
1114 wxSizeEvent sizeEvt( wxSize( clientWidth, -1 ) );
1115 widget->GetEventHandler()->ProcessEvent( sizeEvt );
1116 }
1117
1118 m_scrolledTemplates->FitInside();
1119}
1120
1121
1123{
1124 event.Skip();
1125
1126 // Safety check - ensure webview panel is valid
1127 if( !m_webviewPanel || !m_webviewPanel->GetWebView() )
1128 return;
1129
1130 // Add style to external documets
1132 {
1133 wxString script = wxString::Format( wxS( R"(
1134( function()
1135{
1136 var style = document.createElement( 'style' );
1137 style.textContent = `
1138%s
1139 `;
1140 document.head.appendChild( style );
1141} )();
1142 )" ), GetCommonStyles() );
1143
1144#if !defined( __MINGW32__ ) // RunScriptAsync() is not supported on MINGW build
1145 if( m_webviewPanel->GetBackend() != wxWebViewBackendIE )
1146 m_webviewPanel->RunScriptAsync( script );
1147#endif
1148 }
1149}
1150
1151
1153{
1154 wxString selectedPath;
1155
1156 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
1157 {
1158 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
1159 htmlFile.RemoveLastDir();
1160 selectedPath = htmlFile.GetPath();
1161 }
1162
1163 // Clear pointers before destroying widgets to avoid dangling pointers
1164 m_selectedWidget = nullptr;
1165 m_selectedTemplate = nullptr;
1166
1168
1169 if( !selectedPath.IsEmpty() )
1170 SelectTemplateByPath( selectedPath );
1171}
1172
1173
1174wxString DIALOG_TEMPLATE_SELECTOR::ExtractDescription( const wxFileName& aHtmlFile )
1175{
1176 if( !aHtmlFile.FileExists() || !aHtmlFile.IsFileReadable() )
1177 return wxEmptyString;
1178
1179 wxTextFile file( aHtmlFile.GetFullPath() );
1180
1181 if( !file.Open() )
1182 return wxEmptyString;
1183
1184 wxString content;
1185
1186 for( wxString line = file.GetFirstLine(); !file.Eof(); line = file.GetNextLine() )
1187 {
1188 content += line + wxT( " " );
1189 }
1190
1191 file.Close();
1192
1193 // Try to extract text from the meta description tag
1194 wxRegEx reMetaDesc( wxT( "<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']*)[\"']" ),
1195 wxRE_ICASE );
1196
1197 if( reMetaDesc.Matches( content ) )
1198 return reMetaDesc.GetMatch( content, 1 );
1199
1200 // Fallback to first paragraph tag (allowing nested HTML tags)
1201 wxRegEx reParagraph( wxT( "<p[^>]*>(.*?)</p>" ), wxRE_ICASE );
1202
1203 if( reParagraph.Matches( content ) )
1204 {
1205 wxString desc = reParagraph.GetMatch( content, 1 );
1206
1207 // Strip nested HTML tags
1208 wxRegEx reTags( wxT( "<[^>]*>" ) );
1209 reTags.ReplaceAll( &desc, wxT( " " ) );
1210
1211 // Decode common HTML entities
1212 desc.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1213 desc.Replace( wxT( "&amp;" ), wxT( "&" ) );
1214 desc.Replace( wxT( "&lt;" ), wxT( "<" ) );
1215 desc.Replace( wxT( "&gt;" ), wxT( ">" ) );
1216 desc.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1217
1218 // Normalize whitespace
1219 wxRegEx reWhitespace( wxT( "\\s+" ) );
1220 reWhitespace.ReplaceAll( &desc, wxT( " " ) );
1221
1222 desc = desc.Trim().Trim( false );
1223
1224 if( !desc.IsEmpty() )
1225 return desc;
1226 }
1227
1228 // Final fallback: extract text content from body, stripping all HTML tags
1229 wxRegEx reBody( wxT( "<body[^>]*>(.*)</body>" ), wxRE_ICASE );
1230
1231 if( reBody.Matches( content ) )
1232 {
1233 wxString body = reBody.GetMatch( content, 1 );
1234
1235 // Strip all HTML tags
1236 wxRegEx reTags( wxT( "<[^>]*>" ) );
1237 reTags.ReplaceAll( &body, wxT( " " ) );
1238
1239 // Decode common HTML entities
1240 body.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1241 body.Replace( wxT( "&amp;" ), wxT( "&" ) );
1242 body.Replace( wxT( "&lt;" ), wxT( "<" ) );
1243 body.Replace( wxT( "&gt;" ), wxT( ">" ) );
1244 body.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1245
1246 // Normalize whitespace
1247 wxRegEx reWhitespace( wxT( "\\s+" ) );
1248 reWhitespace.ReplaceAll( &body, wxT( " " ) );
1249
1250 body = body.Trim().Trim( false );
1251
1252 // Return up to 250 characters
1253 if( body.Length() > 250 )
1254 return body.Left( 250 ) + wxS( "..." );
1255
1256 return body;
1257 }
1258
1259 return wxEmptyString;
1260}
1261
1262
wxBitmapBundle KiBitmapBundleDef(BITMAPS aBitmap, int aDefHeight)
Constructs and returns a bitmap bundle for the given icon ID, with the default bitmap size being aDef...
Definition bitmap.cpp:112
wxBitmap KiBitmap(BITMAPS aBitmap, int aHeightTag)
Construct a wxBitmap from an image identifier Returns the image from the active theme if the image ha...
Definition bitmap.cpp:100
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
DIALOG_TEMPLATE_SELECTOR_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Project Template Selector"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(900, 600), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
wxString ExtractDescription(const wxFileName &aHtmlFile)
void SetState(DialogState aState)
void SelectTemplateByPath(const wxString &aPath)
void OnBackClicked(wxCommandEvent &event) override
void LoadTemplatePreview(PROJECT_TEMPLATE *aTemplate)
void OnSearchCtrl(wxCommandEvent &event) override
void OnFileSystemEvent(wxFileSystemWatcherEvent &event)
void OnSearchCtrlCancel(wxCommandEvent &event) override
void OnWebViewLoaded(wxWebViewEvent &event)
void OnFilterChanged(wxCommandEvent &event) override
std::vector< TEMPLATE_WIDGET * > m_templateWidgets
void OnRefreshTimer(wxTimerEvent &event)
void OnScrolledTemplatesSize(wxSizeEvent &event)
void SetWidget(TEMPLATE_WIDGET *aWidget)
std::vector< TEMPLATE_MRU_WIDGET * > m_mruWidgets
void OnSysColourChanged(wxSysColourChangedEvent &event)
DIALOG_TEMPLATE_SELECTOR(wxWindow *aParent, const wxPoint &aPos, const wxSize &aSize, const wxString &aUserTemplatesPath, const wxString &aSystemTemplatesPath, const wxString &aDefaultTemplatesPath, const std::vector< wxString > &aRecentTemplates)
void OnSearchTimer(wxTimerEvent &event)
std::vector< std::unique_ptr< PROJECT_TEMPLATE > > m_templates
std::vector< wxString > m_recentTemplates
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
A class which provides project template functionality.
wxBitmap * GetIcon()
Get the 64px^2 icon for the project template.
wxFileName GetHtmlFile()
Get the full Html filename for the project template.
wxString * GetTitle()
Get the title of the project (extracted from the html title tag)
Traverser class to duplicate/copy project or template files with proper renaming.
T * GetAppSettings(const char *aFilename)
Return a handle to the a given settings by type.
A widget displaying a recently used template with a small icon and title.
DIALOG_TEMPLATE_SELECTOR * m_dialog
void OnEnter(wxMouseEvent &event)
TEMPLATE_MRU_WIDGET(wxWindow *aParent, DIALOG_TEMPLATE_SELECTOR *aDialog, const wxString &aPath, const wxString &aTitle, const wxBitmap &aIcon)
void OnLeave(wxMouseEvent &event)
void OnClick(wxMouseEvent &event)
void OnDoubleClick(wxMouseEvent &event)
TEMPLATE_WIDGET_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxBORDER_THEME|wxTAB_TRAVERSAL, const wxString &name=wxEmptyString)
void SetIsUserTemplate(bool aIsUser)
Set whether this template widget represents a user template.
PROJECT_TEMPLATE * GetTemplate()
void OnMouse(wxMouseEvent &event)
void onOpenFolder(wxCommandEvent &event)
void SetTemplate(PROJECT_TEMPLATE *aTemplate)
Set the project template for this widget, which will determine the icon and title associated with thi...
DIALOG_TEMPLATE_SELECTOR * m_dialog
void SetDescription(const wxString &aDescription)
void OnSize(wxSizeEvent &event)
void onEditTemplate(wxCommandEvent &event)
void OnDoubleClick(wxMouseEvent &event)
void onDuplicateTemplate(wxCommandEvent &event)
void onRightClick(wxMouseEvent &event)
TEMPLATE_WIDGET(wxWindow *aParent, DIALOG_TEMPLATE_SELECTOR *aDialog)
PROJECT_TEMPLATE * m_currTemplate
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition confirm.cpp:192
This file is part of the common library.
static const wxChar * traceTemplateSelector
#define _(s)
bool IsDarkTheme()
Determine if the desktop interface is currently using a dark theme or a light theme.
Definition wxgtk/ui.cpp:50
KICOMMON_API wxFont GetInfoFont(wxWindow *aWindow)
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
#define METADIR
A directory which contains information about the project template and does not get copied.
#define wxFileSystemWatcher
const int scale
wxString GetWelcomeHtml(bool aDarkMode)
wxString GetTemplateInfoHtml(const wxString &aTemplateName, bool aDarkMode)
std::string path
Functions to provide common constants and other functions to assist in making a consistent UI.