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