KiCad PCB EDA Suite
Loading...
Searching...
No Matches
project_tree_pane.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 SoftPLC Corporation, Dick Hollenbeck <[email protected]>
5 * Copyright (C) 2012 Jean-Pierre Charras, jp.charras at wanadoo.fr
6 * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, you may find one here:
20 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21 * or you may search the http://www.gnu.org website for the version 2 license,
22 * or you may write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26#include <stack>
27#include <git2.h>
28
29#include <wx/regex.h>
30#include <wx/stdpaths.h>
31#include <wx/string.h>
32#include <wx/msgdlg.h>
33#include <wx/textdlg.h>
34
35#include <advanced_config.h>
36#include <bitmaps.h>
37#include <bitmap_store.h>
38#include <confirm.h>
41#include <gestfich.h>
42#include <macros.h>
43#include <trace_helpers.h>
46#include <core/kicad_algo.h>
47#include <paths.h>
49#include <scoped_set_reset.h>
50#include <string_utils.h>
51#include <launch_ext.h>
52#include <wx/dcclient.h>
53#include <wx/settings.h>
54
68
70
71#include "project_tree_item.h"
72#include "project_tree.h"
73#include "pgm_kicad.h"
74#include "kicad_id.h"
75#include "kicad_manager_frame.h"
76
77#include "project_tree_pane.h"
78#include <widgets/kistatusbar.h>
79
80#include <kiplatform/io.h>
81
82
83/* Note about the project tree build process:
84 * Building the project tree can be *very* long if there are a lot of subdirectories in the
85 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
86 * user's home directory.
87 * So the tree project is built "on demand":
88 * First the tree is built from the current directory and shows files and subdirs.
89 * > First level subdirs trees are built (i.e subdirs contents are not read)
90 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
91 * populated on the fly.
92 */
93
94// list of files extensions listed in the tree project window
95// Add extensions in a compatible regex format to see others files types
96static const wxChar* s_allowedExtensionsToList[] = {
97 wxT( "^.*\\.pro$" ),
98 wxT( "^.*\\.kicad_pro$" ),
99 wxT( "^.*\\.pdf$" ),
100 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
101 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
102 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
103 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
104 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
105 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
106 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
107 wxT( "^.*\\.net$" ), // pcbnew netlist file
108 wxT( "^.*\\.cir$" ), // Spice netlist file
109 wxT( "^.*\\.lib$" ), // Legacy schematic library file
110 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
111 wxT( "^.*\\.txt$" ), // Text files
112 wxT( "^.*\\.md$" ), // Markdown files
113 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
114 wxT( "^.*\\.gbr$" ), // Gerber file
115 wxT( "^.*\\.gbrjob$" ), // Gerber job file
116 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
117 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
118 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
119 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
120 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
121 wxT( "^.*\\.odt$" ),
122 wxT( "^.*\\.htm$" ),
123 wxT( "^.*\\.html$" ),
124 wxT( "^.*\\.rpt$" ), // Report files
125 wxT( "^.*\\.csv$" ), // Report files in comma separated format
126 wxT( "^.*\\.pos$" ), // Footprint position files
127 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
128 wxT( "^.*\\.drl$" ), // Excellon drill files
129 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
130 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
131 wxT( "^.*\\.svg$" ), // SVG print/plot files
132 wxT( "^.*\\.ps$" ), // PostScript plot files
133 wxT( "^.*\\.zip$" ), // Zip archive files
134 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
135 nullptr // end of list
136};
137
138
146BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
147 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
148 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
149 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
156
171
173
174 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
175 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
176END_EVENT_TABLE()
177
178
180 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize,
181 wxNO_BORDER | wxTAB_TRAVERSAL )
182{
183 m_Parent = parent;
184 m_TreeProject = nullptr;
185 m_isRenaming = false;
186 m_selectedItem = nullptr;
187 m_watcherNeedReset = false;
188 m_lastGitStatusUpdate = wxDateTime::Now();
189 m_gitLastError = GIT_ERROR_NONE;
190
191 m_watcher = nullptr;
192 Connect( wxEVT_FSWATCHER,
193 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
194
195 Bind( wxEVT_SYS_COLOUR_CHANGED,
196 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
197
198 /*
199 * Filtering is now inverted: the filters are actually used to _enable_ support
200 * for a given file type.
201 */
202 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
203 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
204
205 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
206
207 ReCreateTreePrj();
208
209}
210
211
213{
214 Disconnect( wxEVT_FSWATCHER,
215 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
216 Unbind( wxEVT_SYS_COLOUR_CHANGED,
217 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
219}
220
221
223{
224 if( m_watcher )
225 {
226 m_watcher->RemoveAll();
227 m_watcher->SetOwner( nullptr );
228 delete m_watcher;
229 m_watcher = nullptr;
230 }
231}
232
233
235{
236 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
237
238 if( tree_data.size() != 1 )
239 return;
240
241 wxString prj_filename = tree_data[0]->GetFileName();
242
243 m_Parent->LoadProject( prj_filename );
244}
245
246
247void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
248{
249 // Get the root directory name:
250 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
251
252 for( PROJECT_TREE_ITEM* item_data : tree_data )
253 {
254 // Ask for the new sub directory name
255 wxString curr_dir = item_data->GetDir();
256
257 if( curr_dir.IsEmpty() )
258 {
259 // Use project path if the tree view path was empty.
260 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
261
262 // As a last resort use the user's documents folder.
263 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
265
266 if( !curr_dir.IsEmpty() )
267 curr_dir += wxFileName::GetPathSeparator();
268 }
269
270 LaunchExternal( curr_dir );
271 }
272}
273
274
275void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
276{
277 // Get the root directory name:
278 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
279
280 for( PROJECT_TREE_ITEM* item_data : tree_data )
281 {
282 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
283
284 // Ask for the new sub directory name
285 wxString curr_dir = item_data->GetDir();
286
287 if( curr_dir.IsEmpty() )
288 curr_dir = prj_dir;
289
290 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
291
292 if( new_dir.IsEmpty() )
293 return;
294
295 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
296
297 if( !wxMkdir( full_dirname ) )
298 return;
299
300 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
301 }
302}
303
304
306{
307 switch( type )
308 {
309 case TREE_FILE_TYPE::LEGACY_PROJECT: return FILEEXT::LegacyProjectFileExtension;
310 case TREE_FILE_TYPE::JSON_PROJECT: return FILEEXT::ProjectFileExtension;
311 case TREE_FILE_TYPE::LEGACY_SCHEMATIC: return FILEEXT::LegacySchematicFileExtension;
312 case TREE_FILE_TYPE::SEXPR_SCHEMATIC: return FILEEXT::KiCadSchematicFileExtension;
313 case TREE_FILE_TYPE::LEGACY_PCB: return FILEEXT::LegacyPcbFileExtension;
314 case TREE_FILE_TYPE::SEXPR_PCB: return FILEEXT::KiCadPcbFileExtension;
315 case TREE_FILE_TYPE::GERBER: return FILEEXT::GerberFileExtensionsRegex;
316 case TREE_FILE_TYPE::GERBER_JOB_FILE: return FILEEXT::GerberJobFileExtension;
317 case TREE_FILE_TYPE::HTML: return FILEEXT::HtmlFileExtension;
318 case TREE_FILE_TYPE::PDF: return FILEEXT::PdfFileExtension;
319 case TREE_FILE_TYPE::TXT: return FILEEXT::TextFileExtension;
320 case TREE_FILE_TYPE::MD: return FILEEXT::MarkdownFileExtension;
321 case TREE_FILE_TYPE::NET: return FILEEXT::NetlistFileExtension;
322 case TREE_FILE_TYPE::NET_SPICE: return FILEEXT::SpiceFileExtension;
323 case TREE_FILE_TYPE::CMP_LINK: return FILEEXT::FootprintAssignmentFileExtension;
324 case TREE_FILE_TYPE::REPORT: return FILEEXT::ReportFileExtension;
325 case TREE_FILE_TYPE::FP_PLACE: return FILEEXT::FootprintPlaceFileExtension;
326 case TREE_FILE_TYPE::DRILL: return FILEEXT::DrillFileExtension;
327 case TREE_FILE_TYPE::DRILL_NC: return "nc";
328 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
329 case TREE_FILE_TYPE::SVG: return FILEEXT::SVGFileExtension;
330 case TREE_FILE_TYPE::DRAWING_SHEET: return FILEEXT::DrawingSheetFileExtension;
331 case TREE_FILE_TYPE::FOOTPRINT_FILE: return FILEEXT::KiCadFootprintFileExtension;
332 case TREE_FILE_TYPE::SCHEMATIC_LIBFILE: return FILEEXT::LegacySymbolLibFileExtension;
333 case TREE_FILE_TYPE::SEXPR_SYMBOL_LIB_FILE: return FILEEXT::KiCadSymbolLibFileExtension;
334 case TREE_FILE_TYPE::DESIGN_RULES: return FILEEXT::DesignRulesFileExtension;
335 case TREE_FILE_TYPE::ZIP_ARCHIVE: return FILEEXT::ArchiveFileExtension;
336 case TREE_FILE_TYPE::JOBSET_FILE: return FILEEXT::KiCadJobSetFileExtension;
337
338 case TREE_FILE_TYPE::ROOT:
339 case TREE_FILE_TYPE::UNKNOWN:
340 case TREE_FILE_TYPE::MAX:
341 case TREE_FILE_TYPE::DIRECTORY: break;
342 }
343
344 return wxEmptyString;
345}
346
347
348std::vector<wxString> getProjects( const wxDir& dir )
349{
350 std::vector<wxString> projects;
351 wxString dir_filename;
352 bool haveFile = dir.GetFirst( &dir_filename );
353
354 while( haveFile )
355 {
356 wxFileName file( dir_filename );
357
358 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
359 || file.GetExt() == FILEEXT::ProjectFileExtension )
360 projects.push_back( file.GetName() );
361
362 haveFile = dir.GetNext( &dir_filename );
363 }
364
365 return projects;
366}
367
368
369static git_repository* get_git_repository_for_file( const char* filename )
370{
371 git_repository* repo = nullptr;
372 git_buf repo_path = GIT_BUF_INIT;
373
374 // Find the repository path for the given file
375 if( git_repository_discover( &repo_path, filename, 0, NULL ) )
376 {
377#if 0
378 printf( "get_git_repository_for_file: %s\n", git_error_last()->message ); fflush( 0 );
379#endif
380 return nullptr;
381 }
382
383 if( git_repository_open( &repo, repo_path.ptr ) )
384 {
385 git_buf_free( &repo_path );
386 return nullptr;
387 }
388
389 // Free the git_buf memory
390 git_buf_free( &repo_path );
391
392 return repo;
393}
394
395
396wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
397 const wxTreeItemId& aParent,
398 std::vector<wxString>* aProjectNames,
399 bool aRecurse )
400{
401 TREE_FILE_TYPE type = TREE_FILE_TYPE::UNKNOWN;
402 wxFileName fn( aName );
403
404 if( KIPLATFORM::IO::IsFileHidden( aName ) )
405 return wxTreeItemId();
406
407 if( wxDirExists( aName ) )
408 {
409 type = TREE_FILE_TYPE::DIRECTORY;
410 }
411 else
412 {
413 // Filter
414 wxRegEx reg;
415 bool addFile = false;
416
417 for( const wxString& m_filter : m_filters )
418 {
419 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
420 wxString::Format( "Regex %s failed to compile.", m_filter ) );
421
422 if( reg.Matches( aName ) )
423 {
424 addFile = true;
425 break;
426 }
427 }
428
429 if( !addFile )
430 return wxTreeItemId();
431
432 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
433 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
434 {
435 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
436
437 if( ext == wxT( "" ) )
438 continue;
439
440 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ),
441 wxRE_ICASE ) && reg.Matches( aName ) )
442 {
443 type = (TREE_FILE_TYPE) i;
444 break;
445 }
446 }
447 }
448
449 wxString file = wxFileNameFromPath( aName );
450 wxFileName currfile( file );
451 wxFileName project( m_Parent->GetProjectFileName() );
452
453 // Ignore legacy projects with the same name as the current project
454 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
455 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
456 {
457 return wxTreeItemId();
458 }
459
460 if( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
461 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) )
462 {
463 if( aProjectNames )
464 {
465 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
466 return wxTreeItemId();
467 }
468 else
469 {
470 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
471 wxDir parentDir( parentTreeItem->GetDir() );
472 std::vector<wxString> projects = getProjects( parentDir );
473
474 if( !alg::contains( projects, currfile.GetName() ) )
475 return wxTreeItemId();
476 }
477 }
478
479 // also check to see if it is already there.
480 wxTreeItemIdValue cookie;
481 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
482
483 while( kid.IsOk() )
484 {
485 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
486
487 if( itemData && itemData->GetFileName() == aName )
488 return itemData->GetId(); // well, we would have added it, but it is already here!
489
490 kid = m_TreeProject->GetNextChild( aParent, cookie );
491 }
492
493 // Only show current files if both legacy and current files are present
494 if( type == TREE_FILE_TYPE::LEGACY_PROJECT || type == TREE_FILE_TYPE::JSON_PROJECT
495 || type == TREE_FILE_TYPE::LEGACY_SCHEMATIC || type == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
496 {
497 kid = m_TreeProject->GetFirstChild( aParent, cookie );
498
499 while( kid.IsOk() )
500 {
501 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
502
503 if( itemData )
504 {
505 wxFileName fname( itemData->GetFileName() );
506
507 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
508 {
509 switch( type )
510 {
511 case TREE_FILE_TYPE::LEGACY_PROJECT:
512 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
513 return wxTreeItemId();
514
515 break;
516
517 case TREE_FILE_TYPE::LEGACY_SCHEMATIC:
518 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
519 return wxTreeItemId();
520
521 break;
522
523 case TREE_FILE_TYPE::JSON_PROJECT:
524 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
525 m_TreeProject->Delete( kid );
526
527 break;
528
529 case TREE_FILE_TYPE::SEXPR_SCHEMATIC:
530 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
531 m_TreeProject->Delete( kid );
532
533 break;
534
535 default:
536 break;
537 }
538 }
539 }
540
541 kid = m_TreeProject->GetNextChild( aParent, cookie );
542 }
543 }
544
545 // Append the item (only appending the filename not the full path):
546 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
547 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
548
549 m_TreeProject->SetItemData( newItemId, data );
550 data->SetState( 0 );
551
552 // Mark root files (files which have the same aName as the project)
553 wxString fileName = currfile.GetName().Lower();
554 wxString projName = project.GetName().Lower();
555
556 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
557 data->SetRootFile( true );
558
559#ifndef __WINDOWS__
560 bool subdir_populated = false;
561#endif
562
563 // This section adds dirs and files found in the subdirs
564 // in this case AddFile is recursive, but for the first level only.
565 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
566 {
567 wxDir dir( aName );
568
569 if( dir.IsOpened() ) // protected dirs will not open properly.
570 {
571 std::vector<wxString> projects = getProjects( dir );
572 wxString dir_filename;
573 bool haveFile = dir.GetFirst( &dir_filename );
574
575 data->SetPopulated( true );
576
577#ifndef __WINDOWS__
578 subdir_populated = aRecurse;
579#endif
580
581 while( haveFile )
582 {
583 // Add name in tree, but do not recurse
584 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
585 addItemToProjectTree( path, newItemId, &projects, false );
586
587 haveFile = dir.GetNext( &dir_filename );
588 }
589 }
590
591 // Sort filenames by alphabetic order
592 m_TreeProject->SortChildren( newItemId );
593 }
594
595#ifndef __WINDOWS__
596 if( subdir_populated )
597 m_watcherNeedReset = true;
598#endif
599
600 return newItemId;
601}
602
603
605{
606 wxString pro_dir = m_Parent->GetProjectFileName();
607
608 if( !m_TreeProject )
609 m_TreeProject = new PROJECT_TREE( this );
610 else
611 m_TreeProject->DeleteAllItems();
612
613 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
614 return;
615
617 {
618 git_repository_free( m_TreeProject->GetGitRepo() );
619 m_TreeProject->SetGitRepo( nullptr );
620 }
621
622 wxFileName fn = pro_dir;
623 bool prjReset = false;
624
625 if( !fn.IsOk() )
626 {
627 fn.Clear();
628 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
629 fn.SetName( NAMELESS_PROJECT );
631 prjReset = true;
632 }
633
634 bool prjOpened = fn.FileExists();
635
636 // Bind the git repository to the project tree (if it exists)
637 if( ADVANCED_CFG::GetCfg().m_EnableGit )
638 {
639 m_TreeProject->SetGitRepo( get_git_repository_for_file( fn.GetPath().c_str() ) );
640 m_TreeProject->GitCommon()->SetPassword( Prj().GetLocalSettings().m_GitRepoPassword );
641 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
642 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
643
644 wxString conn_type = Prj().GetLocalSettings().m_GitRepoType;
645
646 if( conn_type == "https" )
648 else if( conn_type == "ssh" )
650 else
652 }
653
654 // We may have opened a legacy project, in which case GetProjectFileName will return the
655 // name of the migrated (new format) file, which may not have been saved to disk yet.
656 if( !prjOpened && !prjReset )
657 {
659 prjOpened = fn.FileExists();
660
661 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
663 }
664
665 // root tree:
666 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
667 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
668 m_TreeProject->SetItemBold( m_root, true );
669
670 // The main project file is now a JSON file
671 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( TREE_FILE_TYPE::JSON_PROJECT,
672 fn.GetFullPath(), m_TreeProject );
673
674 m_TreeProject->SetItemData( m_root, data );
675
676 // Now adding all current files if available
677 if( prjOpened )
678 {
679 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
680 wxDir dir( pro_dir );
681
682 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
683 {
684 std::vector<wxString> projects = getProjects( dir );
685 wxString filename;
686 bool haveFile = dir.GetFirst( &filename );
687
688 while( haveFile )
689 {
690 if( filename != fn.GetFullName() )
691 {
692 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
693
694 // Add items living in the project directory, and populate the item
695 // if it is a directory (sub directories will be not populated)
696 addItemToProjectTree( name, m_root, &projects, true );
697 }
698
699 haveFile = dir.GetNext( &filename );
700 }
701 }
702 }
703 else
704 {
705 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
706 }
707
708 m_TreeProject->Expand( m_root );
709
710 // Sort filenames by alphabetic order
711 m_TreeProject->SortChildren( m_root );
713}
714
715
717{
718 git_repository* repo = m_TreeProject->GetGitRepo();
719
720 if( !repo )
721 return false;
722
723 git_status_options opts;
724 git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION );
725
726 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
727 opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX
728 | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY;
729
730 git_status_list* status_list = nullptr;
731 int error = git_status_list_new( &status_list, repo, &opts );
732
733 if( error != GIT_OK )
734 return false;
735
736 bool has_changed_files = git_status_list_entrycount( status_list ) > 0;
737 git_status_list_free( status_list );
738 return has_changed_files;
739}
740
741
742void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
743{
744 wxTreeItemId curr_item = Event.GetItem();
745
746 // Ensure item is selected (Under Windows right click does not select the item)
747 m_TreeProject->SelectItem( curr_item );
748
749 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
750
751 bool can_switch_to_project = true;
752 bool can_create_new_directory = true;
753 bool can_open_this_directory = true;
754 bool can_edit = true;
755 bool can_rename = true;
756 bool can_delete = true;
757 bool run_jobs = false;
758
759 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
760 bool vcs_can_commit = hasChangedFiles();
761 bool vcs_can_init = !vcs_has_repo;
762 bool vcs_can_remove = vcs_has_repo;
763 bool vcs_can_fetch = vcs_has_repo && m_TreeProject->GitCommon()->HasPushAndPullRemote();
764 bool vcs_can_push = vcs_can_fetch && m_TreeProject->GitCommon()->HasLocalCommits();
765 bool vcs_can_pull = vcs_can_fetch;
766 bool vcs_can_switch = vcs_has_repo;
767 bool vcs_menu = ADVANCED_CFG::GetCfg().m_EnableGit;
768
769 // Check if the libgit2 library has been successfully initialized
770#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
771 int major, minor, rev;
772 bool libgit_init = ( git_libgit2_version( &major, &minor, &rev ) == GIT_OK );
773#else
774 //Work around libgit2 API change for supporting older platforms
775 bool libgit_init = true;
776#endif
777
778 vcs_menu &= libgit_init;
779
780 if( selection.size() == 0 )
781 return;
782
783 // Remove things that don't make sense for multiple selections
784 if( selection.size() != 1 )
785 {
786 can_switch_to_project = false;
787 can_create_new_directory = false;
788 can_rename = false;
789 }
790
791 for( PROJECT_TREE_ITEM* item : selection )
792 {
793 // Check for empty project
794 if( !item )
795 {
796 can_switch_to_project = false;
797 can_edit = false;
798 can_rename = false;
799 continue;
800 }
801
802 can_delete = item->CanDelete();
803 can_rename = item->CanRename();
804
805 switch( item->GetType() )
806 {
807 case TREE_FILE_TYPE::JSON_PROJECT:
808 case TREE_FILE_TYPE::LEGACY_PROJECT:
809 can_rename = false;
810
811 if( item->GetId() == m_TreeProject->GetRootItem() )
812 {
813 can_switch_to_project = false;
814 }
815 else
816 {
817 can_create_new_directory = false;
818 can_open_this_directory = false;
819 }
820 break;
821
822 case TREE_FILE_TYPE::DIRECTORY:
823 can_switch_to_project = false;
824 can_edit = false;
825 break;
826
827 case TREE_FILE_TYPE::JOBSET_FILE:
828 run_jobs = true;
829 can_edit = false;
831
832 case TREE_FILE_TYPE::SEXPR_SCHEMATIC:
833 case TREE_FILE_TYPE::SEXPR_PCB:
835
836 default:
837 can_switch_to_project = false;
838 can_create_new_directory = false;
839 can_open_this_directory = false;
840
841 break;
842 }
843 }
844
845 wxMenu popup_menu;
846 wxString text;
847 wxString help_text;
848
849 if( can_switch_to_project )
850 {
851 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
852 _( "Close all editors, and switch to the selected project" ),
853 KiBitmap( BITMAPS::open_project ) );
854 popup_menu.AppendSeparator();
855 }
856
857 if( can_create_new_directory )
858 {
859 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
860 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
861 }
862
863 if( can_open_this_directory )
864 {
865 if( selection.size() == 1 )
866 {
867#ifdef __APPLE__
868 text = _( "Reveal in Finder" );
869 help_text = _( "Reveals the directory in a Finder window" );
870#else
871 text = _( "Open Directory in File Explorer" );
872 help_text = _( "Opens the directory in the default system file manager" );
873#endif
874 }
875 else
876 {
877#ifdef __APPLE__
878 text = _( "Reveal in Finder" );
879 help_text = _( "Reveals the directories in a Finder window" );
880#else
881 text = _( "Open Directories in File Explorer" );
882 help_text = _( "Opens the directories in the default system file manager" );
883#endif
884 }
885
886 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
887 KiBitmap( BITMAPS::directory_browser ) );
888 }
889
890 if( can_edit )
891 {
892 if( selection.size() == 1 )
893 help_text = _( "Open the file in a Text Editor" );
894 else
895 help_text = _( "Open files in a Text Editor" );
896
897 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
898 KiBitmap( BITMAPS::editor ) );
899 }
900
901 if( run_jobs && selection.size() == 1 )
902 {
903 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
904 KiBitmap( BITMAPS::exchange ) );
905 }
906
907 if( can_rename )
908 {
909 if( selection.size() == 1 )
910 {
911 text = _( "Rename File..." );
912 help_text = _( "Rename file" );
913 }
914 else
915 {
916 text = _( "Rename Files..." );
917 help_text = _( "Rename files" );
918 }
919
920 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
921 KiBitmap( BITMAPS::right ) );
922 }
923
924 if( can_delete )
925 {
926 if( selection.size() == 1 )
927 help_text = _( "Delete the file and its content" );
928 else
929 help_text = _( "Delete the files and their contents" );
930
931 if( can_switch_to_project
932 || can_create_new_directory
933 || can_open_this_directory
934 || can_edit
935 || can_rename )
936 {
937 popup_menu.AppendSeparator();
938 }
939
940#ifdef __WINDOWS__
941 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
942 KiBitmap( BITMAPS::trash ) );
943#else
944 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
945 KiBitmap( BITMAPS::trash ) );
946#endif
947 }
948
949 if( vcs_menu )
950 {
951 wxMenu* vcs_submenu = new wxMenu();
952 wxMenu* branch_submenu = new wxMenu();
953 wxMenuItem* vcs_menuitem = nullptr;
954
955 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
956 _( "Add Project to Version Control..." ),
957 _( "Initialize a new repository" ) );
958 vcs_menuitem->Enable( vcs_can_init );
959
960
961 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
962 _( "Commit changes to the local repository" ) );
963 vcs_menuitem->Enable( vcs_can_commit );
964
965 vcs_menuitem = vcs_submenu->Append( ID_GIT_PUSH, _( "Push" ),
966 _( "Push committed local changes to remote repository" ) );
967 vcs_menuitem->Enable( vcs_can_push );
968
969 vcs_menuitem = vcs_submenu->Append( ID_GIT_PULL, _( "Pull" ),
970 _( "Pull changes from remote repository into local" ) );
971 vcs_menuitem->Enable( vcs_can_pull );
972
973 vcs_submenu->AppendSeparator();
974
975 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
976 _( "Commit changes to the local repository" ) );
977 vcs_menuitem->Enable( vcs_can_commit );
978
979 vcs_submenu->AppendSeparator();
980
981 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
982 // _( "Show changes between the repository and working tree" ) );
983 // vcs_menuitem->Enable( vcs_can_diff );
984
985 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
986
987 // Skip the first one (that is the current branch)
988 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
989 {
990 wxString msg = _( "Switch to branch " ) + branchNames[ii];
991 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg );
992 vcs_menuitem->Enable( vcs_can_switch );
993 }
994
995 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
996 _( "Switch to a different branch" ) );
997 vcs_menuitem->Enable( vcs_can_switch );
998
999 vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1000
1001 vcs_submenu->AppendSeparator();
1002
1003 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Remove Version Control" ),
1004 _( "Delete all version control files from the project directory." ) );
1005 vcs_menuitem->Enable( vcs_can_remove );
1006
1007 popup_menu.AppendSeparator();
1008 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1009 }
1010
1011 if( popup_menu.GetMenuItemCount() > 0 )
1012 PopupMenu( &popup_menu );
1013}
1014
1015
1017{
1018 wxString editorname = Pgm().GetTextEditor();
1019
1020 if( editorname.IsEmpty() )
1021 {
1022 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1023 return;
1024 }
1025
1026 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1027
1028 for( PROJECT_TREE_ITEM* item_data : tree_data )
1029 {
1030 wxString fullFileName = item_data->GetFileName();
1031
1032 if( !fullFileName.IsEmpty() )
1033 {
1034 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1035 }
1036 }
1037}
1038
1039
1040void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1041{
1042 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1043
1044 for( PROJECT_TREE_ITEM* item_data : tree_data )
1045 item_data->Delete();
1046}
1047
1048
1049void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1050{
1051 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1052 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1053
1054 // XXX: Unnecessary?
1055 if( tree_data.size() != 1 )
1056 return;
1057
1058 wxString buffer = m_TreeProject->GetItemText( curr_item );
1059 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1060 tree_data[0]->GetFileName() );
1061 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1062
1063 if( dlg.ShowModal() != wxID_OK )
1064 return; // canceled by user
1065
1066 buffer = dlg.GetValue();
1067 buffer.Trim( true );
1068 buffer.Trim( false );
1069
1070 if( buffer.IsEmpty() )
1071 return; // empty file name not allowed
1072
1073 tree_data[0]->Rename( buffer, true );
1074 m_isRenaming = true;
1075}
1076
1077
1078void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1079{
1080 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1081
1082 if( tree_data.size() != 1 )
1083 return;
1084
1085 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1086 // there will be more events at least on Windows in this frame that will steal focus from
1087 // any newly launched windows
1088 m_selectedItem = tree_data[0];
1089}
1090
1091
1092void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1093{
1094 // Idle executes once all other events finished processing. This makes it ideal to launch
1095 // a new window without starting Focus wars.
1096 if( m_watcherNeedReset )
1097 {
1098 m_selectedItem = nullptr;
1100 }
1101
1102 if( m_selectedItem != nullptr )
1103 {
1104 // Activate launches a window which may run the event loop on top of us and cause OnIdle
1105 // to get called again, so be sure to block off the activation condition first.
1107 m_selectedItem = nullptr;
1108
1109 item->Activate( this );
1110 }
1111
1112 // Inside this routine, we rate limit to once per 2 seconds
1114}
1115
1116
1117void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1118{
1119 wxTreeItemId itemId = Event.GetItem();
1120 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1121
1122 if( !tree_data )
1123 return;
1124
1125 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1126 return;
1127
1128 // explore list of non populated subdirs, and populate them
1129 wxTreeItemIdValue cookie;
1130 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1131
1132#ifndef __WINDOWS__
1133 bool subdir_populated = false;
1134#endif
1135
1136 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1137 {
1138 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1139
1140 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1141 continue;
1142
1143 if( itemData->IsPopulated() )
1144 continue;
1145
1146 wxString fileName = itemData->GetFileName();
1147 wxDir dir( fileName );
1148
1149 if( dir.IsOpened() )
1150 {
1151 std::vector<wxString> projects = getProjects( dir );
1152 wxString dir_filename;
1153 bool haveFile = dir.GetFirst( &dir_filename );
1154
1155 while( haveFile )
1156 {
1157 // Add name to tree item, but do not recurse in subdirs:
1158 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1159 addItemToProjectTree( name, kid, &projects, false );
1160
1161 haveFile = dir.GetNext( &dir_filename );
1162 }
1163
1164 itemData->SetPopulated( true ); // set state to populated
1165
1166#ifndef __WINDOWS__
1167 subdir_populated = true;
1168#endif
1169 }
1170
1171 // Sort filenames by alphabetic order
1172 m_TreeProject->SortChildren( kid );
1173 }
1174
1175#ifndef __WINDOWS__
1176 if( subdir_populated )
1177 m_watcherNeedReset = true;
1178#endif
1179}
1180
1181
1182std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1183{
1184 wxArrayTreeItemIds selection;
1185 std::vector<PROJECT_TREE_ITEM*> data;
1186
1187 m_TreeProject->GetSelections( selection );
1188
1189 for( auto it = selection.begin(); it != selection.end(); it++ )
1190 {
1191 PROJECT_TREE_ITEM* item = GetItemIdData( *it );
1192
1193 if( !item )
1194 {
1195 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast "
1196 "failed?" ) );
1197 continue;
1198 }
1199
1200 data.push_back( item );
1201 }
1202
1203 return data;
1204}
1205
1206
1208{
1209 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1210}
1211
1212
1213wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1214{
1215 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1216
1217 // If the subdir is the current working directory, return m_root
1218 // in main list:
1219 if( prj_dir == aSubDir )
1220 return m_root;
1221
1222 // The subdir is in the main tree or in a subdir: Locate it
1223 wxTreeItemIdValue cookie;
1224 wxTreeItemId root_id = m_root;
1225 std::stack<wxTreeItemId> subdirs_id;
1226
1227 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1228
1229 while( true )
1230 {
1231 if( ! child.IsOk() )
1232 {
1233 if( subdirs_id.empty() ) // all items were explored
1234 {
1235 root_id = child; // Not found: return an invalid wxTreeItemId
1236 break;
1237 }
1238 else
1239 {
1240 root_id = subdirs_id.top();
1241 subdirs_id.pop();
1242 child = m_TreeProject->GetFirstChild( root_id, cookie );
1243
1244 if( !child.IsOk() )
1245 continue;
1246 }
1247 }
1248
1249 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1250
1251 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1252 {
1253 if( itemData->GetFileName() == aSubDir ) // Found!
1254 {
1255 root_id = child;
1256 break;
1257 }
1258
1259 // child is a subdir, push in list to explore it later
1260 if( itemData->IsPopulated() )
1261 subdirs_id.push( child );
1262 }
1263
1264 child = m_TreeProject->GetNextChild( root_id, cookie );
1265 }
1266
1267 return root_id;
1268}
1269
1270
1271void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1272{
1273 // No need to process events when we're shutting down
1274 if( !m_watcher )
1275 return;
1276
1277 const wxFileName& pathModified = event.GetPath();
1278 wxString subdir = pathModified.GetPath();
1279 wxString fn = pathModified.GetFullPath();
1280
1281 switch( event.GetChangeType() )
1282 {
1283 case wxFSW_EVENT_DELETE:
1284 case wxFSW_EVENT_CREATE:
1285 case wxFSW_EVENT_RENAME:
1287 break;
1288
1289 case wxFSW_EVENT_MODIFY:
1292 case wxFSW_EVENT_ACCESS:
1293 default:
1294 return;
1295 }
1296
1297 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1298
1299 if( !root_id.IsOk() )
1300 return;
1301
1302 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1303 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1304
1305 switch( event.GetChangeType() )
1306 {
1307 case wxFSW_EVENT_CREATE:
1308 {
1309 wxTreeItemId newitem =
1310 addItemToProjectTree( pathModified.GetFullPath(), root_id, nullptr, true );
1311
1312 // If we are in the process of renaming a file, select the new one
1313 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1314 // pair of DELETE and CREATE events.
1315 if( m_isRenaming && newitem.IsOk() )
1316 {
1317 m_TreeProject->SelectItem( newitem );
1318 m_isRenaming = false;
1319 }
1320 }
1321 break;
1322
1323 case wxFSW_EVENT_DELETE:
1324 while( kid.IsOk() )
1325 {
1326 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1327
1328 if( itemData && itemData->GetFileName() == fn )
1329 {
1330 m_TreeProject->Delete( kid );
1331 return;
1332 }
1333 kid = m_TreeProject->GetNextChild( root_id, cookie );
1334 }
1335 break;
1336
1337 case wxFSW_EVENT_RENAME :
1338 {
1339 const wxFileName& newpath = event.GetNewPath();
1340 wxString newdir = newpath.GetPath();
1341 wxString newfn = newpath.GetFullPath();
1342
1343 while( kid.IsOk() )
1344 {
1345 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1346
1347 if( itemData && itemData->GetFileName() == fn )
1348 {
1349 m_TreeProject->Delete( kid );
1350 break;
1351 }
1352
1353 kid = m_TreeProject->GetNextChild( root_id, cookie );
1354 }
1355
1356 // Add the new item only if it is not the current project file (root item).
1357 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1358 // called after an actual file rename, and the cleanup code does not explore the
1359 // root item, because it cannot be renamed by the user. Also, ensure the new file
1360 // actually exists on the file system before it is readded. On Linux, moving a file
1361 // to the trash can cause the same path to be returned in both the old and new paths
1362 // of the event, even though the file isn't there anymore.
1363 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1364
1365 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1366 {
1367 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1368 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1369
1370 // If the item exists, select it
1371 if( newitem.IsOk() )
1372 m_TreeProject->SelectItem( newitem );
1373 }
1374
1375 m_isRenaming = false;
1376 }
1377 break;
1378 }
1379
1380 // Sort filenames by alphabetic order
1381 m_TreeProject->SortChildren( root_id );
1382}
1383
1384
1386{
1387 m_watcherNeedReset = false;
1388
1389 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1390
1391#if defined( _WIN32 )
1392 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1393
1394 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1395 {
1396 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1397 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1398 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1399 // Windows Server shares :/
1400 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1402 return;
1403 }
1404 else
1405 {
1406 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1408 }
1409#endif
1410
1411 // Prepare file watcher:
1412 if( m_watcher )
1413 {
1414 m_watcher->RemoveAll();
1415 }
1416 else
1417 {
1419 m_watcher->SetOwner( this );
1420 }
1421
1422 // We can see wxString under a debugger, not a wxFileName
1423 wxFileName fn;
1424 fn.AssignDir( prj_dir );
1425 fn.DontFollowLink();
1426
1427 // Add directories which should be monitored.
1428 // under windows, we add the curr dir and all subdirs
1429 // under unix, we add only the curr dir and the populated subdirs
1430 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1431 // under unix, the file watcher needs more work to be efficient
1432 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1433 {
1434 wxLogNull logNo; // avoid log messages
1435#ifdef __WINDOWS__
1436 if( ! m_watcher->AddTree( fn ) )
1437 {
1438 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1439 TO_UTF8( fn.GetFullPath() ) );
1440 return;
1441 }
1442 }
1443#else
1444 if( !m_watcher->Add( fn ) )
1445 {
1446 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1447 TO_UTF8( fn.GetFullPath() ) );
1448 return;
1449 }
1450 }
1451
1452 if( m_TreeProject->IsEmpty() )
1453 return;
1454
1455 // Add subdirs
1456 wxTreeItemIdValue cookie;
1457 wxTreeItemId root_id = m_root;
1458
1459 std::stack < wxTreeItemId > subdirs_id;
1460
1461 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1462 int total_watch_count = 0;
1463
1464 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1465 {
1466 if( !kid.IsOk() )
1467 {
1468 if( subdirs_id.empty() ) // all items were explored
1469 {
1470 break;
1471 }
1472 else
1473 {
1474 root_id = subdirs_id.top();
1475 subdirs_id.pop();
1476 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1477
1478 if( !kid.IsOk() )
1479 continue;
1480 }
1481 }
1482
1483 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1484
1485 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1486 {
1487 // we can see wxString under a debugger, not a wxFileName
1488 const wxString& path = itemData->GetFileName();
1489
1490 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1491
1492 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1493 {
1494 fn.AssignDir( path );
1495 m_watcher->Add( fn );
1496 total_watch_count++;
1497
1498 // if kid is a subdir, push in list to explore it later
1499 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1500 subdirs_id.push( kid );
1501 }
1502 }
1503
1504 kid = m_TreeProject->GetNextChild( root_id, cookie );
1505 }
1506
1507 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1508 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1509#endif
1510
1511#if defined(DEBUG) && 1
1512 wxArrayString paths;
1513 m_watcher->GetWatchedPaths( &paths );
1514 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1515
1516 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1517 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1518#endif
1519 }
1520
1521
1523{
1524 // Make sure we don't try to inspect the tree after we've deleted its items.
1526
1527 m_TreeProject->DeleteAllItems();
1528
1529 // Remove the git repository when the project is unloaded
1530 if( m_TreeProject->GetGitRepo() )
1531 {
1532 git_repository_free( m_TreeProject->GetGitRepo() );
1533 m_TreeProject->SetGitRepo( nullptr );
1534 }
1535}
1536
1537
1538void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1539{
1542 m_TreeProject->Refresh();
1543
1544 aEvent.Skip();
1545}
1546
1547
1548void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1549{
1550 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1551 wxPaintDC dc( this );
1552
1553 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1554 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1555
1556 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1557 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1558}
1559
1560
1561void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1562{
1564}
1565
1566
1567void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1568{
1569 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1570
1571 wxString dir = tree_data->GetDir();
1572
1573 if( dir.empty() )
1574 {
1575 wxLogError( "Failed to initialize git project: project directory is empty." );
1576 return;
1577 }
1578
1579 // Check if the directory is already a git repository
1580 git_repository* repo = nullptr;
1581 int error = git_repository_open(&repo, dir.mb_str());
1582
1583 if( error == 0 )
1584 {
1585 // Directory is already a git repository
1586 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1587
1588 DisplayInfoMessage( topLevelParent,
1589 _( "The selected directory is already a git project." ) );
1590 git_repository_free( repo );
1591 return;
1592 }
1593 else
1594 {
1595 // Directory is not a git repository
1596 error = git_repository_init( &repo, dir.mb_str(), 0 );
1597
1598 if( error != 0 )
1599 {
1600 git_repository_free( repo );
1601
1602 if( m_gitLastError != git_error_last()->klass )
1603 {
1604 m_gitLastError = git_error_last()->klass;
1605 DisplayErrorMessage( m_parent, _( "Failed to initialize git project." ),
1606 git_error_last()->message );
1607 }
1608
1609 return;
1610 }
1611 else
1612 {
1613 m_TreeProject->SetGitRepo( repo );
1614 m_gitLastError = GIT_ERROR_NONE;
1615 }
1616 }
1617
1618 DIALOG_GIT_REPOSITORY dlg( wxGetTopLevelParent( this ), repo );
1619
1620 dlg.SetTitle( _( "Set default remote" ) );
1621
1622 if( dlg.ShowModal() != wxID_OK )
1623 return;
1624
1625 //Set up the git remote
1626
1631
1632 git_remote* remote = nullptr;
1633 wxString fullURL;
1634
1636 {
1637 fullURL = dlg.GetUsername() + "@" + dlg.GetRepoURL();
1638 }
1640 {
1641 fullURL = dlg.GetRepoURL().StartsWith( "https" ) ? "https://" : "http://";
1642
1643 if( !dlg.GetUsername().empty() )
1644 {
1645 fullURL.append( dlg.GetUsername() );
1646
1647 if( !dlg.GetPassword().empty() )
1648 {
1649 fullURL.append( wxS( ":" ) );
1650 fullURL.append( dlg.GetPassword() );
1651 }
1652
1653 fullURL.append( wxS( "@" ) );
1654 }
1655
1656 fullURL.append( dlg.GetBareRepoURL() );
1657 }
1658 else
1659 {
1660 fullURL = dlg.GetRepoURL();
1661 }
1662
1663
1664 error = git_remote_create_with_fetchspec( &remote, repo, "origin",
1665 fullURL.ToStdString().c_str(),
1666 "+refs/heads/*:refs/remotes/origin/*" );
1667
1668 if( error != GIT_OK )
1669 {
1670 if( m_gitLastError != git_error_last()->klass )
1671 {
1672 m_gitLastError = git_error_last()->klass;
1673 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1674 git_error_last()->message );
1675 }
1676
1677 return;
1678 }
1679
1680 m_gitLastError = GIT_ERROR_NONE;
1681
1682 GIT_PULL_HANDLER handler( repo );
1683
1687 handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() );
1688
1689 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1690 _( "Fetching Remote" ),
1691 1 ) );
1692
1693 handler.PerformFetch();
1694
1698
1702 Prj().GetLocalSettings().m_GitRepoType = "https";
1703 else
1704 Prj().GetLocalSettings().m_GitRepoType = "local";
1705}
1706
1707
1708void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
1709{
1710
1711}
1712
1713
1714void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
1715{
1716 git_repository* repo = m_TreeProject->GetGitRepo();
1717
1718 if( !repo )
1719 return;
1720
1721 GIT_PULL_HANDLER handler( repo );
1722
1726 handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() );
1727
1728 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1729 _( "Fetching Remote" ),
1730 1 ) );
1731
1732 if( handler.PerformPull() != PullResult::Success )
1733 {
1734 wxString errorMessage = handler.GetErrorString();
1735
1736 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), errorMessage );
1737 }
1738}
1739
1740
1741void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
1742{
1743 git_repository* repo = m_TreeProject->GetGitRepo();
1744
1745 if( !repo )
1746 return;
1747
1748 GIT_PUSH_HANDLER handler( repo );
1749
1753 handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() );
1754
1755 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1756 _( "Fetching Remote" ),
1757 1 ) );
1758
1759 if( handler.PerformPush() != PushResult::Success )
1760 {
1761 wxString errorMessage = handler.GetErrorString();
1762
1763 DisplayErrorMessage( m_parent, _( "Failed to push project" ), errorMessage );
1764 }
1765}
1766
1767
1768static int git_create_branch( git_repository* aRepo, wxString& aBranchName )
1769{
1770 git_oid head_oid;
1771
1772 if( int error = git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != 0 )
1773 {
1774 wxLogError( "Failed to lookup HEAD reference" );
1775 return error;
1776 }
1777
1778 // Lookup the current commit object
1779 git_commit* commit = nullptr;
1780 if( int error = git_commit_lookup( &commit, aRepo, &head_oid ) != GIT_OK )
1781 {
1782 wxLogError( "Failed to lookup commit" );
1783 return error;
1784 }
1785
1786 git_reference* branchRef = nullptr;
1787
1788 if( git_branch_create( &branchRef, aRepo, aBranchName.mb_str(), commit, 0 ) != 0 )
1789 {
1790 wxLogError( "Failed to create branch" );
1791 git_commit_free( commit );
1792 return -1;
1793 }
1794
1795 git_commit_free( commit );
1796 git_reference_free( branchRef );
1797
1798 return 0;
1799}
1800
1801
1802void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
1803{
1804 git_repository* repo = m_TreeProject->GetGitRepo();
1805
1806 if( !repo )
1807 return;
1808
1809 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), repo );
1810
1811 int retval = dlg.ShowModal();
1812 wxString branchName = dlg.GetBranchName();
1813
1814 if( retval == wxID_ADD )
1815 git_create_branch( repo, branchName );
1816 else if( retval != wxID_OK )
1817 return;
1818
1819 // Retrieve the reference to the existing branch using libgit2
1820 git_reference* branchRef = nullptr;
1821
1822 if( git_reference_lookup( &branchRef, repo, branchName.mb_str() ) != GIT_OK &&
1823 git_reference_dwim( &branchRef, repo, branchName.mb_str() ) != GIT_OK )
1824 {
1825 wxString errorMessage = wxString::Format( _( "Failed to lookup branch '%s': %s" ),
1826 branchName, giterr_last()->message );
1827 DisplayError( m_parent, errorMessage );
1828 return;
1829 }
1830
1831 const char* branchRefName = git_reference_name( branchRef );
1832
1833 git_object* branchObj = nullptr;
1834
1835 if( git_revparse_single( &branchObj, repo, branchName.mb_str() ) != 0 )
1836 {
1837 wxString errorMessage =
1838 wxString::Format( _( "Failed to find branch head for '%s'" ), branchName );
1839 DisplayError( m_parent, errorMessage );
1840 git_reference_free( branchRef );
1841 return;
1842 }
1843
1844
1845 // Switch to the branch
1846 if( git_checkout_tree( repo, branchObj, nullptr ) != 0 )
1847 {
1848 wxString errorMessage =
1849 wxString::Format( _( "Failed to switch to branch '%s'" ), branchName );
1850 DisplayError( m_parent, errorMessage );
1851 git_reference_free( branchRef );
1852 git_object_free( branchObj );
1853 return;
1854 }
1855
1856 // Update the HEAD reference
1857 if( git_repository_set_head( repo, branchRefName ) != 0 )
1858 {
1859 wxString errorMessage = wxString::Format(
1860 _( "Failed to update HEAD reference for branch '%s'" ), branchName );
1861 DisplayError( m_parent, errorMessage );
1862 git_reference_free( branchRef );
1863 git_object_free( branchObj );
1864 return;
1865 }
1866
1867 // Free resources
1868 git_reference_free( branchRef );
1869 git_object_free( branchObj );
1870}
1871
1872
1873void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
1874{
1875 git_repository* repo = m_TreeProject->GetGitRepo();
1876
1877 if( !repo
1878 || !IsOK( wxGetTopLevelParent( this ),
1879 _( "Are you sure you want to remove git tracking from this project?" ) ) )
1880 {
1881 return;
1882 }
1883
1884 // Remove the VCS (git) from the project directory
1885 git_repository_free( repo );
1886 m_TreeProject->SetGitRepo( nullptr );
1887
1888 // Remove the .git directory
1889 wxFileName fn( m_Parent->GetProjectFileName() );
1890 fn.AppendDir( ".git" );
1891
1892 wxString errors;
1893
1894 if( !RmDirRecursive( fn.GetPath(), &errors ) )
1895 {
1896 DisplayErrorMessage( m_parent, _( "Failed to remove git directory" ), errors );
1897 }
1898
1899 // Clear all item states
1900 std::stack<wxTreeItemId> items;
1901 items.push( m_TreeProject->GetRootItem() );
1902
1903 while( !items.empty() )
1904 {
1905 wxTreeItemId current = items.top();
1906 items.pop();
1907
1908 // Process the current item
1909 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
1910
1911 wxTreeItemIdValue cookie;
1912 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
1913
1914 while( child.IsOk() )
1915 {
1916 items.push( child );
1917 child = m_TreeProject->GetNextChild( current, cookie );
1918 }
1919 }
1920}
1921
1922
1924{
1925 if( ADVANCED_CFG::GetCfg().m_EnableGit == false )
1926 return;
1927
1928 if( !m_TreeProject )
1929 return;
1930
1931 wxTimeSpan timeSinceLastUpdate = wxDateTime::Now() - m_lastGitStatusUpdate;
1932
1933 if( timeSinceLastUpdate.Abs() < wxTimeSpan::Seconds( 2 ) )
1934 return;
1935
1936 m_lastGitStatusUpdate = wxDateTime::Now();
1937
1938 wxTreeItemId kid = m_TreeProject->GetRootItem();
1939
1940 if( !kid.IsOk() )
1941 return;
1942
1943 git_repository* repo = m_TreeProject->GetGitRepo();
1944
1945 if( !repo )
1946 return;
1947
1948 // Get Current Branch
1949
1950 git_reference* currentBranchReference = nullptr;
1951 git_repository_head( &currentBranchReference, repo );
1952
1953 PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid );
1954 wxFileName rootFilename( rootItem->GetFileName() );
1955 wxString repoWorkDir( git_repository_workdir( repo ) );
1956
1957 // Get the current branch name
1958 if( currentBranchReference )
1959 {
1960 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
1961 wxString branchName = git_reference_shorthand( currentBranchReference );
1962
1963 m_TreeProject->SetItemText( kid, filename + " [" + branchName + "]" );
1964 git_reference_free( currentBranchReference );
1965 }
1966 else
1967 {
1968 if( giterr_last()->klass != m_gitLastError )
1969 wxLogError( "Failed to lookup current branch: %s", giterr_last()->message );
1970
1971 m_gitLastError = giterr_last()->klass;
1972 }
1973
1974 // Collect a map to easily set the state of each item
1975 std::map<wxString, wxTreeItemId> branchMap;
1976 {
1977 std::stack<wxTreeItemId> items;
1978 items.push( kid );
1979
1980 while( !items.empty() )
1981 {
1982 kid = items.top();
1983 items.pop();
1984
1985 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
1986
1987 wxString gitAbsPath = nextItem->GetFileName();
1988#ifdef _WIN32
1989 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
1990#endif
1991 branchMap[gitAbsPath] = kid;
1992
1993 wxTreeItemIdValue cookie;
1994 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
1995
1996 while( child.IsOk() )
1997 {
1998 items.push( child );
1999 child = m_TreeProject->GetNextChild( kid, cookie );
2000 }
2001 }
2002 }
2003
2004 wxFileName relative = rootFilename;
2005 relative.MakeRelativeTo( repoWorkDir );
2006 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2007
2008#ifdef _WIN32
2009 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2010#endif
2011
2012 const char* pathspec[] = { pathspecStr.c_str().AsChar() };
2013
2014 git_status_options status_options;
2015 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2016 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2017 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED;
2018 status_options.pathspec = { (char**) pathspec, 1 };
2019
2020 git_index* index = nullptr;
2021
2022 if( git_repository_index( &index, repo ) != GIT_OK )
2023 {
2024 m_gitLastError = giterr_last()->klass;
2025 wxLogTrace( traceGit, wxS( "Failed to get git index: %s" ), giterr_last()->message );
2026 return;
2027 }
2028
2029 git_status_list* status_list = nullptr;
2030
2031 if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK )
2032 {
2033 wxLogTrace( traceGit, wxS( "Failed to get git status list: %s" ), giterr_last()->message );
2034 git_index_free( index );
2035 return;
2036 }
2037
2038 auto [ localChanges, remoteChanges ] = m_TreeProject->GitCommon()->GetDifferentFiles();
2039
2040 size_t count = git_status_list_entrycount( status_list );
2041
2042 for( size_t ii = 0; ii < count; ++ii )
2043 {
2044 const git_status_entry* entry = git_status_byindex( status_list, ii );
2045 std::string path( entry->head_to_index? entry->head_to_index->old_file.path
2046 : entry->index_to_workdir->old_file.path );
2047
2048 wxString absPath = repoWorkDir;
2049 absPath << path;
2050
2051 auto iter = branchMap.find( absPath );
2052
2053 if( iter == branchMap.end() )
2054 continue;
2055
2056 // If we are current, don't continue because we still need to check to see if the
2057 // current commit is ahead/behind the remote. If the file is modified/added/deleted,
2058 // that is the main status we want to show.
2059 if( entry->status == GIT_STATUS_CURRENT )
2060 {
2061 m_TreeProject->SetItemState(
2062 iter->second,
2064 }
2065 else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) )
2066 {
2067 m_TreeProject->SetItemState(
2068 iter->second,
2070 continue;
2071 }
2072 else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) )
2073 {
2074 m_TreeProject->SetItemState(
2075 iter->second,
2076 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) );
2077 continue;
2078 }
2079 else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) )
2080 {
2081 m_TreeProject->SetItemState(
2082 iter->second,
2084 continue;
2085 }
2086
2087 // Check if file is up to date with the remote
2088 if( localChanges.count( path ) )
2089 {
2090 m_TreeProject->SetItemState(
2091 iter->second,
2092 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ) );
2093 continue;
2094 }
2095 else if( remoteChanges.count( path ) )
2096 {
2097 m_TreeProject->SetItemState(
2098 iter->second,
2099 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ) );
2100 continue;
2101 }
2102 else
2103 {
2104 m_TreeProject->SetItemState(
2105 iter->second,
2107 continue;
2108 }
2109 }
2110
2111 git_status_list_free( status_list );
2112 git_index_free( index );
2113}
2114
2115
2116void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2117{
2118 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2119
2120 git_repository* repo = m_TreeProject->GetGitRepo();
2121
2122 if( repo == nullptr )
2123 {
2124 wxMessageBox( "The selected directory is not a git project." );
2125 return;
2126 }
2127
2128 git_config* config = nullptr;
2129 git_repository_config( &config, repo );
2130
2131 // Read relevant data from the git config
2132 wxString authorName;
2133 wxString authorEmail;
2134
2135 // Read author name
2136 git_config_entry* name_c = nullptr;
2137 git_config_entry* email_c = nullptr;
2138 int authorNameError = git_config_get_entry( &name_c, config, "user.name" );
2139
2140 if( authorNameError != 0 || name_c == nullptr )
2141 {
2142 authorName = Pgm().GetCommonSettings()->m_Git.authorName;
2143 }
2144 else
2145 {
2146 authorName = name_c->value;
2147 git_config_entry_free( name_c );
2148 }
2149
2150 // Read author email
2151 int authorEmailError = git_config_get_entry( &email_c, config, "user.email" );
2152
2153 if( authorEmailError != 0 || email_c == nullptr )
2154 {
2155 authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail;
2156 }
2157 else
2158 {
2159 authorEmail = email_c->value;
2160 git_config_entry_free( email_c );
2161 }
2162
2163 // Free the config object
2164 git_config_free( config );
2165
2166 // Collect modified files in the repository
2167 git_status_options status_options;
2168 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2169 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2170 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED;
2171
2172 git_status_list* status_list = nullptr;
2173 git_status_list_new( &status_list, repo, &status_options );
2174
2175 std::map<wxString, int> modifiedFiles;
2176
2177 size_t count = git_status_list_entrycount( status_list );
2178
2179 std::set<wxString> selected_files;
2180
2181 for( PROJECT_TREE_ITEM* item : tree_data )
2182 {
2183 if( item->GetType() != TREE_FILE_TYPE::DIRECTORY )
2184 selected_files.emplace( item->GetFileName() );
2185 }
2186
2187 for( size_t i = 0; i < count; ++i )
2188 {
2189 const git_status_entry* entry = git_status_byindex( status_list, i );
2190
2191 // Check if the file is modified (index or workdir changes)
2192 if( entry->status == GIT_STATUS_CURRENT
2193 || ( entry->status & ( GIT_STATUS_CONFLICTED | GIT_STATUS_IGNORED ) ) )
2194 {
2195 continue;
2196 }
2197
2198 wxFileName fn( entry->index_to_workdir->old_file.path );
2199 fn.MakeAbsolute( git_repository_workdir( repo ) );
2200
2201 wxString filePath( entry->index_to_workdir->old_file.path, wxConvUTF8 );
2202
2203 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2204 {
2205 modifiedFiles.emplace( filePath, entry->status );
2206 }
2207 else if( selected_files.count( fn.GetFullPath() ) )
2208 {
2209 modifiedFiles.emplace( filePath, entry->status );
2210 }
2211 }
2212
2213 git_status_list_free( status_list );
2214
2215 // Create a commit dialog
2216 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, authorName, authorEmail,
2217 modifiedFiles );
2218 auto ret = dlg.ShowModal();
2219
2220 if( ret == wxID_OK )
2221 {
2222 // Commit the changes
2223 git_oid tree_id;
2224 git_tree* tree = nullptr;
2225 git_commit* parent = nullptr;
2226 git_index* index = nullptr;
2227
2228 std::vector<wxString> files = dlg.GetSelectedFiles();
2229
2230 if( dlg.GetCommitMessage().IsEmpty() )
2231 {
2232 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2233 return;
2234 }
2235
2236 if( files.empty() )
2237 {
2238 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2239 return;
2240 }
2241
2242 if( git_repository_index( &index, repo ) != 0 )
2243 {
2244 wxMessageBox( _( "Failed to get repository index: %s" ), giterr_last()->message );
2245 return;
2246 }
2247
2248 for( wxString& file :files )
2249 {
2250 if( git_index_add_bypath( index, file.mb_str() ) != 0 )
2251 {
2252 wxMessageBox( _( "Failed to add file to index: %s" ), giterr_last()->message );
2253 git_index_free( index );
2254 return;
2255 }
2256 }
2257
2258 if( git_index_write( index ) != 0 )
2259 {
2260 wxMessageBox( _( "Failed to write index: %s" ), giterr_last()->message );
2261 git_index_free( index );
2262 return;
2263 }
2264
2265 if (git_index_write_tree( &tree_id, index ) != 0)
2266 {
2267 wxMessageBox( _( "Failed to write tree: %s" ), giterr_last()->message );
2268 git_index_free( index );
2269 return;
2270 }
2271
2272 git_index_free( index );
2273
2274 if( git_tree_lookup( &tree, repo, &tree_id ) != 0 )
2275 {
2276 wxMessageBox( _( "Failed to lookup tree: %s" ), giterr_last()->message );
2277 return;
2278 }
2279
2280 git_reference* headRef = nullptr;
2281
2282 if( git_repository_head( &headRef, repo ) != 0 )
2283 {
2284 wxMessageBox( _( "Failed to get HEAD reference: %s" ), giterr_last()->message );
2285 git_index_free( index );
2286 return;
2287 }
2288
2289 if( git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT ) != 0 )
2290 {
2291 wxMessageBox( _( "Failed to get commit: %s" ), giterr_last()->message );
2292 git_reference_free( headRef );
2293 git_index_free( index );
2294 return;
2295 }
2296
2297 git_reference_free( headRef );
2298
2299 const wxString& commit_msg = dlg.GetCommitMessage();
2300 const wxString& author_name = dlg.GetAuthorName();
2301 const wxString& author_email = dlg.GetAuthorEmail();
2302
2303 git_signature* author = nullptr;
2304
2305 if( git_signature_now( &author, author_name.mb_str(), author_email.mb_str() ) != 0 )
2306 {
2307 wxMessageBox( _( "Failed to create author signature: %s" ), giterr_last()->message );
2308 return;
2309 }
2310
2311 git_oid oid;
2312
2313#if( LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
2314 && ( LIBGIT2_VER_REVISION < 2 || LIBGIT2_VER_REVISION == 3 ) )
2315 /*
2316 * For libgit2 versions 1.8.0, 1.8.1. (cf19ddc52)
2317 * This change was reverted for 1.8.2 (49d3fadfc, main branch)
2318 * The revert for 1.8.2 was not included for 1.8.3 (which is on the maint/v1.8 branch, not main)
2319 * This change was also reverted for 1.8.4 (94ba816f6, also maint/v1.8 branch)
2320 *
2321 * As of 1.8.4, the history is like this:
2322 *
2323 * * 3f4182d15 (tag: v1.8.4, maint/v1.8)
2324 * * 94ba816f6 Revert "commit: fix const declaration" [puts const back]
2325 * * 3353f78e8 (tag: v1.8.3)
2326 * | * 4ce872a0f (tag: v1.8.2-rc1, tag: v1.8.2)
2327 * | * 49d3fadfc Revert "commit: fix const declaration" [puts const back]
2328 * |/
2329 * * 36f7e21ad (tag: v1.8.1)
2330 * * d74d49148 (tag: v1.8.0)
2331 * * cf19ddc52 commit: fix const declaration [removes const]
2332 */
2333 git_commit* const parents[1] = { parent };
2334#else
2335 // For libgit2 versions older than 1.8.0, or equal to 1.8.2, or 1.8.4+
2336 const git_commit* parents[1] = { parent };
2337#endif
2338
2339 if( git_commit_create( &oid, repo, "HEAD", author, author, nullptr, commit_msg.mb_str(), tree,
2340 1, parents ) != 0 )
2341 {
2342 wxMessageBox( _( "Failed to create commit: %s" ), giterr_last()->message );
2343 return;
2344 }
2345
2346 git_signature_free( author );
2347 git_commit_free( parent );
2348 git_tree_free( tree );
2349 }
2350}
2351
2352
2353void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
2354{
2355
2356}
2357
2358
2359bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
2360{
2361 git_index *index;
2362 size_t entry_pos;
2363
2364 git_repository* repo = m_TreeProject->GetGitRepo();
2365
2366 if( !repo )
2367 return false;
2368
2369 if( git_repository_index( &index, repo ) != 0 )
2370 return false;
2371
2372 // If we successfully find the file in the index, we may not add it to the VCS
2373 if( git_index_find( &entry_pos, index, aFile.mb_str() ) == 0 )
2374 {
2375 git_index_free( index );
2376 return false;
2377 }
2378
2379 git_index_free( index );
2380 return true;
2381}
2382
2383
2384void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
2385{
2386 git_repository* repo = m_TreeProject->GetGitRepo();
2387
2388 if( !repo )
2389 return;
2390
2391 GIT_SYNC_HANDLER handler( repo );
2392 handler.PerformSync();
2393}
2394
2395
2396void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
2397{
2398 git_repository* repo = m_TreeProject->GetGitRepo();
2399
2400 if( !repo )
2401 return;
2402
2403 GIT_PULL_HANDLER handler( repo );
2404 handler.PerformFetch();
2405}
2406
2407
2408void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
2409{
2410 git_repository* repo = m_TreeProject->GetGitRepo();
2411
2412 if( !repo )
2413 return;
2414
2415 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
2416 handler.PerformResolveConflict();
2417}
2418
2419
2420void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
2421{
2422 git_repository* repo = m_TreeProject->GetGitRepo();
2423
2424 if( !repo )
2425 return;
2426
2427 GIT_REVERT_HANDLER handler( repo );
2428 handler.PerformRevert();
2429}
2430
2431
2432void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
2433{
2434 git_repository* repo = m_TreeProject->GetGitRepo();
2435
2436 if( !repo )
2437 return;
2438
2439 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
2440 handler.PerformRemoveFromIndex();
2441}
2442
2443
2445{
2446
2447}
const char * name
Definition: DXF_plotter.cpp:57
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
BITMAP_STORE * GetBitmapStore()
Definition: bitmap.cpp:92
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
void ThemeChanged()
Notifies the store that the icon theme has been changed by the user, so caches must be invalidated.
wxString GetCommitMessage() const
wxString GetAuthorEmail() const
std::vector< wxString > GetSelectedFiles() const
wxString GetAuthorName() const
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
wxString GetBareRepoURL() const
Get the Bare Repo U R L object.
wxString GetRepoSSHPath() const
wxString GetUsername() const
wxString GetPassword() const
wxString GetBranchName() const
int ShowModal() override
void SetProgressReporter(std::unique_ptr< WX_PROGRESS_REPORTER > aProgressReporter)
Definition: git_progress.h:40
PullResult PerformPull()
PushResult PerformPush()
The main KiCad project manager frame.
PROJECT_TREE_PANE * m_leftWin
const wxString GetProjectFileName() const
void OnChangeWatchedPaths(wxCommandEvent &aEvent)
Called by sending a event with id = ID_INIT_WATCHED_PATHS rebuild the list of watched paths.
void LoadProject(const wxFileName &aProjectFileName)
std::vector< wxString > GetBranchNames() const
void SetConnType(GIT_CONN_TYPE aConnType)
wxString GetSSHKey() const
void SetSSHKey(const wxString &aSSHKey)
void SetUsername(const wxString &aUsername)
GIT_CONN_TYPE GetConnType() const
wxString GetPassword() const
std::pair< std::set< wxString >, std::set< wxString > > GetDifferentFiles() const
Return a pair of sets of files that differ locally from the remote repository The first set is files ...
bool HasPushAndPullRemote() const
wxString GetUsername() const
bool HasLocalCommits() const
void SetPassword(const wxString &aPassword)
wxString GetErrorString()
KISTATUSBAR is a wxStatusBar suitable for Kicad manager.
Definition: kistatusbar.h:45
void SetEllipsedTextField(const wxString &aText, int aFieldId)
Set the text in a field using wxELLIPSIZE_MIDDLE option to adjust the text size to the field size (un...
static wxString GetDefaultUserProjectsPath()
Gets the default path we point users to create projects.
Definition: paths.cpp:140
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition: pgm_base.cpp:679
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition: pgm_base.cpp:196
Handle one item (a file or a directory name) for the tree file.
void SetRootFile(bool aValue)
void SetState(int state)
const wxString & GetFileName() const
bool IsPopulated() const
void SetPopulated(bool aValue)
TREE_FILE_TYPE GetType() const
const wxString GetDir() const
void Activate(PROJECT_TREE_PANE *aTreePrjFrame)
PROJECT_TREE_PANE Window to display the tree files.
PROJECT_TREE_ITEM * m_selectedItem
void onGitFetch(wxCommandEvent &event)
Fetch the latest changes from the git repository.
void onGitInitializeProject(wxCommandEvent &event)
Initialize a new git repository in the current project directory.
void onGitSyncProject(wxCommandEvent &event)
Sync the current project with the git repository.
void onDeleteFile(wxCommandEvent &event)
Function onDeleteFile Delete the selected file or directory in the tree project.
void onGitRemoveVCS(wxCommandEvent &event)
Remove the git repository from the current project directory.
void EmptyTreePrj()
Delete all m_TreeProject entries.
void FileWatcherReset()
Reinit the watched paths Should be called after opening a new project to rebuild the list of watched ...
std::vector< PROJECT_TREE_ITEM * > GetSelectedData()
Function GetSelectedData return the item data from item currently selected (highlighted) Note this is...
void onOpenDirectory(wxCommandEvent &event)
Function onOpenDirectory Handles the right-click menu for opening a directory in the current system f...
void onGitResolveConflict(wxCommandEvent &event)
Resolve conflicts in the git repository.
void onFileSystemEvent(wxFileSystemWatcherEvent &event)
called when a file or directory is modified/created/deleted The tree project is modified when a file ...
wxFileSystemWatcher * m_watcher
wxDateTime m_lastGitStatusUpdate
void onRight(wxTreeEvent &Event)
Called on a right click on an item.
void onGitCommit(wxCommandEvent &event)
Commit the current project saved changes to the git repository.
void onSelect(wxTreeEvent &Event)
Called on a double click on an item.
PROJECT_TREE * m_TreeProject
KICAD_MANAGER_FRAME * m_Parent
void onExpand(wxTreeEvent &Event)
Called on a click on the + or - button of an item with children.
void onIdle(wxIdleEvent &aEvent)
Idle event handler, used process the selected items at a point in time when all other events have bee...
void onGitRevertLocal(wxCommandEvent &event)
Revert the local repository to the last commit.
void onGitPullProject(wxCommandEvent &event)
Pull the latest changes from the git repository.
static wxString GetFileExt(TREE_FILE_TYPE type)
friend class PROJECT_TREE_ITEM
void onGitRemoveFromIndex(wxCommandEvent &event)
Remove a file from the git index.
wxTreeItemId findSubdirTreeItem(const wxString &aSubDir)
Function findSubdirTreeItem searches for the item in tree project which is the node of the subdirecto...
void ReCreateTreePrj()
Create or modify the tree showing project file names.
void onThemeChanged(wxSysColourChangedEvent &aEvent)
void onRunSelectedJobsFile(wxCommandEvent &event)
Run a selected jobs file.
void onGitCompare(wxCommandEvent &event)
Compare the current project to a different branch in the git repository.
void shutdownFileWatcher()
Shutdown the file watcher.
wxTreeItemId addItemToProjectTree(const wxString &aName, const wxTreeItemId &aParent, std::vector< wxString > *aProjectNames, bool aRecurse)
Function addItemToProjectTree.
bool hasChangedFiles()
Returns true if the current project has any uncommitted changes.
void onOpenSelectedFileWithTextEditor(wxCommandEvent &event)
Function onOpenSelectedFileWithTextEditor Call the text editor to open the selected file in the tree ...
void onGitAddToIndex(wxCommandEvent &event)
Add a file to the git index.
void updateGitStatusIcons()
Updates the icons shown in the tree project to reflect the current git status.
PROJECT_TREE_ITEM * GetItemIdData(wxTreeItemId aId)
Function GetItemIdData return the item data corresponding to a wxTreeItemId identifier.
void onGitSwitchBranch(wxCommandEvent &event)
Switch to a different branch in the git repository.
void onCreateNewDirectory(wxCommandEvent &event)
Function onCreateNewDirectory Creates a new subdirectory inside the current kicad project directory t...
void onSwitchToSelectedProject(wxCommandEvent &event)
Switch to a other project selected from the tree project (by selecting an other .pro file inside the ...
void onPaint(wxPaintEvent &aEvent)
We don't have uniform borders so we have to draw them ourselves.
void onRenameFile(wxCommandEvent &event)
Function onRenameFile Rename the selected file or directory in the tree project.
void onGitPushProject(wxCommandEvent &event)
Push the current project changes to the git repository.
bool canFileBeAddedToVCS(const wxString &aFilePath)
Returns true if the file has already been added to the repository or false if it has not been added y...
std::vector< wxString > m_filters
PROJECT_TREE This is the class to show (as a tree) the files in the project directory.
Definition: project_tree.h:43
git_repository * GetGitRepo() const
Definition: project_tree.h:61
KIGIT_COMMON * GitCommon() const
Definition: project_tree.h:63
void LoadIcons()
void SetGitRepo(git_repository *aRepo)
Definition: project_tree.h:60
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition: project.h:206
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition: confirm.cpp:250
void DisplayError(wxWindow *aParent, const wxString &aText, int aDisplayTime)
Display an error or warning message box with aMessage.
Definition: confirm.cpp:170
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition: confirm.cpp:222
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition: confirm.cpp:195
This file is part of the common library.
#define _(s)
int ExecuteFile(const wxString &aEditorName, const wxString &aFileName, wxProcess *aCallback, bool aFileForKicad)
Call the executable file aEditorName with the parameter aFileName.
Definition: gestfich.cpp:143
bool RmDirRecursive(const wxString &aFileName, wxString *aErrors)
Removes the directory aDirName and all its contents including subdirectories and their files.
Definition: gestfich.cpp:330
bool m_EnableGit
Enable git integration.
static const std::string LegacySchematicFileExtension
static const std::string HtmlFileExtension
static const wxString GerberFileExtensionsRegex
static const std::string NetlistFileExtension
static const std::string GerberJobFileExtension
static const std::string ReportFileExtension
static const std::string ProjectFileExtension
static const std::string FootprintPlaceFileExtension
static const std::string LegacyPcbFileExtension
static const std::string LegacyProjectFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string LegacySymbolLibFileExtension
static const std::string KiCadSymbolLibFileExtension
static const std::string SpiceFileExtension
static const std::string PdfFileExtension
static const std::string TextFileExtension
static const std::string DrawingSheetFileExtension
static const std::string KiCadJobSetFileExtension
static const std::string FootprintAssignmentFileExtension
static const std::string SVGFileExtension
static const std::string DrillFileExtension
static const std::string DesignRulesFileExtension
static const std::string MarkdownFileExtension
static const std::string KiCadFootprintFileExtension
static const std::string ArchiveFileExtension
static const std::string KiCadPcbFileExtension
const wxChar *const tracePathsAndFiles
Flag to enable path and file name debug output.
const wxChar *const traceGit
Flag to enable Git debugging output.
PROJECT & Prj()
Definition: kicad.cpp:595
#define GIT_BUF_INIT
IDs used in KiCad main frame foe menuitems and tools.
@ ID_PROJECT_OPEN_DIR
Definition: kicad_id.h:62
@ ID_GIT_PUSH
Definition: kicad_id.h:82
@ ID_GIT_REVERT_LOCAL
Definition: kicad_id.h:85
@ ID_JOBS_RUN
Definition: kicad_id.h:97
@ ID_PROJECT_SWITCH_TO_OTHER
Definition: kicad_id.h:60
@ ID_PROJECT_NEWDIR
Definition: kicad_id.h:61
@ ID_PROJECT_RENAME
Definition: kicad_id.h:64
@ ID_GIT_REMOVE_VCS
Definition: kicad_id.h:87
@ ID_GIT_COMMIT_FILE
Definition: kicad_id.h:79
@ ID_GIT_PULL
Definition: kicad_id.h:83
@ ID_GIT_COMPARE
Definition: kicad_id.h:86
@ ID_PROJECT_TXTEDIT
Definition: kicad_id.h:59
@ ID_GIT_REMOVE_FROM_INDEX
Definition: kicad_id.h:89
@ ID_GIT_RESOLVE_CONFLICT
Definition: kicad_id.h:84
@ ID_GIT_COMMIT_PROJECT
Definition: kicad_id.h:78
@ ID_GIT_SWITCH_BRANCH
Definition: kicad_id.h:90
@ ID_GIT_ADD_TO_INDEX
Definition: kicad_id.h:88
@ ID_GIT_SYNC_PROJECT
Definition: kicad_id.h:80
@ ID_PROJECT_TREE
Definition: kicad_id.h:58
@ ID_GIT_FETCH
Definition: kicad_id.h:81
@ ID_GIT_INITIALIZE_PROJECT
Definition: kicad_id.h:76
@ ID_PROJECT_DELETE
Definition: kicad_id.h:63
@ ID_LEFT_FRAME
Definition: kicad_id.h:57
bool LaunchExternal(const wxString &aPath)
Launches the given file or folder in the host OS.
Definition: launch_ext.cpp:25
This file contains miscellaneous commonly used macros and functions.
#define KI_FALLTHROUGH
The KI_FALLTHROUGH macro is to be used when switch statement cases should purposely fallthrough from ...
Definition: macros.h:83
bool IsNetworkPath(const wxString &aPath)
Determines if a given path is a network shared file apth On Windows for example, any form of path is ...
bool IsFileHidden(const wxString &aFileName)
Helper function to determine the status of the 'Hidden' file attribute.
Definition: unix/io.cpp:63
KICOMMON_API wxMenuItem * AddMenuItem(wxMenu *aMenu, int aId, const wxString &aText, const wxBitmapBundle &aImage, wxItemKind aType=wxITEM_NORMAL)
Create and insert a menu item with an icon into aMenu.
Definition: ui_common.cpp:372
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition: kicad_algo.h:100
PGM_BASE & Pgm()
The global Program "get" accessor.
Definition: pgm_base.cpp:1060
#define NAMELESS_PROJECT
default name for nameless projects
Definition: project.h:43
std::vector< wxString > getProjects(const wxDir &dir)
static git_repository * get_git_repository_for_file(const char *filename)
static const wxChar * s_allowedExtensionsToList[]
static int git_create_branch(git_repository *aRepo, wxString &aBranchName)
#define wxFileSystemWatcher
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition: string_utils.h:398
wxLogTrace helper definitions.
TREE_FILE_TYPE
Definition of file extensions used in Kicad.