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 The 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#include <kiplatform/secrets.h>
82
83
84/* Note about the project tree build process:
85 * Building the project tree can be *very* long if there are a lot of subdirectories in the
86 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
87 * user's home directory.
88 * So the tree project is built "on demand":
89 * First the tree is built from the current directory and shows files and subdirs.
90 * > First level subdirs trees are built (i.e subdirs contents are not read)
91 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
92 * populated on the fly.
93 */
94
95// list of files extensions listed in the tree project window
96// Add extensions in a compatible regex format to see others files types
97static const wxChar* s_allowedExtensionsToList[] = {
98 wxT( "^.*\\.pro$" ),
99 wxT( "^.*\\.kicad_pro$" ),
100 wxT( "^.*\\.pdf$" ),
101 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
102 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
103 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
104 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
105 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
106 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
107 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
108 wxT( "^.*\\.net$" ), // pcbnew netlist file
109 wxT( "^.*\\.cir$" ), // Spice netlist file
110 wxT( "^.*\\.lib$" ), // Legacy schematic library file
111 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
112 wxT( "^.*\\.txt$" ), // Text files
113 wxT( "^.*\\.md$" ), // Markdown files
114 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
115 wxT( "^.*\\.gbr$" ), // Gerber file
116 wxT( "^.*\\.gbrjob$" ), // Gerber job file
117 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
118 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
119 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
120 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
121 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
122 wxT( "^.*\\.odt$" ),
123 wxT( "^.*\\.htm$" ),
124 wxT( "^.*\\.html$" ),
125 wxT( "^.*\\.rpt$" ), // Report files
126 wxT( "^.*\\.csv$" ), // Report files in comma separated format
127 wxT( "^.*\\.pos$" ), // Footprint position files
128 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
129 wxT( "^.*\\.drl$" ), // Excellon drill files
130 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
131 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
132 wxT( "^.*\\.svg$" ), // SVG print/plot files
133 wxT( "^.*\\.ps$" ), // PostScript plot files
134 wxT( "^.*\\.zip$" ), // Zip archive files
135 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
136 nullptr // end of list
137};
138
139
147BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
148 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
149 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
150 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
157
172
174
175 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
176 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
177END_EVENT_TABLE()
178
179
181 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize,
182 wxNO_BORDER | wxTAB_TRAVERSAL )
183{
184 m_Parent = parent;
185 m_TreeProject = nullptr;
186 m_isRenaming = false;
187 m_selectedItem = nullptr;
188 m_watcherNeedReset = false;
189 m_lastGitStatusUpdate = wxDateTime::Now();
190 m_gitLastError = GIT_ERROR_NONE;
191
192 m_watcher = nullptr;
193 Connect( wxEVT_FSWATCHER,
194 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
195
196 Bind( wxEVT_SYS_COLOUR_CHANGED,
197 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
198
199 /*
200 * Filtering is now inverted: the filters are actually used to _enable_ support
201 * for a given file type.
202 */
203 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
204 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
205
206 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
207
208 ReCreateTreePrj();
209
210}
211
212
214{
215 Disconnect( wxEVT_FSWATCHER,
216 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ) );
217 Unbind( wxEVT_SYS_COLOUR_CHANGED,
218 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
220}
221
222
224{
225 if( m_watcher )
226 {
227 m_watcher->RemoveAll();
228 m_watcher->SetOwner( nullptr );
229 delete m_watcher;
230 m_watcher = nullptr;
231 }
232}
233
234
236{
237 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
238
239 if( tree_data.size() != 1 )
240 return;
241
242 wxString prj_filename = tree_data[0]->GetFileName();
243
244 m_Parent->LoadProject( prj_filename );
245}
246
247
248void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
249{
250 // Get the root directory name:
251 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
252
253 for( PROJECT_TREE_ITEM* item_data : tree_data )
254 {
255 // Ask for the new sub directory name
256 wxString curr_dir = item_data->GetDir();
257
258 if( curr_dir.IsEmpty() )
259 {
260 // Use project path if the tree view path was empty.
261 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
262
263 // As a last resort use the user's documents folder.
264 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
266
267 if( !curr_dir.IsEmpty() )
268 curr_dir += wxFileName::GetPathSeparator();
269 }
270
271 LaunchExternal( curr_dir );
272 }
273}
274
275
276void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
277{
278 // Get the root directory name:
279 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
280
281 for( PROJECT_TREE_ITEM* item_data : tree_data )
282 {
283 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
284
285 // Ask for the new sub directory name
286 wxString curr_dir = item_data->GetDir();
287
288 if( curr_dir.IsEmpty() )
289 curr_dir = prj_dir;
290
291 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
292
293 if( new_dir.IsEmpty() )
294 return;
295
296 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
297
298 if( !wxMkdir( full_dirname ) )
299 return;
300
301 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
302 }
303}
304
305
307{
308 switch( type )
309 {
310 case TREE_FILE_TYPE::LEGACY_PROJECT: return FILEEXT::LegacyProjectFileExtension;
311 case TREE_FILE_TYPE::JSON_PROJECT: return FILEEXT::ProjectFileExtension;
312 case TREE_FILE_TYPE::LEGACY_SCHEMATIC: return FILEEXT::LegacySchematicFileExtension;
313 case TREE_FILE_TYPE::SEXPR_SCHEMATIC: return FILEEXT::KiCadSchematicFileExtension;
314 case TREE_FILE_TYPE::LEGACY_PCB: return FILEEXT::LegacyPcbFileExtension;
315 case TREE_FILE_TYPE::SEXPR_PCB: return FILEEXT::KiCadPcbFileExtension;
316 case TREE_FILE_TYPE::GERBER: return FILEEXT::GerberFileExtensionsRegex;
317 case TREE_FILE_TYPE::GERBER_JOB_FILE: return FILEEXT::GerberJobFileExtension;
318 case TREE_FILE_TYPE::HTML: return FILEEXT::HtmlFileExtension;
319 case TREE_FILE_TYPE::PDF: return FILEEXT::PdfFileExtension;
320 case TREE_FILE_TYPE::TXT: return FILEEXT::TextFileExtension;
321 case TREE_FILE_TYPE::MD: return FILEEXT::MarkdownFileExtension;
322 case TREE_FILE_TYPE::NET: return FILEEXT::NetlistFileExtension;
323 case TREE_FILE_TYPE::NET_SPICE: return FILEEXT::SpiceFileExtension;
324 case TREE_FILE_TYPE::CMP_LINK: return FILEEXT::FootprintAssignmentFileExtension;
325 case TREE_FILE_TYPE::REPORT: return FILEEXT::ReportFileExtension;
326 case TREE_FILE_TYPE::FP_PLACE: return FILEEXT::FootprintPlaceFileExtension;
327 case TREE_FILE_TYPE::DRILL: return FILEEXT::DrillFileExtension;
328 case TREE_FILE_TYPE::DRILL_NC: return "nc";
329 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
330 case TREE_FILE_TYPE::SVG: return FILEEXT::SVGFileExtension;
331 case TREE_FILE_TYPE::DRAWING_SHEET: return FILEEXT::DrawingSheetFileExtension;
332 case TREE_FILE_TYPE::FOOTPRINT_FILE: return FILEEXT::KiCadFootprintFileExtension;
333 case TREE_FILE_TYPE::SCHEMATIC_LIBFILE: return FILEEXT::LegacySymbolLibFileExtension;
334 case TREE_FILE_TYPE::SEXPR_SYMBOL_LIB_FILE: return FILEEXT::KiCadSymbolLibFileExtension;
335 case TREE_FILE_TYPE::DESIGN_RULES: return FILEEXT::DesignRulesFileExtension;
336 case TREE_FILE_TYPE::ZIP_ARCHIVE: return FILEEXT::ArchiveFileExtension;
337 case TREE_FILE_TYPE::JOBSET_FILE: return FILEEXT::KiCadJobSetFileExtension;
338
339 case TREE_FILE_TYPE::ROOT:
340 case TREE_FILE_TYPE::UNKNOWN:
341 case TREE_FILE_TYPE::MAX:
342 case TREE_FILE_TYPE::DIRECTORY: break;
343 }
344
345 return wxEmptyString;
346}
347
348
349std::vector<wxString> getProjects( const wxDir& dir )
350{
351 std::vector<wxString> projects;
352 wxString dir_filename;
353 bool haveFile = dir.GetFirst( &dir_filename );
354
355 while( haveFile )
356 {
357 wxFileName file( dir_filename );
358
359 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
360 || file.GetExt() == FILEEXT::ProjectFileExtension )
361 projects.push_back( file.GetName() );
362
363 haveFile = dir.GetNext( &dir_filename );
364 }
365
366 return projects;
367}
368
369
370static git_repository* get_git_repository_for_file( const char* filename )
371{
372 git_repository* repo = nullptr;
373 git_buf repo_path = GIT_BUF_INIT;
374
375 // Find the repository path for the given file
376 if( git_repository_discover( &repo_path, filename, 0, NULL ) )
377 {
378#if 0
379 printf( "get_git_repository_for_file: %s\n", git_error_last()->message ); fflush( 0 );
380#endif
381 return nullptr;
382 }
383
384 if( git_repository_open( &repo, repo_path.ptr ) )
385 {
386 git_buf_free( &repo_path );
387 return nullptr;
388 }
389
390 // Free the git_buf memory
391 git_buf_free( &repo_path );
392
393 return repo;
394}
395
396
397wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
398 const wxTreeItemId& aParent,
399 std::vector<wxString>* aProjectNames,
400 bool aRecurse )
401{
402 TREE_FILE_TYPE type = TREE_FILE_TYPE::UNKNOWN;
403 wxFileName fn( aName );
404
405 if( KIPLATFORM::IO::IsFileHidden( aName ) )
406 return wxTreeItemId();
407
408 if( wxDirExists( aName ) )
409 {
410 type = TREE_FILE_TYPE::DIRECTORY;
411 }
412 else
413 {
414 // Filter
415 wxRegEx reg;
416 bool addFile = false;
417
418 for( const wxString& m_filter : m_filters )
419 {
420 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
421 wxString::Format( "Regex %s failed to compile.", m_filter ) );
422
423 if( reg.Matches( aName ) )
424 {
425 addFile = true;
426 break;
427 }
428 }
429
430 if( !addFile )
431 return wxTreeItemId();
432
433 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
434 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
435 {
436 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
437
438 if( ext == wxT( "" ) )
439 continue;
440
441 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ),
442 wxRE_ICASE ) && reg.Matches( aName ) )
443 {
444 type = (TREE_FILE_TYPE) i;
445 break;
446 }
447 }
448 }
449
450 wxString file = wxFileNameFromPath( aName );
451 wxFileName currfile( file );
452 wxFileName project( m_Parent->GetProjectFileName() );
453
454 // Ignore legacy projects with the same name as the current project
455 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
456 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
457 {
458 return wxTreeItemId();
459 }
460
461 if( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
462 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) )
463 {
464 if( aProjectNames )
465 {
466 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
467 return wxTreeItemId();
468 }
469 else
470 {
471 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
472 wxDir parentDir( parentTreeItem->GetDir() );
473 std::vector<wxString> projects = getProjects( parentDir );
474
475 if( !alg::contains( projects, currfile.GetName() ) )
476 return wxTreeItemId();
477 }
478 }
479
480 // also check to see if it is already there.
481 wxTreeItemIdValue cookie;
482 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
483
484 while( kid.IsOk() )
485 {
486 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
487
488 if( itemData && itemData->GetFileName() == aName )
489 return itemData->GetId(); // well, we would have added it, but it is already here!
490
491 kid = m_TreeProject->GetNextChild( aParent, cookie );
492 }
493
494 // Only show current files if both legacy and current files are present
495 if( type == TREE_FILE_TYPE::LEGACY_PROJECT || type == TREE_FILE_TYPE::JSON_PROJECT
496 || type == TREE_FILE_TYPE::LEGACY_SCHEMATIC || type == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
497 {
498 kid = m_TreeProject->GetFirstChild( aParent, cookie );
499
500 while( kid.IsOk() )
501 {
502 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
503
504 if( itemData )
505 {
506 wxFileName fname( itemData->GetFileName() );
507
508 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
509 {
510 switch( type )
511 {
512 case TREE_FILE_TYPE::LEGACY_PROJECT:
513 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
514 return wxTreeItemId();
515
516 break;
517
518 case TREE_FILE_TYPE::LEGACY_SCHEMATIC:
519 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
520 return wxTreeItemId();
521
522 break;
523
524 case TREE_FILE_TYPE::JSON_PROJECT:
525 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
526 m_TreeProject->Delete( kid );
527
528 break;
529
530 case TREE_FILE_TYPE::SEXPR_SCHEMATIC:
531 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
532 m_TreeProject->Delete( kid );
533
534 break;
535
536 default:
537 break;
538 }
539 }
540 }
541
542 kid = m_TreeProject->GetNextChild( aParent, cookie );
543 }
544 }
545
546 // Append the item (only appending the filename not the full path):
547 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
548 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
549
550 m_TreeProject->SetItemData( newItemId, data );
551 data->SetState( 0 );
552
553 // Mark root files (files which have the same aName as the project)
554 wxString fileName = currfile.GetName().Lower();
555 wxString projName = project.GetName().Lower();
556
557 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
558 data->SetRootFile( true );
559
560#ifndef __WINDOWS__
561 bool subdir_populated = false;
562#endif
563
564 // This section adds dirs and files found in the subdirs
565 // in this case AddFile is recursive, but for the first level only.
566 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
567 {
568 wxDir dir( aName );
569
570 if( dir.IsOpened() ) // protected dirs will not open properly.
571 {
572 std::vector<wxString> projects = getProjects( dir );
573 wxString dir_filename;
574 bool haveFile = dir.GetFirst( &dir_filename );
575
576 data->SetPopulated( true );
577
578#ifndef __WINDOWS__
579 subdir_populated = aRecurse;
580#endif
581
582 while( haveFile )
583 {
584 // Add name in tree, but do not recurse
585 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
586 addItemToProjectTree( path, newItemId, &projects, false );
587
588 haveFile = dir.GetNext( &dir_filename );
589 }
590 }
591
592 // Sort filenames by alphabetic order
593 m_TreeProject->SortChildren( newItemId );
594 }
595
596#ifndef __WINDOWS__
597 if( subdir_populated )
598 m_watcherNeedReset = true;
599#endif
600
601 return newItemId;
602}
603
604
606{
607 wxString pro_dir = m_Parent->GetProjectFileName();
608
609 if( !m_TreeProject )
610 m_TreeProject = new PROJECT_TREE( this );
611 else
612 m_TreeProject->DeleteAllItems();
613
614 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
615 return;
616
618 {
619 git_repository_free( m_TreeProject->GetGitRepo() );
620 m_TreeProject->SetGitRepo( nullptr );
621 }
622
623 wxFileName fn = pro_dir;
624 bool prjReset = false;
625
626 if( !fn.IsOk() )
627 {
628 fn.Clear();
629 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
630 fn.SetName( NAMELESS_PROJECT );
632 prjReset = true;
633 }
634
635 bool prjOpened = fn.FileExists();
636
637 // Bind the git repository to the project tree (if it exists)
638 if( ADVANCED_CFG::GetCfg().m_EnableGit )
639 {
640 m_TreeProject->SetGitRepo( get_git_repository_for_file( fn.GetPath().c_str() ) );
641 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
642 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
644 }
645
646 // We may have opened a legacy project, in which case GetProjectFileName will return the
647 // name of the migrated (new format) file, which may not have been saved to disk yet.
648 if( !prjOpened && !prjReset )
649 {
651 prjOpened = fn.FileExists();
652
653 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
655 }
656
657 // root tree:
658 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
659 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
660 m_TreeProject->SetItemBold( m_root, true );
661
662 // The main project file is now a JSON file
663 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( TREE_FILE_TYPE::JSON_PROJECT,
664 fn.GetFullPath(), m_TreeProject );
665
666 m_TreeProject->SetItemData( m_root, data );
667
668 // Now adding all current files if available
669 if( prjOpened )
670 {
671 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
672 wxDir dir( pro_dir );
673
674 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
675 {
676 std::vector<wxString> projects = getProjects( dir );
677 wxString filename;
678 bool haveFile = dir.GetFirst( &filename );
679
680 while( haveFile )
681 {
682 if( filename != fn.GetFullName() )
683 {
684 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
685
686 // Add items living in the project directory, and populate the item
687 // if it is a directory (sub directories will be not populated)
688 addItemToProjectTree( name, m_root, &projects, true );
689 }
690
691 haveFile = dir.GetNext( &filename );
692 }
693 }
694 }
695 else
696 {
697 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
698 }
699
700 m_TreeProject->Expand( m_root );
701
702 // Sort filenames by alphabetic order
703 m_TreeProject->SortChildren( m_root );
705}
706
707
709{
710 git_repository* repo = m_TreeProject->GetGitRepo();
711
712 if( !repo )
713 return false;
714
715 git_status_options opts;
716 git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION );
717
718 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
719 opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX
720 | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY;
721
722 git_status_list* status_list = nullptr;
723 int error = git_status_list_new( &status_list, repo, &opts );
724
725 if( error != GIT_OK )
726 return false;
727
728 bool has_changed_files = git_status_list_entrycount( status_list ) > 0;
729 git_status_list_free( status_list );
730 return has_changed_files;
731}
732
733
734void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
735{
736 wxTreeItemId curr_item = Event.GetItem();
737
738 // Ensure item is selected (Under Windows right click does not select the item)
739 m_TreeProject->SelectItem( curr_item );
740
741 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
742
743 bool can_switch_to_project = true;
744 bool can_create_new_directory = true;
745 bool can_open_this_directory = true;
746 bool can_edit = true;
747 bool can_rename = true;
748 bool can_delete = true;
749 bool run_jobs = false;
750
751 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
752 bool vcs_can_commit = hasChangedFiles();
753 bool vcs_can_init = !vcs_has_repo;
754 bool vcs_can_remove = vcs_has_repo;
755 bool vcs_can_fetch = vcs_has_repo && m_TreeProject->GitCommon()->HasPushAndPullRemote();
756 bool vcs_can_push = vcs_can_fetch && m_TreeProject->GitCommon()->HasLocalCommits();
757 bool vcs_can_pull = vcs_can_fetch;
758 bool vcs_can_switch = vcs_has_repo;
759 bool vcs_menu = ADVANCED_CFG::GetCfg().m_EnableGit;
760
761 // Check if the libgit2 library has been successfully initialized
762#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
763 int major, minor, rev;
764 bool libgit_init = ( git_libgit2_version( &major, &minor, &rev ) == GIT_OK );
765#else
766 //Work around libgit2 API change for supporting older platforms
767 bool libgit_init = true;
768#endif
769
770 vcs_menu &= libgit_init;
771
772 if( selection.size() == 0 )
773 return;
774
775 // Remove things that don't make sense for multiple selections
776 if( selection.size() != 1 )
777 {
778 can_switch_to_project = false;
779 can_create_new_directory = false;
780 can_rename = false;
781 }
782
783 for( PROJECT_TREE_ITEM* item : selection )
784 {
785 // Check for empty project
786 if( !item )
787 {
788 can_switch_to_project = false;
789 can_edit = false;
790 can_rename = false;
791 continue;
792 }
793
794 can_delete = item->CanDelete();
795 can_rename = item->CanRename();
796
797 switch( item->GetType() )
798 {
799 case TREE_FILE_TYPE::JSON_PROJECT:
800 case TREE_FILE_TYPE::LEGACY_PROJECT:
801 can_rename = false;
802
803 if( item->GetId() == m_TreeProject->GetRootItem() )
804 {
805 can_switch_to_project = false;
806 }
807 else
808 {
809 can_create_new_directory = false;
810 can_open_this_directory = false;
811 }
812 break;
813
814 case TREE_FILE_TYPE::DIRECTORY:
815 can_switch_to_project = false;
816 can_edit = false;
817 break;
818
819 case TREE_FILE_TYPE::ZIP_ARCHIVE:
820 case TREE_FILE_TYPE::PDF:
821 can_edit = false;
822 can_switch_to_project = false;
823 can_create_new_directory = false;
824 can_open_this_directory = 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 // Adjust directories to look like a file item (path and name).
1282 if( pathModified.GetFullName().IsEmpty() )
1283 {
1284 subdir = subdir.BeforeLast( '/' );
1285 fn = fn.BeforeLast( '/' );
1286 }
1287
1288 switch( event.GetChangeType() )
1289 {
1290 case wxFSW_EVENT_DELETE:
1291 case wxFSW_EVENT_CREATE:
1292 case wxFSW_EVENT_RENAME:
1294 break;
1295
1296 case wxFSW_EVENT_MODIFY:
1299 case wxFSW_EVENT_ACCESS:
1300 default:
1301 return;
1302 }
1303
1304 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1305
1306 if( !root_id.IsOk() )
1307 return;
1308
1309 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1310 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1311
1312 switch( event.GetChangeType() )
1313 {
1314 case wxFSW_EVENT_CREATE:
1315 {
1316 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1317
1318 // If we are in the process of renaming a file, select the new one
1319 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1320 // pair of DELETE and CREATE events.
1321 if( m_isRenaming && newitem.IsOk() )
1322 {
1323 m_TreeProject->SelectItem( newitem );
1324 m_isRenaming = false;
1325 }
1326 }
1327 break;
1328
1329 case wxFSW_EVENT_DELETE:
1330 while( kid.IsOk() )
1331 {
1332 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1333
1334 if( itemData && itemData->GetFileName() == fn )
1335 {
1336 m_TreeProject->Delete( kid );
1337 return;
1338 }
1339 kid = m_TreeProject->GetNextChild( root_id, cookie );
1340 }
1341 break;
1342
1343 case wxFSW_EVENT_RENAME :
1344 {
1345 const wxFileName& newpath = event.GetNewPath();
1346 wxString newdir = newpath.GetPath();
1347 wxString newfn = newpath.GetFullPath();
1348
1349 while( kid.IsOk() )
1350 {
1351 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1352
1353 if( itemData && itemData->GetFileName() == fn )
1354 {
1355 m_TreeProject->Delete( kid );
1356 break;
1357 }
1358
1359 kid = m_TreeProject->GetNextChild( root_id, cookie );
1360 }
1361
1362 // Add the new item only if it is not the current project file (root item).
1363 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1364 // called after an actual file rename, and the cleanup code does not explore the
1365 // root item, because it cannot be renamed by the user. Also, ensure the new file
1366 // actually exists on the file system before it is readded. On Linux, moving a file
1367 // to the trash can cause the same path to be returned in both the old and new paths
1368 // of the event, even though the file isn't there anymore.
1369 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1370
1371 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1372 {
1373 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1374 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1375
1376 // If the item exists, select it
1377 if( newitem.IsOk() )
1378 m_TreeProject->SelectItem( newitem );
1379 }
1380
1381 m_isRenaming = false;
1382 }
1383 break;
1384 }
1385
1386 // Sort filenames by alphabetic order
1387 m_TreeProject->SortChildren( root_id );
1388}
1389
1390
1392{
1393 m_watcherNeedReset = false;
1394
1395 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1396
1397#if defined( _WIN32 )
1398 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1399
1400 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1401 {
1402 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1403 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1404 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1405 // Windows Server shares :/
1406 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1408 return;
1409 }
1410 else
1411 {
1412 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1414 }
1415#endif
1416
1417 // Prepare file watcher:
1418 if( m_watcher )
1419 {
1420 m_watcher->RemoveAll();
1421 }
1422 else
1423 {
1425 m_watcher->SetOwner( this );
1426 }
1427
1428 // We can see wxString under a debugger, not a wxFileName
1429 wxFileName fn;
1430 fn.AssignDir( prj_dir );
1431 fn.DontFollowLink();
1432
1433 // Add directories which should be monitored.
1434 // under windows, we add the curr dir and all subdirs
1435 // under unix, we add only the curr dir and the populated subdirs
1436 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1437 // under unix, the file watcher needs more work to be efficient
1438 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1439 {
1440 wxLogNull logNo; // avoid log messages
1441#ifdef __WINDOWS__
1442 if( ! m_watcher->AddTree( fn ) )
1443 {
1444 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1445 TO_UTF8( fn.GetFullPath() ) );
1446 return;
1447 }
1448 }
1449#else
1450 if( !m_watcher->Add( fn ) )
1451 {
1452 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1453 TO_UTF8( fn.GetFullPath() ) );
1454 return;
1455 }
1456 }
1457
1458 if( m_TreeProject->IsEmpty() )
1459 return;
1460
1461 // Add subdirs
1462 wxTreeItemIdValue cookie;
1463 wxTreeItemId root_id = m_root;
1464
1465 std::stack < wxTreeItemId > subdirs_id;
1466
1467 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1468 int total_watch_count = 0;
1469
1470 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1471 {
1472 if( !kid.IsOk() )
1473 {
1474 if( subdirs_id.empty() ) // all items were explored
1475 {
1476 break;
1477 }
1478 else
1479 {
1480 root_id = subdirs_id.top();
1481 subdirs_id.pop();
1482 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1483
1484 if( !kid.IsOk() )
1485 continue;
1486 }
1487 }
1488
1489 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1490
1491 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1492 {
1493 // we can see wxString under a debugger, not a wxFileName
1494 const wxString& path = itemData->GetFileName();
1495
1496 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1497
1498 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1499 {
1500 fn.AssignDir( path );
1501 m_watcher->Add( fn );
1502 total_watch_count++;
1503
1504 // if kid is a subdir, push in list to explore it later
1505 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1506 subdirs_id.push( kid );
1507 }
1508 }
1509
1510 kid = m_TreeProject->GetNextChild( root_id, cookie );
1511 }
1512
1513 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1514 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1515#endif
1516
1517#if defined(DEBUG) && 1
1518 wxArrayString paths;
1519 m_watcher->GetWatchedPaths( &paths );
1520 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1521
1522 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1523 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1524#endif
1525 }
1526
1527
1529{
1530 // Make sure we don't try to inspect the tree after we've deleted its items.
1532
1533 m_TreeProject->DeleteAllItems();
1534
1535 // Remove the git repository when the project is unloaded
1536 if( m_TreeProject->GetGitRepo() )
1537 {
1538 git_repository_free( m_TreeProject->GetGitRepo() );
1539 m_TreeProject->SetGitRepo( nullptr );
1540 }
1541}
1542
1543
1544void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1545{
1548 m_TreeProject->Refresh();
1549
1550 aEvent.Skip();
1551}
1552
1553
1554void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1555{
1556 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1557 wxPaintDC dc( this );
1558
1559 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1560 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1561
1562 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1563 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1564}
1565
1566
1567void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1568{
1570}
1571
1572
1573void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1574{
1575 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1576
1577 wxString dir = tree_data->GetDir();
1578
1579 if( dir.empty() )
1580 {
1581 wxLogError( "Failed to initialize git project: project directory is empty." );
1582 return;
1583 }
1584
1585 // Check if the directory is already a git repository
1586 git_repository* repo = nullptr;
1587 int error = git_repository_open(&repo, dir.mb_str());
1588
1589 if( error == 0 )
1590 {
1591 // Directory is already a git repository
1592 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1593
1594 DisplayInfoMessage( topLevelParent,
1595 _( "The selected directory is already a git project." ) );
1596 git_repository_free( repo );
1597 return;
1598 }
1599 else
1600 {
1601 // Directory is not a git repository
1602 error = git_repository_init( &repo, dir.mb_str(), 0 );
1603
1604 if( error != 0 )
1605 {
1606 git_repository_free( repo );
1607
1608 if( m_gitLastError != git_error_last()->klass )
1609 {
1610 m_gitLastError = git_error_last()->klass;
1611 DisplayErrorMessage( m_parent, _( "Failed to initialize git project." ),
1612 git_error_last()->message );
1613 }
1614
1615 return;
1616 }
1617 else
1618 {
1619 m_TreeProject->SetGitRepo( repo );
1620 m_gitLastError = GIT_ERROR_NONE;
1621 }
1622 }
1623
1624 DIALOG_GIT_REPOSITORY dlg( wxGetTopLevelParent( this ), repo );
1625
1626 dlg.SetTitle( _( "Set default remote" ) );
1627
1628 if( dlg.ShowModal() != wxID_OK )
1629 return;
1630
1631 //Set up the git remote
1632
1637
1638 git_remote* remote = nullptr;
1639 wxString fullURL;
1640
1642 {
1643 fullURL = dlg.GetUsername() + "@" + dlg.GetRepoURL();
1644 }
1646 {
1647 fullURL = dlg.GetRepoURL().StartsWith( "https" ) ? "https://" : "http://";
1648
1649 if( !dlg.GetUsername().empty() )
1650 {
1651 fullURL.append( dlg.GetUsername() );
1652
1653 if( !dlg.GetPassword().empty() )
1654 {
1655 fullURL.append( wxS( ":" ) );
1656 fullURL.append( dlg.GetPassword() );
1657 }
1658
1659 fullURL.append( wxS( "@" ) );
1660 }
1661
1662 fullURL.append( dlg.GetBareRepoURL() );
1663 }
1664 else
1665 {
1666 fullURL = dlg.GetRepoURL();
1667 }
1668
1669
1670 error = git_remote_create_with_fetchspec( &remote, repo, "origin",
1671 fullURL.ToStdString().c_str(),
1672 "+refs/heads/*:refs/remotes/origin/*" );
1673
1674 if( error != GIT_OK )
1675 {
1676 if( m_gitLastError != git_error_last()->klass )
1677 {
1678 m_gitLastError = git_error_last()->klass;
1679 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1680 git_error_last()->message );
1681 }
1682
1683 return;
1684 }
1685
1686 m_gitLastError = GIT_ERROR_NONE;
1687
1689
1690 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1691 _( "Fetching Remote" ),
1692 1 ) );
1693
1694 handler.PerformFetch();
1695
1699
1703 Prj().GetLocalSettings().m_GitRepoType = "https";
1704 else
1705 Prj().GetLocalSettings().m_GitRepoType = "local";
1706}
1707
1708
1709void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
1710{
1711
1712}
1713
1714
1715void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
1716{
1717 git_repository* repo = m_TreeProject->GetGitRepo();
1718
1719 if( !repo )
1720 return;
1721
1723
1724 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1725 _( "Fetching Remote" ),
1726 1 ) );
1727
1728 if( handler.PerformPull() < PullResult::Success )
1729 {
1730 wxString errorMessage = handler.GetErrorString();
1731
1732 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), errorMessage );
1733 }
1734}
1735
1736
1737void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
1738{
1739 git_repository* repo = m_TreeProject->GetGitRepo();
1740
1741 if( !repo )
1742 return;
1743
1745
1746 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this,
1747 _( "Fetching Remote" ),
1748 1 ) );
1749
1750 if( handler.PerformPush() != PushResult::Success )
1751 {
1752 wxString errorMessage = handler.GetErrorString();
1753
1754 DisplayErrorMessage( m_parent, _( "Failed to push project" ), errorMessage );
1755 }
1756}
1757
1758
1759static int git_create_branch( git_repository* aRepo, wxString& aBranchName )
1760{
1761 git_oid head_oid;
1762
1763 if( int error = git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != 0 )
1764 {
1765 wxLogError( "Failed to lookup HEAD reference" );
1766 return error;
1767 }
1768
1769 // Lookup the current commit object
1770 git_commit* commit = nullptr;
1771 if( int error = git_commit_lookup( &commit, aRepo, &head_oid ) != GIT_OK )
1772 {
1773 wxLogError( "Failed to lookup commit" );
1774 return error;
1775 }
1776
1777 git_reference* branchRef = nullptr;
1778
1779 if( git_branch_create( &branchRef, aRepo, aBranchName.mb_str(), commit, 0 ) != 0 )
1780 {
1781 wxLogError( "Failed to create branch" );
1782 git_commit_free( commit );
1783 return -1;
1784 }
1785
1786 git_commit_free( commit );
1787 git_reference_free( branchRef );
1788
1789 return 0;
1790}
1791
1792
1793void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
1794{
1795 git_repository* repo = m_TreeProject->GetGitRepo();
1796
1797 if( !repo )
1798 return;
1799
1800 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), repo );
1801
1802 int retval = dlg.ShowModal();
1803 wxString branchName = dlg.GetBranchName();
1804
1805 if( retval == wxID_ADD )
1806 git_create_branch( repo, branchName );
1807 else if( retval != wxID_OK )
1808 return;
1809
1810 // Retrieve the reference to the existing branch using libgit2
1811 git_reference* branchRef = nullptr;
1812
1813 if( git_reference_lookup( &branchRef, repo, branchName.mb_str() ) != GIT_OK &&
1814 git_reference_dwim( &branchRef, repo, branchName.mb_str() ) != GIT_OK )
1815 {
1816 wxString errorMessage = wxString::Format( _( "Failed to lookup branch '%s': %s" ),
1817 branchName, giterr_last()->message );
1818 DisplayError( m_parent, errorMessage );
1819 return;
1820 }
1821
1822 const char* branchRefName = git_reference_name( branchRef );
1823
1824 git_object* branchObj = nullptr;
1825
1826 if( git_revparse_single( &branchObj, repo, branchName.mb_str() ) != 0 )
1827 {
1828 wxString errorMessage =
1829 wxString::Format( _( "Failed to find branch head for '%s'" ), branchName );
1830 DisplayError( m_parent, errorMessage );
1831 git_reference_free( branchRef );
1832 return;
1833 }
1834
1835
1836 // Switch to the branch
1837 if( git_checkout_tree( repo, branchObj, nullptr ) != 0 )
1838 {
1839 wxString errorMessage =
1840 wxString::Format( _( "Failed to switch to branch '%s'" ), branchName );
1841 DisplayError( m_parent, errorMessage );
1842 git_reference_free( branchRef );
1843 git_object_free( branchObj );
1844 return;
1845 }
1846
1847 // Update the HEAD reference
1848 if( git_repository_set_head( repo, branchRefName ) != 0 )
1849 {
1850 wxString errorMessage = wxString::Format(
1851 _( "Failed to update HEAD reference for branch '%s'" ), branchName );
1852 DisplayError( m_parent, errorMessage );
1853 git_reference_free( branchRef );
1854 git_object_free( branchObj );
1855 return;
1856 }
1857
1858 // Free resources
1859 git_reference_free( branchRef );
1860 git_object_free( branchObj );
1861}
1862
1863
1864void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
1865{
1866 git_repository* repo = m_TreeProject->GetGitRepo();
1867
1868 if( !repo
1869 || !IsOK( wxGetTopLevelParent( this ),
1870 _( "Are you sure you want to remove git tracking from this project?" ) ) )
1871 {
1872 return;
1873 }
1874
1875 // Remove the VCS (git) from the project directory
1876 git_repository_free( repo );
1877 m_TreeProject->SetGitRepo( nullptr );
1878
1879 // Remove the .git directory
1880 wxFileName fn( m_Parent->GetProjectFileName() );
1881 fn.AppendDir( ".git" );
1882
1883 wxString errors;
1884
1885 if( !RmDirRecursive( fn.GetPath(), &errors ) )
1886 {
1887 DisplayErrorMessage( m_parent, _( "Failed to remove git directory" ), errors );
1888 }
1889
1890 // Clear all item states
1891 std::stack<wxTreeItemId> items;
1892 items.push( m_TreeProject->GetRootItem() );
1893
1894 while( !items.empty() )
1895 {
1896 wxTreeItemId current = items.top();
1897 items.pop();
1898
1899 // Process the current item
1900 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
1901
1902 wxTreeItemIdValue cookie;
1903 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
1904
1905 while( child.IsOk() )
1906 {
1907 items.push( child );
1908 child = m_TreeProject->GetNextChild( current, cookie );
1909 }
1910 }
1911}
1912
1913
1915{
1916 if( ADVANCED_CFG::GetCfg().m_EnableGit == false )
1917 return;
1918
1919 if( !m_TreeProject )
1920 return;
1921
1922 wxTimeSpan timeSinceLastUpdate = wxDateTime::Now() - m_lastGitStatusUpdate;
1923
1924 if( timeSinceLastUpdate.Abs() < wxTimeSpan::Seconds( 2 ) )
1925 return;
1926
1927 m_lastGitStatusUpdate = wxDateTime::Now();
1928
1929 wxTreeItemId kid = m_TreeProject->GetRootItem();
1930
1931 if( !kid.IsOk() )
1932 return;
1933
1934 git_repository* repo = m_TreeProject->GetGitRepo();
1935
1936 if( !repo )
1937 return;
1938
1939 // Get Current Branch
1940
1941 git_reference* currentBranchReference = nullptr;
1942 git_repository_head( &currentBranchReference, repo );
1943
1944 PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid );
1945 wxFileName rootFilename( rootItem->GetFileName() );
1946 wxString repoWorkDir( git_repository_workdir( repo ) );
1947
1948 // Get the current branch name
1949 if( currentBranchReference )
1950 {
1951 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
1952 wxString branchName = git_reference_shorthand( currentBranchReference );
1953
1954 m_TreeProject->SetItemText( kid, filename + " [" + branchName + "]" );
1955 git_reference_free( currentBranchReference );
1956 }
1957 else
1958 {
1959 if( giterr_last()->klass != m_gitLastError )
1960 wxLogError( "Failed to lookup current branch: %s", giterr_last()->message );
1961
1962 m_gitLastError = giterr_last()->klass;
1963 }
1964
1965 // Collect a map to easily set the state of each item
1966 std::map<wxString, wxTreeItemId> branchMap;
1967 {
1968 std::stack<wxTreeItemId> items;
1969 items.push( kid );
1970
1971 while( !items.empty() )
1972 {
1973 kid = items.top();
1974 items.pop();
1975
1976 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
1977
1978 wxString gitAbsPath = nextItem->GetFileName();
1979#ifdef _WIN32
1980 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
1981#endif
1982 branchMap[gitAbsPath] = kid;
1983
1984 wxTreeItemIdValue cookie;
1985 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
1986
1987 while( child.IsOk() )
1988 {
1989 items.push( child );
1990 child = m_TreeProject->GetNextChild( kid, cookie );
1991 }
1992 }
1993 }
1994
1995 wxFileName relative = rootFilename;
1996 relative.MakeRelativeTo( repoWorkDir );
1997 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
1998
1999#ifdef _WIN32
2000 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2001#endif
2002
2003 const char* pathspec[] = { pathspecStr.c_str().AsChar() };
2004
2005 git_status_options status_options;
2006 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2007 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2008 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED;
2009 status_options.pathspec = { (char**) pathspec, 1 };
2010
2011 git_index* index = nullptr;
2012
2013 if( git_repository_index( &index, repo ) != GIT_OK )
2014 {
2015 m_gitLastError = giterr_last()->klass;
2016 wxLogTrace( traceGit, wxS( "Failed to get git index: %s" ), giterr_last()->message );
2017 return;
2018 }
2019
2020 git_status_list* status_list = nullptr;
2021
2022 if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK )
2023 {
2024 wxLogTrace( traceGit, wxS( "Failed to get git status list: %s" ), giterr_last()->message );
2025 git_index_free( index );
2026 return;
2027 }
2028
2029 auto [ localChanges, remoteChanges ] = m_TreeProject->GitCommon()->GetDifferentFiles();
2030
2031 size_t count = git_status_list_entrycount( status_list );
2032
2033 for( size_t ii = 0; ii < count; ++ii )
2034 {
2035 const git_status_entry* entry = git_status_byindex( status_list, ii );
2036 std::string path( entry->head_to_index? entry->head_to_index->old_file.path
2037 : entry->index_to_workdir->old_file.path );
2038
2039 wxString absPath = repoWorkDir;
2040 absPath << path;
2041
2042 auto iter = branchMap.find( absPath );
2043
2044 if( iter == branchMap.end() )
2045 continue;
2046
2047 // If we are current, don't continue because we still need to check to see if the
2048 // current commit is ahead/behind the remote. If the file is modified/added/deleted,
2049 // that is the main status we want to show.
2050 if( entry->status == GIT_STATUS_CURRENT )
2051 {
2052 m_TreeProject->SetItemState(
2053 iter->second,
2055 }
2056 else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) )
2057 {
2058 m_TreeProject->SetItemState(
2059 iter->second,
2061 continue;
2062 }
2063 else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) )
2064 {
2065 m_TreeProject->SetItemState(
2066 iter->second,
2067 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) );
2068 continue;
2069 }
2070 else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) )
2071 {
2072 m_TreeProject->SetItemState(
2073 iter->second,
2075 continue;
2076 }
2077
2078 // Check if file is up to date with the remote
2079 if( localChanges.count( path ) )
2080 {
2081 m_TreeProject->SetItemState(
2082 iter->second,
2083 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ) );
2084 continue;
2085 }
2086 else if( remoteChanges.count( path ) )
2087 {
2088 m_TreeProject->SetItemState(
2089 iter->second,
2090 static_cast<int>( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ) );
2091 continue;
2092 }
2093 else
2094 {
2095 m_TreeProject->SetItemState(
2096 iter->second,
2098 continue;
2099 }
2100 }
2101
2102 git_status_list_free( status_list );
2103 git_index_free( index );
2104}
2105
2106
2107void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2108{
2109 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2110
2111 git_repository* repo = m_TreeProject->GetGitRepo();
2112
2113 if( repo == nullptr )
2114 {
2115 wxMessageBox( "The selected directory is not a git project." );
2116 return;
2117 }
2118
2119 git_config* config = nullptr;
2120 git_repository_config( &config, repo );
2121
2122 // Read relevant data from the git config
2123 wxString authorName;
2124 wxString authorEmail;
2125
2126 // Read author name
2127 git_config_entry* name_c = nullptr;
2128 git_config_entry* email_c = nullptr;
2129 int authorNameError = git_config_get_entry( &name_c, config, "user.name" );
2130
2131 if( authorNameError != 0 || name_c == nullptr )
2132 {
2133 authorName = Pgm().GetCommonSettings()->m_Git.authorName;
2134 }
2135 else
2136 {
2137 authorName = name_c->value;
2138 git_config_entry_free( name_c );
2139 }
2140
2141 // Read author email
2142 int authorEmailError = git_config_get_entry( &email_c, config, "user.email" );
2143
2144 if( authorEmailError != 0 || email_c == nullptr )
2145 {
2146 authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail;
2147 }
2148 else
2149 {
2150 authorEmail = email_c->value;
2151 git_config_entry_free( email_c );
2152 }
2153
2154 // Free the config object
2155 git_config_free( config );
2156
2157 // Collect modified files in the repository
2158 git_status_options status_options;
2159 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2160 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2161 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED;
2162
2163 git_status_list* status_list = nullptr;
2164 git_status_list_new( &status_list, repo, &status_options );
2165
2166 std::map<wxString, int> modifiedFiles;
2167
2168 size_t count = git_status_list_entrycount( status_list );
2169
2170 std::set<wxString> selected_files;
2171
2172 for( PROJECT_TREE_ITEM* item : tree_data )
2173 {
2174 if( item->GetType() != TREE_FILE_TYPE::DIRECTORY )
2175 selected_files.emplace( item->GetFileName() );
2176 }
2177
2178 for( size_t i = 0; i < count; ++i )
2179 {
2180 const git_status_entry* entry = git_status_byindex( status_list, i );
2181
2182 // Check if the file is modified (index or workdir changes)
2183 if( entry->status == GIT_STATUS_CURRENT
2184 || ( entry->status & ( GIT_STATUS_CONFLICTED | GIT_STATUS_IGNORED ) ) )
2185 {
2186 continue;
2187 }
2188
2189 wxFileName fn( entry->index_to_workdir->old_file.path );
2190 fn.MakeAbsolute( git_repository_workdir( repo ) );
2191
2192 wxString filePath( entry->index_to_workdir->old_file.path, wxConvUTF8 );
2193
2194 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2195 {
2196 modifiedFiles.emplace( filePath, entry->status );
2197 }
2198 else if( selected_files.count( fn.GetFullPath() ) )
2199 {
2200 modifiedFiles.emplace( filePath, entry->status );
2201 }
2202 }
2203
2204 git_status_list_free( status_list );
2205
2206 // Create a commit dialog
2207 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, authorName, authorEmail,
2208 modifiedFiles );
2209 auto ret = dlg.ShowModal();
2210
2211 if( ret == wxID_OK )
2212 {
2213 // Commit the changes
2214 git_oid tree_id;
2215 git_tree* tree = nullptr;
2216 git_commit* parent = nullptr;
2217 git_index* index = nullptr;
2218
2219 std::vector<wxString> files = dlg.GetSelectedFiles();
2220
2221 if( dlg.GetCommitMessage().IsEmpty() )
2222 {
2223 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2224 return;
2225 }
2226
2227 if( files.empty() )
2228 {
2229 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2230 return;
2231 }
2232
2233 if( git_repository_index( &index, repo ) != 0 )
2234 {
2235 wxMessageBox( _( "Failed to get repository index: %s" ), giterr_last()->message );
2236 return;
2237 }
2238
2239 for( wxString& file :files )
2240 {
2241 if( git_index_add_bypath( index, file.mb_str() ) != 0 )
2242 {
2243 wxMessageBox( _( "Failed to add file to index: %s" ), giterr_last()->message );
2244 git_index_free( index );
2245 return;
2246 }
2247 }
2248
2249 if( git_index_write( index ) != 0 )
2250 {
2251 wxMessageBox( _( "Failed to write index: %s" ), giterr_last()->message );
2252 git_index_free( index );
2253 return;
2254 }
2255
2256 if (git_index_write_tree( &tree_id, index ) != 0)
2257 {
2258 wxMessageBox( _( "Failed to write tree: %s" ), giterr_last()->message );
2259 git_index_free( index );
2260 return;
2261 }
2262
2263 git_index_free( index );
2264
2265 if( git_tree_lookup( &tree, repo, &tree_id ) != 0 )
2266 {
2267 wxMessageBox( _( "Failed to lookup tree: %s" ), giterr_last()->message );
2268 return;
2269 }
2270
2271 git_reference* headRef = nullptr;
2272
2273 if( git_repository_head( &headRef, repo ) != 0 )
2274 {
2275 wxMessageBox( _( "Failed to get HEAD reference: %s" ), giterr_last()->message );
2276 git_index_free( index );
2277 return;
2278 }
2279
2280 if( git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT ) != 0 )
2281 {
2282 wxMessageBox( _( "Failed to get commit: %s" ), giterr_last()->message );
2283 git_reference_free( headRef );
2284 git_index_free( index );
2285 return;
2286 }
2287
2288 git_reference_free( headRef );
2289
2290 const wxString& commit_msg = dlg.GetCommitMessage();
2291 const wxString& author_name = dlg.GetAuthorName();
2292 const wxString& author_email = dlg.GetAuthorEmail();
2293
2294 git_signature* author = nullptr;
2295
2296 if( git_signature_now( &author, author_name.mb_str(), author_email.mb_str() ) != 0 )
2297 {
2298 wxMessageBox( _( "Failed to create author signature: %s" ), giterr_last()->message );
2299 return;
2300 }
2301
2302 git_oid oid;
2303
2304#if( LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
2305 && ( LIBGIT2_VER_REVISION < 2 || LIBGIT2_VER_REVISION == 3 ) )
2306 /*
2307 * For libgit2 versions 1.8.0, 1.8.1. (cf19ddc52)
2308 * This change was reverted for 1.8.2 (49d3fadfc, main branch)
2309 * The revert for 1.8.2 was not included for 1.8.3 (which is on the maint/v1.8 branch, not main)
2310 * This change was also reverted for 1.8.4 (94ba816f6, also maint/v1.8 branch)
2311 *
2312 * As of 1.8.4, the history is like this:
2313 *
2314 * * 3f4182d15 (tag: v1.8.4, maint/v1.8)
2315 * * 94ba816f6 Revert "commit: fix const declaration" [puts const back]
2316 * * 3353f78e8 (tag: v1.8.3)
2317 * | * 4ce872a0f (tag: v1.8.2-rc1, tag: v1.8.2)
2318 * | * 49d3fadfc Revert "commit: fix const declaration" [puts const back]
2319 * |/
2320 * * 36f7e21ad (tag: v1.8.1)
2321 * * d74d49148 (tag: v1.8.0)
2322 * * cf19ddc52 commit: fix const declaration [removes const]
2323 */
2324 git_commit* const parents[1] = { parent };
2325#else
2326 // For libgit2 versions older than 1.8.0, or equal to 1.8.2, or 1.8.4+
2327 const git_commit* parents[1] = { parent };
2328#endif
2329
2330 if( git_commit_create( &oid, repo, "HEAD", author, author, nullptr, commit_msg.mb_str(), tree,
2331 1, parents ) != 0 )
2332 {
2333 wxMessageBox( _( "Failed to create commit: %s" ), giterr_last()->message );
2334 return;
2335 }
2336
2337 git_signature_free( author );
2338 git_commit_free( parent );
2339 git_tree_free( tree );
2340 }
2341}
2342
2343
2344void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
2345{
2346
2347}
2348
2349
2350bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
2351{
2352 git_index *index;
2353 size_t entry_pos;
2354
2355 git_repository* repo = m_TreeProject->GetGitRepo();
2356
2357 if( !repo )
2358 return false;
2359
2360 if( git_repository_index( &index, repo ) != 0 )
2361 return false;
2362
2363 // If we successfully find the file in the index, we may not add it to the VCS
2364 if( git_index_find( &entry_pos, index, aFile.mb_str() ) == 0 )
2365 {
2366 git_index_free( index );
2367 return false;
2368 }
2369
2370 git_index_free( index );
2371 return true;
2372}
2373
2374
2375void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
2376{
2377 git_repository* repo = m_TreeProject->GetGitRepo();
2378
2379 if( !repo )
2380 return;
2381
2382 GIT_SYNC_HANDLER handler( repo );
2383 handler.PerformSync();
2384}
2385
2386
2387void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
2388{
2389 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2390
2391 if( !gitCommon )
2392 return;
2393
2394 GIT_PULL_HANDLER handler( gitCommon );
2395 handler.PerformFetch();
2396}
2397
2398
2399void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
2400{
2401 git_repository* repo = m_TreeProject->GetGitRepo();
2402
2403 if( !repo )
2404 return;
2405
2406 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
2407 handler.PerformResolveConflict();
2408}
2409
2410
2411void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
2412{
2413 git_repository* repo = m_TreeProject->GetGitRepo();
2414
2415 if( !repo )
2416 return;
2417
2418 GIT_REVERT_HANDLER handler( repo );
2419 handler.PerformRevert();
2420}
2421
2422
2423void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
2424{
2425 git_repository* repo = m_TreeProject->GetGitRepo();
2426
2427 if( !repo )
2428 return;
2429
2430 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
2431 handler.PerformRemoveFromIndex();
2432}
2433
2434
2436{
2437
2438}
const char * name
Definition: DXF_plotter.cpp:59
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)
void SetSSHKey(const wxString &aSSHKey)
void SetUsername(const wxString &aUsername)
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
void UpdateCurrentBranchInfo()
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.
static wxString GetDefaultUserProjectsPath()
Gets the default path we point users to create projects.
Definition: paths.cpp:143
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition: pgm_base.cpp:689
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)
Remove the directory aDirName and all its contents including subdirectories and their files.
Definition: gestfich.cpp:311
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:597
#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:70
bool StoreSecret(const wxString &aService, const wxString &aKey, const wxString &aSecret)
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:374
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:1073
#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.