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
699 // Only treat subdirectories that contain a meta directory as templates.
700 // Directories without meta (e.g. regular project directories the user
701 // placed in the template path) are silently skipped.
702 wxFileName metaTest;
703 metaTest.AssignDir( subFull );
704 metaTest.AppendDir( METADIR );
705
706 if( metaTest.DirExists() )
707 {
708 auto templ = std::make_unique<PROJECT_TEMPLATE>( subFull );
709 wxFileName htmlFile = templ->GetHtmlFile();
710 wxString description = templ->GetError().IsEmpty() ? ExtractDescription( htmlFile ) : templ->GetError();
711
712 TEMPLATE_WIDGET* widget =
714 widget->SetTemplate( templ.get() );
715 widget->SetDescription( description );
716 widget->SetIsUserTemplate( aIsUser );
717
718 m_templates.push_back( std::move( templ ) );
719 m_templateWidgets.push_back( widget );
720 }
721
722 cont = dir.GetNext( &subName );
723 }
724 }
725 };
726
727 scanDirectory( m_userTemplatesPath, true );
728 scanDirectory( m_systemTemplatesPath, false );
729
730 // Sort alphabetically with "Default" first
731 std::sort( m_templateWidgets.begin(), m_templateWidgets.end(),
733 {
734 wxString titleA = *a->GetTemplate()->GetTitle();
735 wxString titleB = *b->GetTemplate()->GetTitle();
736 int cmp = titleA.CmpNoCase( titleB );
737
738 if( cmp == 0 )
739 return false;
740
741 if( titleA.CmpNoCase( "default" ) == 0 )
742 return true;
743 else if( titleB.CmpNoCase( "default" ) == 0 )
744 return false;
745
746 return cmp < 0;
747 } );
748
749 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
750 {
751 m_sizerTemplateList->Add( widget, 0, wxEXPAND | wxBOTTOM, 8 );
752 }
753
754 ApplyFilter();
755
756 // Force initial sizing of widgets after layout
757 CallAfter(
758 [this]()
759 {
760 wxSizeEvent evt( m_scrolledTemplates->GetSize() );
762 } );
763
764 wxLogTrace( traceTemplateSelector, "BuildTemplateList() found %zu templates", m_templateWidgets.size() );
765}
766
767
769{
770 int filterChoice = m_filterChoice->GetSelection();
771 wxString searchText = m_searchCtrl->GetValue().Lower();
772
773 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
774 {
775 bool matchesFilter = true;
776
777 if( filterChoice == 1 && !widget->IsUserTemplate() )
778 matchesFilter = false;
779 else if( filterChoice == 2 && widget->IsUserTemplate() )
780 matchesFilter = false;
781
782 bool matchesSearch = true;
783
784 if( !searchText.IsEmpty() )
785 {
786 wxString title = widget->GetTemplate()->GetTitle()->Lower();
787 wxString description = widget->GetDescription().Lower();
788
789 matchesSearch = title.Contains( searchText ) || description.Contains( searchText );
790 }
791
792 widget->Show( matchesFilter && matchesSearch );
793 }
794
795 m_scrolledTemplates->FitInside();
796 m_scrolledTemplates->Layout();
797 Layout();
798}
799
800
801void DIALOG_TEMPLATE_SELECTOR::OnSearchCtrl( wxCommandEvent& event )
802{
803 m_searchTimer.Stop();
804 m_searchTimer.StartOnce( 200 );
805}
806
807
809{
810 m_searchCtrl->Clear();
811 ApplyFilter();
812}
813
814
815void DIALOG_TEMPLATE_SELECTOR::OnFilterChanged( wxCommandEvent& event )
816{
818
819 if( settings )
820 settings->m_TemplateFilterChoice = m_filterChoice->GetSelection();
821
822 ApplyFilter();
823}
824
825
827{
828 ApplyFilter();
829}
830
831
833{
834 wxString selectedPath;
835
836 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
837 {
838 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
839 htmlFile.RemoveLastDir();
840 selectedPath = htmlFile.GetPath();
841 }
842
843 // Clear pointers before destroying widgets to avoid dangling pointers
844 m_selectedWidget = nullptr;
845 m_selectedTemplate = nullptr;
846
848
849 if( !selectedPath.IsEmpty() )
850 SelectTemplateByPath( selectedPath );
851}
852
853
854void DIALOG_TEMPLATE_SELECTOR::OnBackClicked( wxCommandEvent& event )
855{
856 if( m_selectedWidget )
857 m_selectedWidget->Unselect();
858
859 m_selectedWidget = nullptr;
860 m_selectedTemplate = nullptr;
861
864}
865
866
868{
869 if( m_selectedWidget != nullptr && m_selectedWidget != aWidget )
870 m_selectedWidget->Unselect();
871
872 m_selectedWidget = aWidget;
873 m_selectedTemplate = aWidget ? aWidget->GetTemplate() : nullptr;
874
876
879}
880
881
883{
884 SelectTemplateByPath( aPath, false );
885}
886
887
888void DIALOG_TEMPLATE_SELECTOR::SelectTemplateByPath( const wxString& aPath, bool aKeepMRUVisible )
889{
890 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
891 {
892 if( widget->GetTemplate() )
893 {
894 wxFileName htmlFile = widget->GetTemplate()->GetHtmlFile();
895 htmlFile.RemoveLastDir();
896
897 if( htmlFile.GetPath() == aPath )
898 {
899 if( aKeepMRUVisible )
900 {
901 // Select the widget but keep MRU visible, don't show preview
902 if( m_selectedWidget != nullptr && m_selectedWidget != widget )
903 m_selectedWidget->Unselect();
904
905 m_selectedWidget = widget;
906 m_selectedTemplate = widget->GetTemplate();
907
908 widget->SelectWithoutStateChange();
909
910 // Scroll the widget into view
911 int y = 0;
912 int scrollRate = 0;
913 widget->GetPosition( nullptr, &y );
914 m_scrolledTemplates->GetScrollPixelsPerUnit( nullptr, &scrollRate );
915
916 if( scrollRate > 0 )
917 m_scrolledTemplates->Scroll( -1, y / scrollRate );
918 }
919 else
920 {
921 widget->Select();
922 }
923
924 return;
925 }
926 }
927 }
928
929 wxLogTrace( traceTemplateSelector, "SelectTemplateByPath: template not found at %s", aPath );
930}
931
932
934{
935 if( m_webviewPanel )
936 return;
937
938 // Create the WEBVIEW_PANEL lazily to avoid WebKit JavaScript VM initialization issues
939 // when the dialog is opened from a coroutine context.
940 m_webviewPanel = new WEBVIEW_PANEL( m_webviewPlaceholder, wxID_ANY, wxDefaultPosition, wxDefaultSize,
941 wxTAB_TRAVERSAL );
942
943 wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
944 sizer->Add( m_webviewPanel, 1, wxEXPAND );
945 m_webviewPlaceholder->SetSizer( sizer );
946 m_webviewPlaceholder->Layout();
947
948 m_webviewPanel->BindLoadedEvent();
949
950 if( m_webviewPanel->GetWebView() )
951 {
952 Bind( wxEVT_WEBVIEW_LOADED, &DIALOG_TEMPLATE_SELECTOR::OnWebViewLoaded, this,
953 m_webviewPanel->GetWebView()->GetId() );
954 }
955}
956
957
959{
960 if( !aTemplate )
961 return;
962
964
965 wxFileName htmlFile = aTemplate->GetHtmlFile();
966
967 if( htmlFile.FileExists() && htmlFile.IsFileReadable() )
968 {
970 wxString url = wxFileName::FileNameToURL( htmlFile );
971 m_webviewPanel->LoadURL( url );
972 }
973 else
974 {
975 m_loadingExternalHtml = false;
976 wxString html = GetTemplateInfoHtml( *aTemplate->GetTitle(), KIPLATFORM::UI::IsDarkTheme() );
977 m_webviewPanel->SetPage( html );
978 }
979}
980
981
988
989
991{
992 if( m_watcher )
993 {
994 m_watcher->RemoveAll();
995 delete m_watcher;
996 m_watcher = nullptr;
997 }
998
1000 m_watcher->SetOwner( this );
1001
1002 wxLogNull logNo;
1003
1004 if( !m_userTemplatesPath.IsEmpty() )
1005 {
1006 wxFileName userDir;
1007 userDir.AssignDir( m_userTemplatesPath );
1008
1009 if( userDir.DirExists() )
1010 {
1011 m_watcher->Add( userDir );
1012 wxLogTrace( traceTemplateSelector, "Watching user templates: %s", m_userTemplatesPath );
1013 }
1014 }
1015
1016 if( !m_systemTemplatesPath.IsEmpty() )
1017 {
1018 wxFileName systemDir;
1019 systemDir.AssignDir( m_systemTemplatesPath );
1020
1021 if( systemDir.DirExists() )
1022 {
1023 m_watcher->Add( systemDir );
1024 wxLogTrace( traceTemplateSelector, "Watching system templates: %s", m_systemTemplatesPath );
1025 }
1026 }
1027}
1028
1029
1030void DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent( wxFileSystemWatcherEvent& event )
1031{
1032 if( !m_watcher )
1033 return;
1034
1035 wxLogTrace( traceTemplateSelector, "File system event detected" );
1036
1037 // wxFileSystemWatcher may fire events from a worker thread on some platforms.
1038 // Use CallAfter to marshal timer operations to the main thread.
1039 CallAfter(
1040 [this]()
1041 {
1042 m_refreshTimer.Stop();
1043 m_refreshTimer.StartOnce( 500 );
1044 } );
1045}
1046
1047
1048void DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged( wxSysColourChangedEvent& event )
1049{
1050 event.Skip();
1051
1052 // Update background colors for the templates panel
1053 m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1054
1055 // Update all template widgets
1056 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1057 {
1058 if( widget->IsSelected() )
1059 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
1060 else
1061 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1062
1063 widget->Refresh();
1064 }
1065
1066 // Update all MRU widgets
1067 for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
1068 {
1069 widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
1070 widget->Refresh();
1071 }
1072
1073 m_scrolledTemplates->Refresh();
1074 m_scrolledMRU->Refresh();
1075}
1076
1077
1079{
1080 event.Skip();
1081
1082 // Get the client width of the scrolled window
1083 int clientWidth = m_scrolledTemplates->GetClientSize().GetWidth();
1084
1085 if( clientWidth <= 0 )
1086 return;
1087
1088 // Force each widget to rewrap its text at the new width
1089 for( TEMPLATE_WIDGET* widget : m_templateWidgets )
1090 {
1091 wxSizeEvent sizeEvt( wxSize( clientWidth, -1 ) );
1092 widget->GetEventHandler()->ProcessEvent( sizeEvt );
1093 }
1094
1095 m_scrolledTemplates->FitInside();
1096}
1097
1098
1100{
1101 event.Skip();
1102
1103 // Safety check - ensure webview panel is valid
1104 if( !m_webviewPanel || !m_webviewPanel->GetWebView() )
1105 return;
1106
1107 // Add style to external documets
1109 {
1110 wxString script = wxString::Format( wxS( R"(
1111( function()
1112{
1113 var style = document.createElement( 'style' );
1114 style.textContent = `
1115%s
1116 `;
1117 document.head.appendChild( style );
1118} )();
1119 )" ), GetCommonStyles() );
1120
1121#if !defined( __MINGW32__ ) // RunScriptAsync() is not supported on MINGW build
1122 if( m_webviewPanel->GetBackend() != wxWebViewBackendIE )
1123 m_webviewPanel->RunScriptAsync( script );
1124#endif
1125 }
1126}
1127
1128
1130{
1131 wxString selectedPath;
1132
1133 if( m_selectedWidget && m_selectedWidget->GetTemplate() )
1134 {
1135 wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
1136 htmlFile.RemoveLastDir();
1137 selectedPath = htmlFile.GetPath();
1138 }
1139
1140 // Clear pointers before destroying widgets to avoid dangling pointers
1141 m_selectedWidget = nullptr;
1142 m_selectedTemplate = nullptr;
1143
1145
1146 if( !selectedPath.IsEmpty() )
1147 SelectTemplateByPath( selectedPath );
1148}
1149
1150
1151wxString DIALOG_TEMPLATE_SELECTOR::ExtractDescription( const wxFileName& aHtmlFile )
1152{
1153 if( !aHtmlFile.FileExists() || !aHtmlFile.IsFileReadable() )
1154 return wxEmptyString;
1155
1156 wxTextFile file( aHtmlFile.GetFullPath() );
1157
1158 if( !file.Open() )
1159 return wxEmptyString;
1160
1161 wxString content;
1162
1163 for( wxString line = file.GetFirstLine(); !file.Eof(); line = file.GetNextLine() )
1164 {
1165 content += line + wxT( " " );
1166 }
1167
1168 file.Close();
1169
1170 // Try to extract text from the meta description tag
1171 wxRegEx reMetaDesc( wxT( "<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']*)[\"']" ),
1172 wxRE_ICASE );
1173
1174 if( reMetaDesc.Matches( content ) )
1175 return reMetaDesc.GetMatch( content, 1 );
1176
1177 // Fallback to first paragraph tag (allowing nested HTML tags)
1178 wxRegEx reParagraph( wxT( "<p[^>]*>(.*?)</p>" ), wxRE_ICASE );
1179
1180 if( reParagraph.Matches( content ) )
1181 {
1182 wxString desc = reParagraph.GetMatch( content, 1 );
1183
1184 // Strip nested HTML tags
1185 wxRegEx reTags( wxT( "<[^>]*>" ) );
1186 reTags.ReplaceAll( &desc, wxT( " " ) );
1187
1188 // Decode common HTML entities
1189 desc.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1190 desc.Replace( wxT( "&amp;" ), wxT( "&" ) );
1191 desc.Replace( wxT( "&lt;" ), wxT( "<" ) );
1192 desc.Replace( wxT( "&gt;" ), wxT( ">" ) );
1193 desc.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1194
1195 // Normalize whitespace
1196 wxRegEx reWhitespace( wxT( "\\s+" ) );
1197 reWhitespace.ReplaceAll( &desc, wxT( " " ) );
1198
1199 desc = desc.Trim().Trim( false );
1200
1201 if( !desc.IsEmpty() )
1202 return desc;
1203 }
1204
1205 // Final fallback: extract text content from body, stripping all HTML tags
1206 wxRegEx reBody( wxT( "<body[^>]*>(.*)</body>" ), wxRE_ICASE );
1207
1208 if( reBody.Matches( content ) )
1209 {
1210 wxString body = reBody.GetMatch( content, 1 );
1211
1212 // Strip all HTML tags
1213 wxRegEx reTags( wxT( "<[^>]*>" ) );
1214 reTags.ReplaceAll( &body, wxT( " " ) );
1215
1216 // Decode common HTML entities
1217 body.Replace( wxT( "&nbsp;" ), wxT( " " ) );
1218 body.Replace( wxT( "&amp;" ), wxT( "&" ) );
1219 body.Replace( wxT( "&lt;" ), wxT( "<" ) );
1220 body.Replace( wxT( "&gt;" ), wxT( ">" ) );
1221 body.Replace( wxT( "&quot;" ), wxT( "\"" ) );
1222
1223 // Normalize whitespace
1224 wxRegEx reWhitespace( wxT( "\\s+" ) );
1225 reWhitespace.ReplaceAll( &body, wxT( " " ) );
1226
1227 body = body.Trim().Trim( false );
1228
1229 // Return up to 250 characters
1230 if( body.Length() > 250 )
1231 return body.Left( 250 ) + wxS( "..." );
1232
1233 return body;
1234 }
1235
1236 return wxEmptyString;
1237}
1238
1239
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:130
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)
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.