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, you may find one here:
19 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20 * or you may search the http://www.gnu.org website for the version 2 license,
21 * or you may write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 */
24
26#include <bitmaps.h>
27#include <kiplatform/ui.h>
28#include <pgm_base.h>
32#include <widgets/ui_common.h>
33#include <algorithm>
34#include <wx_filename.h>
35#include <wx/dir.h>
36#include <wx/dirdlg.h>
37#include <wx/settings.h>
38#include <wx/bitmap.h>
39#include <wx/image.h>
40#include <wx/math.h>
41#include <wx/menu.h>
42#include <wx/textdlg.h>
43#include <wx/textfile.h>
44#include <wx/regex.h>
45#include <confirm.h>
48
49
50static const wxChar* traceTemplateSelector = wxT( "KICAD_TEMPLATE_SELECTOR" );
51
52
54 const wxString& aPath, const wxString& aTitle,
55 const wxBitmap& aIcon ) :
56 wxPanel( aParent, wxID_ANY ),
57 m_dialog( aDialog ),
58 m_templatePath( aPath )
59{
60 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
61
62 wxBoxSizer* sizer = new wxBoxSizer( wxHORIZONTAL );
63
64 wxStaticBitmap* icon = new wxStaticBitmap( this, wxID_ANY, aIcon );
65 icon->SetMinSize( FromDIP( wxSize( 16, 16 ) ) );
66 sizer->Add( icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxTOP | wxBOTTOM, 6 );
67
68 wxStaticText* label = new wxStaticText( this, wxID_ANY, aTitle );
69 label->SetFont( KIUI::GetInfoFont( this ) );
70 sizer->Add( label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 6 );
71
72 SetSizer( sizer );
73 Layout();
74
75 Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
76 Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
77 Bind( wxEVT_ENTER_WINDOW, &TEMPLATE_MRU_WIDGET::OnEnter, this );
78 Bind( wxEVT_LEAVE_WINDOW, &TEMPLATE_MRU_WIDGET::OnLeave, this );
79
80 icon->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
81 icon->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
82 label->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
83 label->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
84}
85
86
87void TEMPLATE_MRU_WIDGET::OnClick( wxMouseEvent& event )
88{
89 // Just select the template in the list without showing the preview pane
90 m_dialog->SelectTemplateByPath( m_templatePath, true );
91 event.Skip();
92}
93
94
95void TEMPLATE_MRU_WIDGET::OnDoubleClick( wxMouseEvent& event )
96{
97 m_dialog->SelectTemplateByPath( m_templatePath );
98 m_dialog->EndModal( wxID_OK );
99 event.Skip();
100}
101
102
103void TEMPLATE_MRU_WIDGET::OnEnter( wxMouseEvent& event )
104{
105 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ).ChangeLightness( 150 ) );
106 Refresh();
107 event.Skip();
108}
109
110
111void TEMPLATE_MRU_WIDGET::OnLeave( wxMouseEvent& event )
112{
113 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
114 Refresh();
115 event.Skip();
116}
117
118
120 TEMPLATE_WIDGET_BASE( aParent )
121{
122 m_parent = aParent;
123 m_dialog = aDialog;
124 m_isUserTemplate = false;
125
126 // Set a small minimum size to allow the dialog to shrink
127 // The actual size will be determined by the parent sizer
128 SetMinSize( FromDIP( wxSize( 200, -1 ) ) );
129
130 m_bitmapIcon->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ),
131 nullptr, this );
132 m_staticTitle->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ),
133 nullptr, this );
134 m_staticDescription->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ),
135 nullptr, this );
136
137 Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
138
139 m_bitmapIcon->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ),
140 nullptr, this );
141 m_staticTitle->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ),
142 nullptr, this );
143 m_staticDescription->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ),
144 nullptr, this );
145 Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ),
146 nullptr, this );
147
148 m_bitmapIcon->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ),
149 nullptr, this );
150 m_staticTitle->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ),
151 nullptr, this );
152 m_staticDescription->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ),
153 nullptr, this );
154 Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ),
155 nullptr, this );
156
157 // Set description text to use a smaller font
158 m_staticDescription->SetFont( KIUI::GetInfoFont( this ) );
159
160 // Set small minimum sizes on text controls to allow dialog shrinking
161 m_staticTitle->SetMinSize( FromDIP( wxSize( 100, -1 ) ) );
162 m_staticDescription->SetMinSize( FromDIP( wxSize( 100, -1 ) ) );
163
164#if wxCHECK_VERSION( 3, 3, 2 )
165 m_staticDescription->SetWindowStyle( wxST_WRAP );
166#endif
167
168 // Bind size event for dynamic text wrapping
169 Bind( wxEVT_SIZE, &TEMPLATE_WIDGET::OnSize, this );
170
171 Unselect();
172
173 m_currTemplate = nullptr;
174}
175
176
178{
179 m_dialog->SetWidget( this );
180 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
181 m_staticTitle->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
182 m_staticDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
183 m_selected = true;
184 Refresh();
185}
186
187
189{
190 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
191 m_staticTitle->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
192 m_staticDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
193 m_selected = true;
194 Refresh();
195}
196
197
199{
200 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
201 m_staticTitle->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
202 m_staticDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
203 m_selected = false;
204 Refresh();
205}
206
207
209{
210 m_currTemplate = aTemplate;
211 m_staticTitle->SetFont( KIUI::GetInfoFont( this ).Bold() );
212 m_staticTitle->SetLabel( *aTemplate->GetTitle() );
213
214 // Generate bitmap bundle from template icon bitmap (64x64)
215 // so wxStaticBitmap can size itself to the closest match
216 const std::vector<int> c_bitmapSizes = { 48, 64, 96, 128 };
217
218 wxBitmapBundle bundle;
219 wxVector<wxBitmap> bitmaps;
220 wxBitmap* icon = aTemplate->GetIcon();
221
222 if( icon && icon->IsOk() )
223 bundle = *icon;
224 else
225 bundle = KiBitmapBundleDef( BITMAPS::icon_kicad, c_bitmapSizes[0] );
226
227 wxSize defSize = bundle.GetDefaultSize();
228
229 for( const int genSize : c_bitmapSizes )
230 {
231 double scale = std::min( (double) genSize / defSize.GetWidth(),
232 (double) genSize / defSize.GetHeight() );
233
234 wxSize scaledSize( wxRound( defSize.x * scale ),
235 wxRound( defSize.y * scale ) );
236
237 wxBitmap scaled = bundle.GetBitmap( scaledSize );
238 scaled.SetScaleFactor( 1.0 );
239
240 bitmaps.push_back( scaled );
241 }
242
243 m_bitmapIcon->SetBitmap( wxBitmapBundle::FromBitmaps( bitmaps ) );
244}
245
246
247void TEMPLATE_WIDGET::SetDescription( const wxString& aDescription )
248{
249 m_description = aDescription;
250
251 // Truncate long descriptions to approximately 3 lines worth
252 wxString displayDesc = aDescription;
253
254 if( displayDesc.Length() > 120 )
255 displayDesc = displayDesc.Left( 120 ) + wxS( "..." );
256
257 m_staticDescription->SetLabel( displayDesc );
258}
259
260
261void TEMPLATE_WIDGET::OnSize( wxSizeEvent& event )
262{
263 event.Skip();
264
265 // wx 3.3.2+ can wrap automatically
266#if !wxCHECK_VERSION( 3, 3, 2 )
267 // Calculate available width for description text (widget width minus icon and padding)
268 int wrapWidth = GetClientSize().GetWidth() - 48 - 20; // 48px icon + 20px padding
269
270 if( wrapWidth > 100 )
271 {
272 // Recalculate the truncated description from the original stored description
273 // to avoid cumulative wrapping artifacts
274 wxString displayDesc = m_description;
275
276 if( displayDesc.Length() > 120 )
277 displayDesc = displayDesc.Left( 120 ) + wxS( "..." );
278
279 m_staticDescription->SetLabel( displayDesc );
280 m_staticDescription->Wrap( wrapWidth );
281 Layout();
282 }
283#endif
284}
285
286
287void TEMPLATE_WIDGET::OnMouse( wxMouseEvent& event )
288{
289 Select();
290 event.Skip();
291}
292
293
294void TEMPLATE_WIDGET::OnDoubleClick( wxMouseEvent& event )
295{
296 Select();
297 m_dialog->EndModal( wxID_OK );
298 event.Skip();
299}
300
301
302void TEMPLATE_WIDGET::onRightClick( wxMouseEvent& event )
303{
305 {
306 event.Skip();
307 return;
308 }
309
310 wxMenu menu;
311 menu.Append( wxID_EDIT, _( "Edit Template" ) );
312 menu.Append( wxID_COPY, _( "Duplicate Template" ) );
313
314 menu.Bind( wxEVT_COMMAND_MENU_SELECTED,
315 [this]( wxCommandEvent& evt )
316 {
317 if( evt.GetId() == wxID_EDIT )
318 onEditTemplate( evt );
319 else if( evt.GetId() == wxID_COPY )
320 onDuplicateTemplate( evt );
321 } );
322
323 PopupMenu( &menu );
324}
325
326
327void TEMPLATE_WIDGET::onEditTemplate( wxCommandEvent& event )
328{
329 if( !m_currTemplate )
330 return;
331
332 wxFileName templatePath = m_currTemplate->GetHtmlFile();
333 templatePath.RemoveLastDir();
334
335 wxDir dir( templatePath.GetPath() );
336
337 if( !dir.IsOpened() )
338 {
339 DisplayErrorMessage( m_dialog, _( "Could not open template directory." ) );
340 return;
341 }
342
343 wxString filename;
344 bool found = dir.GetFirst( &filename, "*.kicad_pro", wxDIR_FILES );
345
346 if( !found )
347 {
348 DisplayErrorMessage( m_dialog, _( "No project file found in template directory." ) );
349 return;
350 }
351
352 wxFileName projectFile( templatePath.GetPath(), filename );
353
354 m_dialog->SetProjectToEdit( projectFile.GetFullPath() );
355
356 m_dialog->EndModal( wxID_APPLY );
357}
358
359
360void TEMPLATE_WIDGET::onDuplicateTemplate( wxCommandEvent& event )
361{
362 if( !m_currTemplate )
363 return;
364
365 wxFileName templatePath = m_currTemplate->GetHtmlFile();
366 templatePath.RemoveLastDir();
367 wxString srcTemplatePath = templatePath.GetPath();
368 wxString srcTemplateName = m_currTemplate->GetPrjDirName();
369
370 wxTextEntryDialog nameDlg( m_dialog,
371 _( "Enter name for the new template:" ),
372 _( "Duplicate Template" ),
373 srcTemplateName + _( "_copy" ) );
374
375 if( nameDlg.ShowModal() != wxID_OK )
376 return;
377
378 wxString newTemplateName = nameDlg.GetValue();
379
380 if( newTemplateName.IsEmpty() )
381 {
382 DisplayErrorMessage( m_dialog, _( "Template name cannot be empty." ) );
383 return;
384 }
385
386 wxString userTemplatesPath = m_dialog->GetUserTemplatesPath();
387
388 if( userTemplatesPath.IsEmpty() )
389 {
390 DisplayErrorMessage( m_dialog, _( "Could not find user templates directory." ) );
391 return;
392 }
393
394 wxFileName destPath( userTemplatesPath, wxEmptyString );
395 destPath.AppendDir( newTemplateName );
396 wxString newTemplatePath = destPath.GetPath();
397
398 if( destPath.DirExists() )
399 {
401 wxString::Format( _( "Directory '%s' already exists." ),
402 newTemplatePath ) );
403 return;
404 }
405
406 if( !destPath.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
407 {
409 wxString::Format( _( "Could not create directory '%s'." ),
410 newTemplatePath ) );
411 return;
412 }
413
414 wxDir sourceDir( srcTemplatePath );
415
416 if( !sourceDir.IsOpened() )
417 {
418 DisplayErrorMessage( m_dialog, _( "Could not open source template directory." ) );
419 return;
420 }
421
422 PROJECT_TREE_TRAVERSER traverser( nullptr, srcTemplatePath, srcTemplateName,
423 newTemplatePath, newTemplateName );
424
425 sourceDir.Traverse( traverser );
426
427 if( !traverser.GetErrors().empty() )
428 {
429 DisplayErrorMessage( m_dialog, traverser.GetErrors() );
430 return;
431 }
432
433 wxFileName metaHtmlFile( newTemplatePath, "info.html" );
434 metaHtmlFile.AppendDir( "meta" );
435
436 if( metaHtmlFile.FileExists() )
437 {
438 wxTextFile htmlFile( metaHtmlFile.GetFullPath() );
439
440 if( htmlFile.Open() )
441 {
442 bool modified = false;
443
444 for( size_t i = 0; i < htmlFile.GetLineCount(); i++ )
445 {
446 wxString line = htmlFile.GetLine( i );
447
448 if( line.Contains( wxT( "<title>" ) ) && line.Contains( wxT( "</title>" ) ) )
449 {
450 int titleStart = line.Find( wxT( "<title>" ) );
451 int titleEnd = line.Find( wxT( "</title>" ) );
452
453 if( titleStart != wxNOT_FOUND && titleEnd != wxNOT_FOUND && titleEnd > titleStart )
454 {
455 wxString before = line.Left( titleStart + 7 );
456 wxString after = line.Mid( titleEnd );
457 line = before + newTemplateName + after;
458 htmlFile[i] = line;
459 modified = true;
460 }
461 }
462 }
463
464 if( modified )
465 htmlFile.Write();
466
467 htmlFile.Close();
468 }
469 }
470
471 DisplayInfoMessage( m_dialog, wxString::Format( _( "Template duplicated successfully to '%s'." ),
472 newTemplatePath ) );
473
474 m_dialog->RefreshTemplateList();
475}
476
477
478DIALOG_TEMPLATE_SELECTOR::DIALOG_TEMPLATE_SELECTOR( wxWindow* aParent, const wxPoint& aPos,
479 const wxSize& aSize,
480 const wxString& aUserTemplatesPath,
481 const wxString& aSystemTemplatesPath,
482 const std::vector<wxString>& aRecentTemplates ) :
483 DIALOG_TEMPLATE_SELECTOR_BASE( aParent, wxID_ANY, _( "Project Template Selector" ), aPos, aSize ),
485 m_selectedWidget( nullptr ),
486 m_selectedTemplate( nullptr ),
487 m_userTemplatesPath( aUserTemplatesPath ),
488 m_systemTemplatesPath( aSystemTemplatesPath ),
489 m_recentTemplates( aRecentTemplates ),
490 m_searchTimer( this ),
491 m_refreshTimer( this ),
492 m_watcher( nullptr ),
493 m_webviewPanel( nullptr ),
494 m_loadingExternalHtml( false )
495{
496 // The base class now provides the UI structure via wxFormBuilder.
497 // Configure the scrolled windows.
498 m_scrolledMRU->SetScrollRate( 0, 25 );
499 m_scrolledTemplates->SetScrollRate( 0, 25 );
500 m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
501
502 // Override minimum sizes to allow dialog shrinking (base class Fit() sets large sizes)
503 m_scrolledTemplates->SetMinSize( FromDIP( wxSize( 300, 300 ) ) );
504 m_panelTemplates->SetMinSize( FromDIP( wxSize( 450, 500 ) ) );
505
506 // Configure the search control
507 m_searchCtrl->ShowSearchButton( true );
508 m_searchCtrl->ShowCancelButton( true );
509
510 Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnSearchTimer, this, m_searchTimer.GetId() );
511 Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnRefreshTimer, this, m_refreshTimer.GetId() );
512 Bind( wxEVT_FSWATCHER, &DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent, this );
513 Bind( wxEVT_SYS_COLOUR_CHANGED, &DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged, this );
515
516 // Set initial filter based on saved setting
518
519 if( settings )
520 {
521 int filterChoice = settings->m_TemplateFilterChoice;
522
523 if( filterChoice >= 0 && filterChoice < static_cast<int>( m_filterChoice->GetCount() ) )
524 m_filterChoice->SetSelection( filterChoice );
525 else
526 m_filterChoice->SetSelection( 0 );
527 }
528 else
529 {
530 m_filterChoice->SetSelection( 0 );
531 }
532
533 BuildMRUList();
536
537 // Auto-select the most recently used template if available
538 if( !m_recentTemplates.empty() )
540
542
543 m_sdbSizerOK->SetDefault();
544
546}
547
548
550{
551 m_searchTimer.Stop();
552 m_refreshTimer.Stop();
553
554 if( m_watcher )
555 {
556 m_watcher->RemoveAll();
557 m_watcher->SetOwner( nullptr );
558 delete m_watcher;
559 m_watcher = nullptr;
560 }
561}
562
563
565{
566 m_state = aState;
567
568 switch( aState )
569 {
571 m_panelMRU->Show();
572 m_panelPreview->Hide();
573 m_btnBack->Enable( false );
574 break;
575
577 m_panelMRU->Hide();
578 m_panelPreview->Show();
579 m_btnBack->Enable( true );
580 break;
581
583 m_panelMRU->Show();
584 m_panelPreview->Show();
585 m_btnBack->Enable( true );
586 break;
587 }
588
589 Layout();
590}
591
592
594{
595 // Clear existing MRU widgets
596 for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
597 {
598 m_sizerMRU->Detach( widget );
599 widget->Destroy();
600 }
601
602 m_mruWidgets.clear();
603
604 for( const wxString& path : m_recentTemplates )
605 {
606 wxFileName templateDir;
607 templateDir.AssignDir( path );
608
609 if( !templateDir.DirExists() )
610 continue;
611
612 PROJECT_TEMPLATE templ( path );
613
614 wxString* title = templ.GetTitle();
615 wxBitmap* icon = templ.GetIcon();
616
617 wxBitmap scaledIcon;
618
619 if( icon && icon->IsOk() )
620 {
621 wxImage img = icon->ConvertToImage();
622 img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
623 scaledIcon = wxBitmap( img );
624 }
625 else
626 {
627 scaledIcon = KiBitmap( BITMAPS::icon_kicad );
628 wxImage img = scaledIcon.ConvertToImage();
629 img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
630 scaledIcon = wxBitmap( img );
631 }
632
633 wxString displayTitle = title ? *title : templateDir.GetDirs().Last();
634
636 displayTitle, scaledIcon );
637 m_sizerMRU->Add( mruWidget, 0, wxEXPAND | wxBOTTOM, 2 );
638 m_mruWidgets.push_back( mruWidget );
639 }
640
641 m_scrolledMRU->FitInside();
642 m_scrolledMRU->Layout();
643 m_panelMRU->Layout();
644}
645
646
648{
649 wxLogTrace( traceTemplateSelector, "BuildTemplateList() called" );
650
651 // Clear existing template widgets
652 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
653 {
654 m_sizerTemplateList->Detach( widget );
655 widget->Destroy();
656 }
657
658 m_templateWidgets.clear();
659 m_templates.clear();
660
661 auto scanDirectory = [this]( const wxString& aPath, bool aIsUser )
662 {
663 if( aPath.IsEmpty() )
664 return;
665
666 wxDir dir;
667
668 if( !dir.Open( aPath ) )
669 return;
670
671 wxLogTrace( traceTemplateSelector, "Scanning directory: %s (user=%d)", aPath, aIsUser );
672
673 if( dir.HasSubDirs( "meta" ) )
674 {
675 auto templ = std::make_unique<PROJECT_TEMPLATE>( aPath );
676 wxFileName htmlFile = templ->GetHtmlFile();
677 wxString description = ExtractDescription( htmlFile );
678
680 widget->SetTemplate( templ.get() );
681 widget->SetDescription( description );
682 widget->SetIsUserTemplate( aIsUser );
683
684 m_templates.push_back( std::move( templ ) );
685 m_templateWidgets.push_back( widget );
686 }
687 else
688 {
689 wxString subName;
690 bool cont = dir.GetFirst( &subName, wxEmptyString, wxDIR_DIRS );
691
692 while( cont )
693 {
694 wxString subFull = aPath + wxFileName::GetPathSeparator() + subName;
695 wxDir subDir;
696
697 if( subDir.Open( subFull ) )
698 {
699 auto templ = std::make_unique<PROJECT_TEMPLATE>( subFull );
700 wxFileName htmlFile = templ->GetHtmlFile();
701 wxString description = ExtractDescription( htmlFile );
702
704 widget->SetTemplate( templ.get() );
705 widget->SetDescription( description );
706 widget->SetIsUserTemplate( aIsUser );
707
708 m_templates.push_back( std::move( templ ) );
709 m_templateWidgets.push_back( widget );
710 }
711
712 cont = dir.GetNext( &subName );
713 }
714 }
715 };
716
717 scanDirectory( m_userTemplatesPath, true );
718 scanDirectory( m_systemTemplatesPath, false );
719
720 // Sort alphabetically with "Default" first
721 std::sort( m_templateWidgets.begin(), m_templateWidgets.end(),
723 {
724 wxString titleA = *a->GetTemplate()->GetTitle();
725 wxString titleB = *b->GetTemplate()->GetTitle();
726
727 if( titleA.CmpNoCase( "default" ) == 0 )
728 return true;
729
730 if( titleB.CmpNoCase( "default" ) == 0 )
731 return false;
732
733 return titleA.CmpNoCase( titleB ) < 0;
734 } );
735
736 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
737 {
738 m_sizerTemplateList->Add( widget, 0, wxEXPAND | wxBOTTOM, 8 );
739 }
740
741 ApplyFilter();
742
743 // Force initial sizing of widgets after layout
744 CallAfter(
745 [this]()
746 {
747 wxSizeEvent evt( m_scrolledTemplates->GetSize() );
749 } );
750
751 wxLogTrace( traceTemplateSelector, "BuildTemplateList() found %zu templates",
752 m_templateWidgets.size() );
753}
754
755
757{
758 int filterChoice = m_filterChoice->GetSelection();
759 wxString searchText = m_searchCtrl->GetValue().Lower();
760
761 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
762 {
763 bool matchesFilter = true;
764
765 if( filterChoice == 1 && !widget->IsUserTemplate() )
766 matchesFilter = false;
767 else if( filterChoice == 2 && widget->IsUserTemplate() )
768 matchesFilter = false;
769
770 bool matchesSearch = true;
771
772 if( !searchText.IsEmpty() )
773 {
774 wxString title = widget->GetTemplate()->GetTitle()->Lower();
775 wxString description = widget->GetDescription().Lower();
776
777 matchesSearch = title.Contains( searchText ) || description.Contains( searchText );
778 }
779
780 widget->Show( matchesFilter && matchesSearch );
781 }
782
783 m_scrolledTemplates->FitInside();
784 m_scrolledTemplates->Layout();
785 Layout();
786}
787
788
789void DIALOG_TEMPLATE_SELECTOR::OnSearchCtrl( wxCommandEvent& event )
790{
791 m_searchTimer.Stop();
792 m_searchTimer.StartOnce( 200 );
793}
794
795
797{
798 m_searchCtrl->Clear();
799 ApplyFilter();
800}
801
802
803void DIALOG_TEMPLATE_SELECTOR::OnFilterChanged( wxCommandEvent& event )
804{
806
807 if( settings )
808 settings->m_TemplateFilterChoice = m_filterChoice->GetSelection();
809
810 ApplyFilter();
811}
812
813
815{
816 ApplyFilter();
817}
818
819
821{
822 wxString selectedPath;
823
824 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
825 {
826 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
827 htmlFile.RemoveLastDir();
828 selectedPath = htmlFile.GetPath();
829 }
830
831 // Clear pointers before destroying widgets to avoid dangling pointers
832 m_selectedWidget = nullptr;
833 m_selectedTemplate = nullptr;
834
836
837 if( !selectedPath.IsEmpty() )
838 SelectTemplateByPath( selectedPath );
839}
840
841
842void DIALOG_TEMPLATE_SELECTOR::OnBackClicked( wxCommandEvent& event )
843{
844 if( m_selectedWidget )
845 m_selectedWidget->Unselect();
846
847 m_selectedWidget = nullptr;
848 m_selectedTemplate = nullptr;
849
852}
853
854
856{
857 if( m_selectedWidget != nullptr && m_selectedWidget != aWidget )
858 m_selectedWidget->Unselect();
859
860 m_selectedWidget = aWidget;
861 m_selectedTemplate = aWidget ? aWidget->GetTemplate() : nullptr;
862
864
867}
868
869
871{
872 SelectTemplateByPath( aPath, false );
873}
874
875
876void DIALOG_TEMPLATE_SELECTOR::SelectTemplateByPath( const wxString& aPath, bool aKeepMRUVisible )
877{
878 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
879 {
880 if( widget->GetTemplate() )
881 {
882 wxFileName htmlFile = widget->GetTemplate()->GetHtmlFile();
883 htmlFile.RemoveLastDir();
884
885 if( htmlFile.GetPath() == aPath )
886 {
887 if( aKeepMRUVisible )
888 {
889 // Select the widget but keep MRU visible, don't show preview
890 if( m_selectedWidget != nullptr && m_selectedWidget != widget )
891 m_selectedWidget->Unselect();
892
893 m_selectedWidget = widget;
894 m_selectedTemplate = widget->GetTemplate();
895
896 widget->SelectWithoutStateChange();
897
898 // Scroll the widget into view
899 int y = 0;
900 int scrollRate = 0;
901 widget->GetPosition( nullptr, &y );
902 m_scrolledTemplates->GetScrollPixelsPerUnit( nullptr, &scrollRate );
903
904 if( scrollRate > 0 )
905 m_scrolledTemplates->Scroll( -1, y / scrollRate );
906 }
907 else
908 {
909 widget->Select();
910 }
911
912 return;
913 }
914 }
915 }
916
917 wxLogTrace( traceTemplateSelector, "SelectTemplateByPath: template not found at %s", aPath );
918}
919
920
922{
923 if( m_webviewPanel )
924 return;
925
926 // Create the WEBVIEW_PANEL lazily to avoid WebKit JavaScript VM initialization issues
927 // when the dialog is opened from a coroutine context.
928 m_webviewPanel = new WEBVIEW_PANEL( m_webviewPlaceholder, wxID_ANY, wxDefaultPosition,
929 wxDefaultSize, wxTAB_TRAVERSAL );
930
931 wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
932 sizer->Add( m_webviewPanel, 1, wxEXPAND );
933 m_webviewPlaceholder->SetSizer( sizer );
934 m_webviewPlaceholder->Layout();
935
936 m_webviewPanel->BindLoadedEvent();
937
938 if( m_webviewPanel->GetWebView() )
939 {
940 Bind( wxEVT_WEBVIEW_LOADED, &DIALOG_TEMPLATE_SELECTOR::OnWebViewLoaded, this,
941 m_webviewPanel->GetWebView()->GetId() );
942 }
943}
944
945
947{
948 if( !aTemplate )
949 return;
950
952
953 wxFileName htmlFile = aTemplate->GetHtmlFile();
954
955 if( htmlFile.FileExists() && htmlFile.IsFileReadable() )
956 {
958 wxString url = wxFileName::FileNameToURL( htmlFile );
959 m_webviewPanel->LoadURL( url );
960 }
961 else
962 {
963 m_loadingExternalHtml = false;
964 wxString html = GetTemplateInfoHtml( *aTemplate->GetTitle(), KIPLATFORM::UI::IsDarkTheme() );
965 m_webviewPanel->SetPage( html );
966 }
967}
968
969
976
977
979{
980 if( m_watcher )
981 {
982 m_watcher->RemoveAll();
983 delete m_watcher;
984 m_watcher = nullptr;
985 }
986
988 m_watcher->SetOwner( this );
989
990 wxLogNull logNo;
991
992 if( !m_userTemplatesPath.IsEmpty() )
993 {
994 wxFileName userDir;
995 userDir.AssignDir( m_userTemplatesPath );
996
997 if( userDir.DirExists() )
998 {
999 m_watcher->Add( userDir );
1000 wxLogTrace( traceTemplateSelector, "Watching user templates: %s", m_userTemplatesPath );
1001 }
1002 }
1003
1004 if( !m_systemTemplatesPath.IsEmpty() )
1005 {
1006 wxFileName systemDir;
1007 systemDir.AssignDir( m_systemTemplatesPath );
1008
1009 if( systemDir.DirExists() )
1010 {
1011 m_watcher->Add( systemDir );
1012 wxLogTrace( traceTemplateSelector, "Watching system templates: %s", m_systemTemplatesPath );
1013 }
1014 }
1015}
1016
1017
1018void DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent( wxFileSystemWatcherEvent& event )
1019{
1020 if( !m_watcher )
1021 return;
1022
1023 wxLogTrace( traceTemplateSelector, "File system event detected" );
1024
1025 // wxFileSystemWatcher may fire events from a worker thread on some platforms.
1026 // Use CallAfter to marshal timer operations to the main thread.
1027 CallAfter( [this]()
1028 {
1029 m_refreshTimer.Stop();
1030 m_refreshTimer.StartOnce( 500 );
1031 } );
1032}
1033
1034
1035void DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged( wxSysColourChangedEvent& event )
1036{
1037 event.Skip();
1038
1039 // Update background colors for the templates panel
1040 m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1041
1042 // Update all template widgets
1043 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1044 {
1045 if( widget->IsSelected() )
1046 {
1047 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
1048 }
1049 else
1050 {
1051 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1052 }
1053
1054 widget->Refresh();
1055 }
1056
1057 // Update all MRU widgets
1058 for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
1059 {
1060 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1061 widget->Refresh();
1062 }
1063
1064 m_scrolledTemplates->Refresh();
1065 m_scrolledMRU->Refresh();
1066}
1067
1068
1070{
1071 event.Skip();
1072
1073 // Get the client width of the scrolled window
1074 int clientWidth = m_scrolledTemplates->GetClientSize().GetWidth();
1075
1076 if( clientWidth <= 0 )
1077 return;
1078
1079 // Force each widget to rewrap its text at the new width
1080 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1081 {
1082 wxSizeEvent sizeEvt( wxSize( clientWidth, -1 ) );
1083 widget->GetEventHandler()->ProcessEvent( sizeEvt );
1084 }
1085
1086 m_scrolledTemplates->FitInside();
1087}
1088
1089
1091{
1092 event.Skip();
1093
1094 // Safety check - ensure webview panel is valid
1095 if( !m_webviewPanel || !m_webviewPanel->GetWebView() )
1096 return;
1097
1098 // If we loaded an external HTML file and we're in dark mode, inject the dark mode class.
1099 // External templates may not have the kicad-dark class pre-set in their HTML.
1101 {
1102 wxString script = wxS(
1103 "(function() {"
1104 " document.body.classList.add('kicad-dark');"
1105 " var style = document.createElement('style');"
1106 " style.textContent = '"
1107 " :root {"
1108 " --bg-primary: #FFFFFF;"
1109 " --bg-secondary: #F3F3F3;"
1110 " --bg-elevated: #FFFFFF;"
1111 " --text-primary: #545454;"
1112 " --text-secondary: #848484;"
1113 " --accent: #1A81C4;"
1114 " --accent-subtle: rgba(26, 129, 196, 0.08);"
1115 " --border: #E0E0E0;"
1116 " --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);"
1117 " }"
1118 " body.kicad-dark {"
1119 " --bg-primary: #1E1E1E;"
1120 " --bg-secondary: #2D2D2D;"
1121 " --bg-elevated: #333333;"
1122 " --text-primary: #DED3DD;"
1123 " --text-secondary: #848484;"
1124 " --accent: #42B8EB;"
1125 " --accent-subtle: rgba(66, 184, 235, 0.1);"
1126 " --border: #404040;"
1127 " --shadow: 0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.15);"
1128 " }"
1129 " body.kicad-dark {"
1130 " background: var(--bg-primary) !important;"
1131 " color: var(--text-primary) !important;"
1132 " }"
1133 " ';"
1134 " document.head.appendChild(style);"
1135 "})();"
1136 );
1137 m_webviewPanel->RunScriptAsync( script );
1138 }
1139}
1140
1141
1143{
1144 wxString selectedPath;
1145
1146 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
1147 {
1148 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
1149 htmlFile.RemoveLastDir();
1150 selectedPath = htmlFile.GetPath();
1151 }
1152
1153 // Clear pointers before destroying widgets to avoid dangling pointers
1154 m_selectedWidget = nullptr;
1155 m_selectedTemplate = nullptr;
1156
1158
1159 if( !selectedPath.IsEmpty() )
1160 SelectTemplateByPath( selectedPath );
1161}
1162
1163
1164wxString DIALOG_TEMPLATE_SELECTOR::ExtractDescription( const wxFileName& aHtmlFile )
1165{
1166 if( !aHtmlFile.FileExists() || !aHtmlFile.IsFileReadable() )
1167 return wxEmptyString;
1168
1169 wxTextFile file( aHtmlFile.GetFullPath() );
1170
1171 if( !file.Open() )
1172 return wxEmptyString;
1173
1174 wxString content;
1175
1176 for( wxString line = file.GetFirstLine(); !file.Eof(); line = file.GetNextLine() )
1177 {
1178 content += line + wxT( " " );
1179 }
1180
1181 file.Close();
1182
1183 // Try to extract text from the meta description tag
1184 wxRegEx reMetaDesc( wxT( "<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']*)[\"']" ),
1185 wxRE_ICASE );
1186
1187 if( reMetaDesc.Matches( content ) )
1188 return reMetaDesc.GetMatch( content, 1 );
1189
1190 // Fallback to first paragraph tag (allowing nested HTML tags)
1191 wxRegEx reParagraph( wxT( "<p[^>]*>(.*?)</p>" ), wxRE_ICASE );
1192
1193 if( reParagraph.Matches( content ) )
1194 {
1195 wxString desc = reParagraph.GetMatch( content, 1 );
1196
1197 // Strip nested HTML tags
1198 wxRegEx reTags( wxT( "<[^>]*>" ) );
1199 reTags.ReplaceAll( &desc, wxT( " " ) );
1200
1201 // Decode common HTML entities
1202 desc.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1203 desc.Replace( wxT( "&amp;" ), wxT( "&" ) );
1204 desc.Replace( wxT( "&lt;" ), wxT( "<" ) );
1205 desc.Replace( wxT( "&gt;" ), wxT( ">" ) );
1206 desc.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1207
1208 // Normalize whitespace
1209 wxRegEx reWhitespace( wxT( "\\s+" ) );
1210 reWhitespace.ReplaceAll( &desc, wxT( " " ) );
1211
1212 desc = desc.Trim().Trim( false );
1213
1214 if( !desc.IsEmpty() )
1215 return desc;
1216 }
1217
1218 // Final fallback: extract text content from body, stripping all HTML tags
1219 wxRegEx reBody( wxT( "<body[^>]*>(.*)</body>" ), wxRE_ICASE );
1220
1221 if( reBody.Matches( content ) )
1222 {
1223 wxString body = reBody.GetMatch( content, 1 );
1224
1225 // Strip all HTML tags
1226 wxRegEx reTags( wxT( "<[^>]*>" ) );
1227 reTags.ReplaceAll( &body, wxT( " " ) );
1228
1229 // Decode common HTML entities
1230 body.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1231 body.Replace( wxT( "&amp;" ), wxT( "&" ) );
1232 body.Replace( wxT( "&lt;" ), wxT( "<" ) );
1233 body.Replace( wxT( "&gt;" ), wxT( ">" ) );
1234 body.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1235
1236 // Normalize whitespace
1237 wxRegEx reWhitespace( wxT( "\\s+" ) );
1238 reWhitespace.ReplaceAll( &body, wxT( " " ) );
1239
1240 body = body.Trim().Trim( false );
1241
1242 // Return up to 250 characters
1243 if( body.Length() > 250 )
1244 return body.Left( 250 ) + wxS( "..." );
1245
1246 return body;
1247 }
1248
1249 return wxEmptyString;
1250}
1251
1252
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:116
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:104
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)
DIALOG_TEMPLATE_SELECTOR(wxWindow *aParent, const wxPoint &aPos, const wxSize &aSize, const wxString &aUserTemplatesPath, const wxString &aSystemTemplatesPath, const std::vector< wxString > &aRecentTemplates)
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)
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:132
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=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 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:230
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:202
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:49
KICOMMON_API wxFont GetInfoFont(wxWindow *aWindow)
void Refresh()
Update the board display after modifying it by a python script (note: it is automatically called by a...
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
#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.