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 <git/git_backend.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#include <wx/timer.h>
35#include <wx/wupdlock.h>
36#include <wx/log.h>
37
39#include <advanced_config.h>
40#include <bitmaps.h>
41#include <bitmap_store.h>
42#include <confirm.h>
46#include <gestfich.h>
47#include <macros.h>
48#include <trace_helpers.h>
51#include <core/kicad_algo.h>
52#include <paths.h>
54#include <scoped_set_reset.h>
55#include <string_utils.h>
56#include <thread_pool.h>
57#include <launch_ext.h>
58#include <wx/dcclient.h>
59#include <wx/progdlg.h>
60#include <wx/settings.h>
61
77
79
80#include "project_tree_item.h"
81#include "project_tree.h"
82#include "pgm_kicad.h"
83#include "kicad_id.h"
84#include "kicad_manager_frame.h"
85
86#include "project_tree_pane.h"
87#include <widgets/kistatusbar.h>
88
89#include <kiplatform/io.h>
90#include <kiplatform/secrets.h>
91
92
93/* Note about the project tree build process:
94 * Building the project tree can be *very* long if there are a lot of subdirectories in the
95 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
96 * user's home directory.
97 * So the tree project is built "on demand":
98 * First the tree is built from the current directory and shows files and subdirs.
99 * > First level subdirs trees are built (i.e subdirs contents are not read)
100 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
101 * populated on the fly.
102 */
103
104// list of files extensions listed in the tree project window
105// Add extensions in a compatible regex format to see others files types
106static const wxChar* s_allowedExtensionsToList[] =
107{
108 wxT( "^.*\\.pro$" ),
109 wxT( "^.*\\.kicad_pro$" ),
110 wxT( "^.*\\.pdf$" ),
111 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
112 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
113 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
114 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
115 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
116 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
117 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
118 wxT( "^.*\\.net$" ), // pcbnew netlist file
119 wxT( "^.*\\.cir$" ), // Spice netlist file
120 wxT( "^.*\\.lib$" ), // Legacy schematic library file
121 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
122 wxT( "^.*\\.txt$" ), // Text files
123 wxT( "^.*\\.md$" ), // Markdown files
124 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
125 wxT( "^.*\\.gbr$" ), // Gerber file
126 wxT( "^.*\\.gbrjob$" ), // Gerber job file
127 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
128 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
129 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
130 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
131 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
132 wxT( "^.*\\.odt$" ),
133 wxT( "^.*\\.htm$" ),
134 wxT( "^.*\\.html$" ),
135 wxT( "^.*\\.rpt$" ), // Report files
136 wxT( "^.*\\.csv$" ), // Report files in comma separated format
137 wxT( "^.*\\.pos$" ), // Footprint position files
138 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
139 wxT( "^.*\\.drl$" ), // Excellon drill files
140 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
141 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
142 wxT( "^.*\\.svg$" ), // SVG print/plot files
143 wxT( "^.*\\.ps$" ), // PostScript plot files
144 wxT( "^.*\\.zip$" ), // Zip archive files
145 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
146 nullptr // end of list
147};
148
149
155
156
158{
159 ID_PROJECT_TXTEDIT = 8700, // Start well above wxIDs
165
166 ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project
167 ID_GIT_CLONE_PROJECT, // Clone a project from a remote repository
168 ID_GIT_COMMIT_PROJECT, // Commit all files in the project
169 ID_GIT_COMMIT_FILE, // Commit a single file
170 ID_GIT_SYNC_PROJECT, // Sync the project with the remote repository (pull and push -- same as Update)
171 ID_GIT_FETCH, // Fetch the remote repository (without merging -- this is the same as Refresh)
172 ID_GIT_PUSH, // Push the local repository to the remote repository
173 ID_GIT_PULL, // Pull the remote repository to the local repository
174 ID_GIT_RESOLVE_CONFLICT, // Present the user with a resolve conflicts dialog (ours/theirs/merge)
175 ID_GIT_REVERT_LOCAL, // Revert the local repository to the last commit
176 ID_GIT_COMPARE, // Compare the current project to a different branch or commit in the git repository
177 ID_GIT_REMOVE_VCS, // Toggle Git integration for this project (preference in kicad_prl)
178 ID_GIT_ADD_TO_INDEX, // Add a file to the git index
179 ID_GIT_REMOVE_FROM_INDEX, // Remove a file from the git index
180 ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch
181 ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch
182 ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch
183 ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch
184 ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch
185 ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch
186
188};
189
190
191BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
192 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
193 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
194 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
201
217
219
220 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
221 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
222END_EVENT_TABLE()
223
224
225wxDECLARE_EVENT( UPDATE_ICONS, wxCommandEvent );
226
228 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxTAB_TRAVERSAL )
229{
230 m_Parent = parent;
231 m_TreeProject = nullptr;
232 m_isRenaming = false;
233 m_selectedItem = nullptr;
234 m_watcherNeedReset = false;
235 m_gitLastError = GIT_ERROR_NONE;
236 m_watcher = nullptr;
237 m_gitIconsInitialized = false;
238
239 Bind( wxEVT_FSWATCHER,
240 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
241
242 Bind( wxEVT_SYS_COLOUR_CHANGED,
243 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
244
245 m_gitSyncTimer.SetOwner( this );
246 m_gitStatusTimer.SetOwner( this );
247 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
248 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
249 /*
250 * Filtering is now inverted: the filters are actually used to _enable_ support
251 * for a given file type.
252 */
253 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
254 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
255
256 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
257
259}
260
261
263{
264 Unbind( wxEVT_FSWATCHER,
265 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
266 Unbind( wxEVT_SYS_COLOUR_CHANGED,
267 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
268
269 m_gitSyncTimer.Stop();
270 m_gitStatusTimer.Stop();
271 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
272 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
274
275 if( m_gitSyncTask.valid() )
276 m_gitSyncTask.wait();
277
278 if( m_gitStatusIconTask.valid() )
279 m_gitStatusIconTask.wait();
280}
281
282
284{
285 if( m_watcher )
286 {
287 m_watcher->RemoveAll();
288 m_watcher->SetOwner( nullptr );
289 delete m_watcher;
290 m_watcher = nullptr;
291 }
292}
293
294
296{
297 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
298
299 if( tree_data.size() != 1 )
300 return;
301
302 wxString prj_filename = tree_data[0]->GetFileName();
303
304 m_Parent->LoadProject( prj_filename );
305}
306
307
308void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
309{
310 // Get the root directory name:
311 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
312
313 for( PROJECT_TREE_ITEM* item_data : tree_data )
314 {
315 // Ask for the new sub directory name
316 wxString curr_dir = item_data->GetDir();
317
318 if( curr_dir.IsEmpty() )
319 {
320 // Use project path if the tree view path was empty.
321 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
322
323 // As a last resort use the user's documents folder.
324 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
326
327 if( !curr_dir.IsEmpty() )
328 curr_dir += wxFileName::GetPathSeparator();
329 }
330
331 LaunchExternal( curr_dir );
332 }
333}
334
335
336void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
337{
338 // Get the root directory name:
339 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
340
341 for( PROJECT_TREE_ITEM* item_data : tree_data )
342 {
343 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
344
345 // Ask for the new sub directory name
346 wxString curr_dir = item_data->GetDir();
347
348 if( curr_dir.IsEmpty() )
349 curr_dir = prj_dir;
350
351 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
352
353 if( new_dir.IsEmpty() )
354 return;
355
356 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
357
358 if( !wxMkdir( full_dirname ) )
359 return;
360
361 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
362 }
363}
364
365
367{
368 switch( type )
369 {
388 case TREE_FILE_TYPE::DRILL_NC: return "nc";
389 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
399
403 case TREE_FILE_TYPE::DIRECTORY: break;
404 }
405
406 return wxEmptyString;
407}
408
409
410std::vector<wxString> getProjects( const wxDir& dir )
411{
412 std::vector<wxString> projects;
413 wxString dir_filename;
414 bool haveFile = dir.GetFirst( &dir_filename );
415
416 while( haveFile )
417 {
418 wxFileName file( dir_filename );
419
420 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
421 || file.GetExt() == FILEEXT::ProjectFileExtension )
422 projects.push_back( file.GetName() );
423
424 haveFile = dir.GetNext( &dir_filename );
425 }
426
427 return projects;
428}
429
430
431
432
433
434wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
435 const wxTreeItemId& aParent,
436 std::vector<wxString>* aProjectNames,
437 bool aRecurse )
438{
440 wxFileName fn( aName );
441
442 if( KIPLATFORM::IO::IsFileHidden( aName ) )
443 return wxTreeItemId();
444
445 if( wxDirExists( aName ) )
446 {
448 }
449 else
450 {
451 // Filter
452 wxRegEx reg;
453 bool addFile = false;
454
455 for( const wxString& m_filter : m_filters )
456 {
457 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
458 wxString::Format( "Regex %s failed to compile.", m_filter ) );
459
460 if( reg.Matches( aName ) )
461 {
462 addFile = true;
463 break;
464 }
465 }
466
467 if( !addFile )
468 return wxTreeItemId();
469
470 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
471 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
472 {
473 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
474
475 if( ext == wxT( "" ) )
476 continue;
477
478 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ), wxRE_ICASE )
479 && reg.Matches( aName ) )
480 {
481 type = (TREE_FILE_TYPE) i;
482 break;
483 }
484 }
485 }
486
487 wxString file = wxFileNameFromPath( aName );
488 wxFileName currfile( file );
489 wxFileName project( m_Parent->GetProjectFileName() );
490 bool showAllSchematics = m_TreeProject->GetGitRepo() != nullptr;
491
492 // Ignore legacy projects with the same name as the current project
493 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
494 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
495 {
496 return wxTreeItemId();
497 }
498
499 if( !showAllSchematics && ( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
500 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) ) )
501 {
502 if( aProjectNames )
503 {
504 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
505 return wxTreeItemId();
506 }
507 else
508 {
509 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
510
511 if( !parentTreeItem )
512 return wxTreeItemId();
513
514 wxDir parentDir( parentTreeItem->GetDir() );
515 std::vector<wxString> projects = getProjects( parentDir );
516
517 if( !alg::contains( projects, currfile.GetName() ) )
518 return wxTreeItemId();
519 }
520 }
521
522 // also check to see if it is already there.
523 wxTreeItemIdValue cookie;
524 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
525
526 while( kid.IsOk() )
527 {
528 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
529
530 if( itemData && itemData->GetFileName() == aName )
531 return itemData->GetId(); // well, we would have added it, but it is already here!
532
533 kid = m_TreeProject->GetNextChild( aParent, cookie );
534 }
535
536 // Only show current files if both legacy and current files are present
541 {
542 kid = m_TreeProject->GetFirstChild( aParent, cookie );
543
544 while( kid.IsOk() )
545 {
546 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
547
548 if( itemData )
549 {
550 wxFileName fname( itemData->GetFileName() );
551
552 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
553 {
554 switch( type )
555 {
557 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
558 return wxTreeItemId();
559
560 break;
561
563 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
564 return wxTreeItemId();
565
566 break;
567
569 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
570 m_TreeProject->Delete( kid );
571
572 break;
573
575 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
576 m_TreeProject->Delete( kid );
577
578 break;
579
580 default:
581 break;
582 }
583 }
584 }
585
586 kid = m_TreeProject->GetNextChild( aParent, cookie );
587 }
588 }
589
590 // Append the item (only appending the filename not the full path):
591 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
592 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
593
594 m_TreeProject->SetItemData( newItemId, data );
595 data->SetState( 0 );
596
597 // Mark root files (files which have the same aName as the project)
598 wxString fileName = currfile.GetName().Lower();
599 wxString projName = project.GetName().Lower();
600
601 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
602 data->SetRootFile( true );
603
604#ifndef __WINDOWS__
605 bool subdir_populated = false;
606#endif
607
608 // This section adds dirs and files found in the subdirs
609 // in this case AddFile is recursive, but for the first level only.
610 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
611 {
612 wxDir dir( aName );
613
614 if( dir.IsOpened() ) // protected dirs will not open properly.
615 {
616 std::vector<wxString> projects = getProjects( dir );
617 wxString dir_filename;
618 bool haveFile = dir.GetFirst( &dir_filename );
619
620 data->SetPopulated( true );
621
622#ifndef __WINDOWS__
623 subdir_populated = aRecurse;
624#endif
625
626 while( haveFile )
627 {
628 // Add name in tree, but do not recurse
629 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
630 addItemToProjectTree( path, newItemId, &projects, false );
631
632 haveFile = dir.GetNext( &dir_filename );
633 }
634 }
635
636 // Sort filenames by alphabetic order
637 m_TreeProject->SortChildren( newItemId );
638 }
639
640#ifndef __WINDOWS__
641 if( subdir_populated )
642 m_watcherNeedReset = true;
643#endif
644
645 return newItemId;
646}
647
648
650{
651 std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
652 std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
653
654 m_gitStatusTimer.Stop();
655 m_gitSyncTimer.Stop();
656
657 if( m_TreeProject && m_TreeProject->GetGitRepo() )
658 m_TreeProject->GitCommon()->SetCancelled( true );
659
660 m_gitTreeCache.clear();
661 m_gitStatusIcons.clear();
662
663 wxString pro_dir = m_Parent->GetProjectFileName();
664
665 if( !m_TreeProject )
666 m_TreeProject = new PROJECT_TREE( this );
667 else
668 m_TreeProject->DeleteAllItems();
669
670 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
671 return;
672
673 if( m_TreeProject->GetGitRepo() )
674 {
675 git_repository* repo = m_TreeProject->GetGitRepo();
677 m_TreeProject->SetGitRepo( nullptr );
678 m_gitIconsInitialized = false;
679 }
680
681 wxFileName fn = pro_dir;
682 bool prjReset = false;
683
684 if( !fn.IsOk() )
685 {
686 fn.Clear();
687 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
688 fn.SetName( NAMELESS_PROJECT );
690 prjReset = true;
691 }
692
693 bool prjOpened = fn.FileExists();
694
695 // Bind the git repository to the project tree (if it exists and not disabled for this project)
696 if( Pgm().GetCommonSettings()->m_Git.enableGit
697 && !Prj().GetLocalSettings().m_GitIntegrationDisabled )
698 {
699 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
700
701 if( m_TreeProject->GetGitRepo() )
702 {
703 // Reset the cancel flag so git operations work after project switches.
704 // EmptyTreePrj() sets this to true during shutdown.
705 m_TreeProject->GitCommon()->SetCancelled( false );
706
707 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
708
709 if( canonicalWorkDir )
710 {
712 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
713 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
714 }
715
716 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
717 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
718 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
719 }
720 }
721
722 // We may have opened a legacy project, in which case GetProjectFileName will return the
723 // name of the migrated (new format) file, which may not have been saved to disk yet.
724 if( !prjOpened && !prjReset )
725 {
727 prjOpened = fn.FileExists();
728
729 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
731 }
732
733 // root tree:
734 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
735 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
736 m_TreeProject->SetItemBold( m_root, true );
737
738 // The main project file is now a JSON file
741
742 m_TreeProject->SetItemData( m_root, data );
743
744 // Now adding all current files if available
745 if( prjOpened )
746 {
747 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
748 wxDir dir( pro_dir );
749
750 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
751 {
752 std::vector<wxString> projects = getProjects( dir );
753 wxString filename;
754 bool haveFile = dir.GetFirst( &filename );
755
756 while( haveFile )
757 {
758 if( filename != fn.GetFullName() )
759 {
760 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
761
762 // Add items living in the project directory, and populate the item
763 // if it is a directory (sub directories will be not populated)
764 addItemToProjectTree( name, m_root, &projects, true );
765 }
766
767 haveFile = dir.GetNext( &filename );
768 }
769 }
770 }
771 else
772 {
773 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
774 }
775
776 m_TreeProject->Expand( m_root );
777
778 // Sort filenames by alphabetic order
779 m_TreeProject->SortChildren( m_root );
780
781 CallAfter(
782 [this] ()
783 {
784 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
785 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
786 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
787 } );
788}
789
790
792{
793 if( !m_TreeProject->GetGitRepo() )
794 return false;
795
796 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
797 return statusHandler.HasChangedFiles();
798}
799
800
801void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
802{
803 wxTreeItemId curr_item = Event.GetItem();
804
805 // Ensure item is selected (Under Windows right click does not select the item)
806 m_TreeProject->SelectItem( curr_item );
807
808 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
809 KIGIT_COMMON* git = m_TreeProject->GitCommon();
810
811 bool can_switch_to_project = true;
812 bool can_create_new_directory = true;
813 bool can_open_this_directory = true;
814 bool can_edit = true;
815 bool can_rename = true;
816 bool can_delete = true;
817 bool run_jobs = false;
818
819 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
820 bool vcs_can_commit = hasChangedFiles();
821 bool vcs_can_init = !vcs_has_repo;
822 bool gitIntegrationDisabled = Prj().GetLocalSettings().m_GitIntegrationDisabled;
823 // Disable/Enable is a per-project preference toggle, so it's available whenever we
824 // detected a repository for this project or integration is currently disabled.
825 bool vcs_can_remove = vcs_has_repo || gitIntegrationDisabled;
826 bool vcs_can_fetch = vcs_has_repo && git->HasPushAndPullRemote();
827 bool vcs_can_push = vcs_can_fetch && git->HasLocalCommits();
828 bool vcs_can_pull = vcs_can_fetch;
829 bool vcs_can_switch = vcs_has_repo;
830 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
831
832 // Check if the libgit2 library is available via backend
833 bool libgit_init = GetGitBackend() && GetGitBackend()->IsLibraryAvailable();
834
835 vcs_menu &= libgit_init;
836
837 if( selection.size() == 0 )
838 return;
839
840 // Remove things that don't make sense for multiple selections
841 if( selection.size() != 1 )
842 {
843 can_switch_to_project = false;
844 can_create_new_directory = false;
845 can_rename = false;
846 }
847
848 for( PROJECT_TREE_ITEM* item : selection )
849 {
850 // Check for empty project
851 if( !item )
852 {
853 can_switch_to_project = false;
854 can_edit = false;
855 can_rename = false;
856 continue;
857 }
858
859 can_delete = item->CanDelete();
860 can_rename = item->CanRename();
861
862 switch( item->GetType() )
863 {
866 can_rename = false;
867
868 if( item->GetId() == m_TreeProject->GetRootItem() )
869 {
870 can_switch_to_project = false;
871 }
872 else
873 {
874 can_create_new_directory = false;
875 can_open_this_directory = false;
876 }
877 break;
878
880 can_switch_to_project = false;
881 can_edit = false;
882 break;
883
886 can_edit = false;
887 can_switch_to_project = false;
888 can_create_new_directory = false;
889 can_open_this_directory = false;
890 break;
891
893 run_jobs = true;
894 can_edit = false;
896
900
901 default:
902 can_switch_to_project = false;
903 can_create_new_directory = false;
904 can_open_this_directory = false;
905
906 break;
907 }
908 }
909
910 wxMenu popup_menu;
911 wxString text;
912 wxString help_text;
913
914 if( can_switch_to_project )
915 {
916 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
917 _( "Close all editors, and switch to the selected project" ),
919 popup_menu.AppendSeparator();
920 }
921
922 if( can_create_new_directory )
923 {
924 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
925 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
926 }
927
928 if( can_open_this_directory )
929 {
930 if( selection.size() == 1 )
931 {
932#ifdef __APPLE__
933 text = _( "Reveal in Finder" );
934 help_text = _( "Reveals the directory in a Finder window" );
935#else
936 text = _( "Open Directory in File Explorer" );
937 help_text = _( "Opens the directory in the default system file manager" );
938#endif
939 }
940 else
941 {
942#ifdef __APPLE__
943 text = _( "Reveal in Finder" );
944 help_text = _( "Reveals the directories in a Finder window" );
945#else
946 text = _( "Open Directories in File Explorer" );
947 help_text = _( "Opens the directories in the default system file manager" );
948#endif
949 }
950
951 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
953 }
954
955 if( can_edit )
956 {
957 if( selection.size() == 1 )
958 help_text = _( "Open the file in a Text Editor" );
959 else
960 help_text = _( "Open files in a Text Editor" );
961
962 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
964 }
965
966 if( run_jobs && selection.size() == 1 )
967 {
968 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
970 }
971
972 if( can_rename )
973 {
974 if( selection.size() == 1 )
975 {
976 text = _( "Rename File..." );
977 help_text = _( "Rename file" );
978 }
979 else
980 {
981 text = _( "Rename Files..." );
982 help_text = _( "Rename files" );
983 }
984
985 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
987 }
988
989 if( can_delete )
990 {
991 if( selection.size() == 1 )
992 help_text = _( "Delete the file and its content" );
993 else
994 help_text = _( "Delete the files and their contents" );
995
996 if( can_switch_to_project
997 || can_create_new_directory
998 || can_open_this_directory
999 || can_edit
1000 || can_rename )
1001 {
1002 popup_menu.AppendSeparator();
1003 }
1004
1005#ifdef __WINDOWS__
1006 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
1008#else
1009 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
1011#endif
1012 }
1013
1014 if( vcs_menu )
1015 {
1016 wxMenu* vcs_submenu = new wxMenu();
1017 wxMenu* branch_submenu = new wxMenu();
1018 wxMenuItem* vcs_menuitem = nullptr;
1019
1020 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
1021 _( "Add Project to Version Control..." ),
1022 _( "Initialize a new repository" ) );
1023 vcs_menuitem->Enable( vcs_can_init );
1024
1025
1026 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
1027 _( "Commit changes to the local repository" ) );
1028 vcs_menuitem->Enable( vcs_can_commit );
1029
1030 vcs_menuitem = vcs_submenu->Append( ID_GIT_PUSH, _( "Push" ),
1031 _( "Push committed local changes to remote repository" ) );
1032 vcs_menuitem->Enable( vcs_can_push );
1033
1034 vcs_menuitem = vcs_submenu->Append( ID_GIT_PULL, _( "Pull" ),
1035 _( "Pull changes from remote repository into local" ) );
1036 vcs_menuitem->Enable( vcs_can_pull );
1037
1038 vcs_submenu->AppendSeparator();
1039
1040 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
1041 _( "Commit changes to the local repository" ) );
1042 vcs_menuitem->Enable( vcs_can_commit );
1043
1044 vcs_submenu->AppendSeparator();
1045
1046 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
1047 // _( "Show changes between the repository and working tree" ) );
1048 // vcs_menuitem->Enable( vcs_can_diff );
1049
1050 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
1051
1052 // Skip the first one (that is the current branch)
1053 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
1054 {
1055 wxString msg = _( "Switch to branch " ) + branchNames[ii];
1056 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg );
1057 vcs_menuitem->Enable( vcs_can_switch );
1058 }
1059
1060 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
1061 _( "Switch to a different branch" ) );
1062 vcs_menuitem->Enable( vcs_can_switch );
1063
1064 vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1065
1066 vcs_submenu->AppendSeparator();
1067
1068 if( gitIntegrationDisabled )
1069 {
1070 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Enable Git Integration" ),
1071 _( "Re-enable Git integration for this project" ) );
1072 }
1073 else
1074 {
1075 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Disable Git Integration" ),
1076 _( "Disable Git integration for this project" ) );
1077 }
1078
1079 vcs_menuitem->Enable( vcs_can_remove );
1080
1081 popup_menu.AppendSeparator();
1082 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1083 }
1084
1085 if( popup_menu.GetMenuItemCount() > 0 )
1086 PopupMenu( &popup_menu );
1087}
1088
1089
1091{
1092 wxString editorname = Pgm().GetTextEditor();
1093
1094 if( editorname.IsEmpty() )
1095 {
1096 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1097 return;
1098 }
1099
1100 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1101
1102 for( PROJECT_TREE_ITEM* item_data : tree_data )
1103 {
1104 wxString fullFileName = item_data->GetFileName();
1105
1106 if( !fullFileName.IsEmpty() )
1107 {
1108 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1109 }
1110 }
1111}
1112
1113
1114void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1115{
1116 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1117
1118 for( PROJECT_TREE_ITEM* item_data : tree_data )
1119 item_data->Delete();
1120}
1121
1122
1123void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1124{
1125 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1126 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1127
1128 // XXX: Unnecessary?
1129 if( tree_data.size() != 1 )
1130 return;
1131
1132 wxString buffer = m_TreeProject->GetItemText( curr_item );
1133 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1134 tree_data[0]->GetFileName() );
1135 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1136
1137 if( dlg.ShowModal() != wxID_OK )
1138 return; // canceled by user
1139
1140 buffer = dlg.GetValue();
1141 buffer.Trim( true );
1142 buffer.Trim( false );
1143
1144 if( buffer.IsEmpty() )
1145 return; // empty file name not allowed
1146
1147 tree_data[0]->Rename( buffer, true );
1148 m_isRenaming = true;
1149}
1150
1151
1152void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1153{
1154 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1155
1156 if( tree_data.size() != 1 )
1157 return;
1158
1159 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1160 // there will be more events at least on Windows in this frame that will steal focus from
1161 // any newly launched windows
1162 m_selectedItem = tree_data[0];
1163}
1164
1165
1166void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1167{
1168 // Idle executes once all other events finished processing. This makes it ideal to launch
1169 // a new window without starting Focus wars.
1170 if( m_watcherNeedReset )
1171 {
1172 m_selectedItem = nullptr;
1174 }
1175
1176 if( m_selectedItem != nullptr && m_TreeProject->GetRootItem().IsOk() )
1177 {
1178 // Make sure m_selectedItem still exists in the tree before activating it.
1179 std::vector<wxTreeItemId> validItemIds;
1180 m_TreeProject->GetItemsRecursively( m_TreeProject->GetRootItem(), validItemIds );
1181
1182 for( wxTreeItemId id : validItemIds )
1183 {
1184 if( GetItemIdData( id ) == m_selectedItem )
1185 {
1186 // Activate launches a window which may run the event loop on top of us and cause
1187 // onIdle to get called again, so be sure to null out m_selectedItem first.
1189 m_selectedItem = nullptr;
1190
1191 item->Activate( this );
1192 break;
1193 }
1194 }
1195 }
1196}
1197
1198
1199void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1200{
1201 wxTreeItemId itemId = Event.GetItem();
1202 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1203
1204 if( !tree_data )
1205 return;
1206
1207 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1208 return;
1209
1210 // explore list of non populated subdirs, and populate them
1211 wxTreeItemIdValue cookie;
1212 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1213
1214#ifndef __WINDOWS__
1215 bool subdir_populated = false;
1216#endif
1217
1218 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1219 {
1220 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1221
1222 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1223 continue;
1224
1225 if( itemData->IsPopulated() )
1226 continue;
1227
1228 wxString fileName = itemData->GetFileName();
1229 wxDir dir( fileName );
1230
1231 if( dir.IsOpened() )
1232 {
1233 std::vector<wxString> projects = getProjects( dir );
1234 wxString dir_filename;
1235 bool haveFile = dir.GetFirst( &dir_filename );
1236
1237 while( haveFile )
1238 {
1239 // Add name to tree item, but do not recurse in subdirs:
1240 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1241 addItemToProjectTree( name, kid, &projects, false );
1242
1243 haveFile = dir.GetNext( &dir_filename );
1244 }
1245
1246 itemData->SetPopulated( true ); // set state to populated
1247
1248#ifndef __WINDOWS__
1249 subdir_populated = true;
1250#endif
1251 }
1252
1253 // Sort filenames by alphabetic order
1254 m_TreeProject->SortChildren( kid );
1255 }
1256
1257#ifndef __WINDOWS__
1258 if( subdir_populated )
1259 m_watcherNeedReset = true;
1260#endif
1261}
1262
1263
1264std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1265{
1266 wxArrayTreeItemIds selection;
1267 std::vector<PROJECT_TREE_ITEM*> data;
1268
1269 m_TreeProject->GetSelections( selection );
1270
1271 for( const wxTreeItemId itemId : selection )
1272 {
1273 PROJECT_TREE_ITEM* item = GetItemIdData( itemId );
1274
1275 if( !item )
1276 {
1277 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast failed?" ) );
1278 continue;
1279 }
1280
1281 data.push_back( item );
1282 }
1283
1284 return data;
1285}
1286
1287
1289{
1290 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1291}
1292
1293
1294wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1295{
1296 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1297
1298 // If the subdir is the current working directory, return m_root
1299 // in main list:
1300 if( prj_dir == aSubDir )
1301 return m_root;
1302
1303 // The subdir is in the main tree or in a subdir: Locate it
1304 wxTreeItemIdValue cookie;
1305 wxTreeItemId root_id = m_root;
1306 std::stack<wxTreeItemId> subdirs_id;
1307
1308 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1309
1310 while( true )
1311 {
1312 if( ! child.IsOk() )
1313 {
1314 if( subdirs_id.empty() ) // all items were explored
1315 {
1316 root_id = child; // Not found: return an invalid wxTreeItemId
1317 break;
1318 }
1319 else
1320 {
1321 root_id = subdirs_id.top();
1322 subdirs_id.pop();
1323 child = m_TreeProject->GetFirstChild( root_id, cookie );
1324
1325 if( !child.IsOk() )
1326 continue;
1327 }
1328 }
1329
1330 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1331
1332 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1333 {
1334 if( itemData->GetFileName() == aSubDir ) // Found!
1335 {
1336 root_id = child;
1337 break;
1338 }
1339
1340 // child is a subdir, push in list to explore it later
1341 if( itemData->IsPopulated() )
1342 subdirs_id.push( child );
1343 }
1344
1345 child = m_TreeProject->GetNextChild( root_id, cookie );
1346 }
1347
1348 return root_id;
1349}
1350
1351
1352void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1353{
1354 // No need to process events when we're shutting down
1355 if( !m_watcher )
1356 return;
1357
1358 // Ignore events that are not file creation, deletion, renaming or modification because
1359 // they are not relevant to the project tree.
1360 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1361 wxFSW_EVENT_DELETE |
1362 wxFSW_EVENT_RENAME |
1363 wxFSW_EVENT_MODIFY ) ) )
1364 {
1365 return;
1366 }
1367
1368 const wxFileName& pathModified = event.GetPath();
1369
1370 // Ignore events from .history directory (local backup)
1371 if( pathModified.GetFullPath().Contains( wxS( ".history" ) ) )
1372 return;
1373
1374 wxString subdir = pathModified.GetPath();
1375 wxString fn = pathModified.GetFullPath();
1376
1377 // Adjust directories to look like a file item (path and name).
1378 if( pathModified.GetFullName().IsEmpty() )
1379 {
1380 subdir = subdir.BeforeLast( '/' );
1381 fn = fn.BeforeLast( '/' );
1382 }
1383
1384 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1385
1386 if( !root_id.IsOk() )
1387 return;
1388
1389 CallAfter( [this] ()
1390 {
1391 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1392 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1393 } );
1394
1395 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1396 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1397
1398 switch( event.GetChangeType() )
1399 {
1400 case wxFSW_EVENT_CREATE:
1401 {
1402 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1403
1404 // If we are in the process of renaming a file, select the new one
1405 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1406 // pair of DELETE and CREATE events.
1407 if( m_isRenaming && newitem.IsOk() )
1408 {
1409 m_TreeProject->SelectItem( newitem );
1410 m_isRenaming = false;
1411 }
1412 }
1413 break;
1414
1415 case wxFSW_EVENT_DELETE:
1416 while( kid.IsOk() )
1417 {
1418 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1419
1420 if( itemData && itemData->GetFileName() == fn )
1421 {
1422 m_TreeProject->Delete( kid );
1423 return;
1424 }
1425 kid = m_TreeProject->GetNextChild( root_id, cookie );
1426 }
1427 break;
1428
1429 case wxFSW_EVENT_RENAME :
1430 {
1431 const wxFileName& newpath = event.GetNewPath();
1432 wxString newdir = newpath.GetPath();
1433 wxString newfn = newpath.GetFullPath();
1434
1435 while( kid.IsOk() )
1436 {
1437 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1438
1439 if( itemData && itemData->GetFileName() == fn )
1440 {
1441 m_TreeProject->Delete( kid );
1442 break;
1443 }
1444
1445 kid = m_TreeProject->GetNextChild( root_id, cookie );
1446 }
1447
1448 // Add the new item only if it is not the current project file (root item).
1449 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1450 // called after an actual file rename, and the cleanup code does not explore the
1451 // root item, because it cannot be renamed by the user. Also, ensure the new file
1452 // actually exists on the file system before it is readded. On Linux, moving a file
1453 // to the trash can cause the same path to be returned in both the old and new paths
1454 // of the event, even though the file isn't there anymore.
1455 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1456
1457 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1458 {
1459 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1460 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1461
1462 // If the item exists, select it
1463 if( newitem.IsOk() )
1464 m_TreeProject->SelectItem( newitem );
1465 }
1466
1467 m_isRenaming = false;
1468 }
1469 break;
1470
1471 default:
1472 return;
1473 }
1474
1475 // Sort filenames by alphabetic order
1476 m_TreeProject->SortChildren( root_id );
1477}
1478
1479
1481{
1482 m_watcherNeedReset = false;
1483
1484 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1485
1486#if defined( _WIN32 )
1487 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1488
1489 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1490 {
1491 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1492 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1493 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1494 // Windows Server shares :/
1495 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1496 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1497 return;
1498 }
1499 else
1500 {
1501 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1502 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1503 }
1504#endif
1505
1506 // Prepare file watcher:
1507 if( m_watcher )
1508 {
1509 m_watcher->RemoveAll();
1510 }
1511 else
1512 {
1513 // Create a wxWidgets log handler to catch errors during watcher creation
1514 // We need to to this because we cannot get error codes from the wxFileSystemWatcher
1515 // constructor. On Linux, if inotify cannot be initialized (usually due to resource limits),
1516 // wxWidgets will throw a system error and then, we throw another error below, trying to
1517 // add paths to a null watcher. We skip this by installing a temporary log handler that
1518 // catches errors during watcher creation and aborts if any error is detected.
1519 class WatcherLogHandler : public wxLog
1520 {
1521 public:
1522 explicit WatcherLogHandler( bool* err ) :
1523 m_err( err )
1524 {
1525 if( m_err )
1526 *m_err = false;
1527 }
1528
1529 protected:
1530 void DoLogTextAtLevel( wxLogLevel level, const wxString& text ) override
1531 {
1532 if( m_err && ( level == wxLOG_Error || level == wxLOG_FatalError ) )
1533 *m_err = true;
1534 }
1535
1536 private:
1537 bool* m_err;
1538 };
1539
1540 bool watcherHasError = false;
1541 WatcherLogHandler tmpLog( &watcherHasError );
1542 wxLog* oldLog = wxLog::SetActiveTarget( &tmpLog );
1543
1545 m_watcher->SetOwner( this );
1546
1547 // Restore previous log handler
1548 wxLog::SetActiveTarget( oldLog );
1549
1550 if( watcherHasError )
1551 {
1552 return;
1553 }
1554 }
1555
1556 // We can see wxString under a debugger, not a wxFileName
1557 wxFileName fn;
1558 fn.AssignDir( prj_dir );
1559 fn.DontFollowLink();
1560
1561 // Add directories which should be monitored.
1562 // under windows, we add the curr dir and all subdirs
1563 // under unix, we add only the curr dir and the populated subdirs
1564 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1565 // under unix, the file watcher needs more work to be efficient
1566 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1567 {
1568 wxLogNull logNo; // avoid log messages
1569#ifdef __WINDOWS__
1570 if( ! m_watcher->AddTree( fn ) )
1571 {
1572 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1573 TO_UTF8( fn.GetFullPath() ) );
1574 return;
1575 }
1576 }
1577#else
1578 if( !m_watcher->Add( fn ) )
1579 {
1580 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1581 TO_UTF8( fn.GetFullPath() ) );
1582 return;
1583 }
1584 }
1585
1586 if( m_TreeProject->IsEmpty() )
1587 return;
1588
1589 // Add subdirs
1590 wxTreeItemIdValue cookie;
1591 wxTreeItemId root_id = m_root;
1592
1593 std::stack < wxTreeItemId > subdirs_id;
1594
1595 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1596 int total_watch_count = 0;
1597
1598 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1599 {
1600 if( !kid.IsOk() )
1601 {
1602 if( subdirs_id.empty() ) // all items were explored
1603 {
1604 break;
1605 }
1606 else
1607 {
1608 root_id = subdirs_id.top();
1609 subdirs_id.pop();
1610 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1611
1612 if( !kid.IsOk() )
1613 continue;
1614 }
1615 }
1616
1617 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1618
1619 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1620 {
1621 // we can see wxString under a debugger, not a wxFileName
1622 const wxString& path = itemData->GetFileName();
1623
1624 // Skip .history directory and its descendants (local backup)
1625 if( path.Contains( wxS( ".history" ) ) )
1626 {
1627 kid = m_TreeProject->GetNextChild( root_id, cookie );
1628 continue;
1629 }
1630
1631 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1632
1633 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1634 {
1635 {
1636 // Silence OS errors that come from the watcher
1637 wxLogNull silence;
1638 fn.AssignDir( path );
1639 m_watcher->Add( fn );
1640 total_watch_count++;
1641 }
1642
1643 // if kid is a subdir, push in list to explore it later
1644 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1645 subdirs_id.push( kid );
1646 }
1647 }
1648
1649 kid = m_TreeProject->GetNextChild( root_id, cookie );
1650 }
1651
1652 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1653 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1654#endif
1655
1656#if defined(DEBUG) && 1
1657 wxArrayString paths;
1658 m_watcher->GetWatchedPaths( &paths );
1659 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1660
1661 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1662 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1663#endif
1664 }
1665
1666
1668{
1669 // Make sure we don't try to inspect the tree after we've deleted its items.
1671
1672 m_TreeProject->DeleteAllItems();
1673
1674 if( m_TreeProject->GetGitRepo() )
1675 {
1676 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1677 common->SetCancelled( true );
1678
1679 std::unique_lock<std::mutex> lock( common->m_gitActionMutex, std::try_to_lock );
1680
1681 constexpr auto kGraceMs = std::chrono::seconds( 2 );
1682 auto graceEnd = std::chrono::steady_clock::now() + kGraceMs;
1683
1684 while( !lock.owns_lock() && std::chrono::steady_clock::now() < graceEnd )
1685 {
1686 if( lock.try_lock() )
1687 break;
1688 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1689 }
1690
1691 constexpr auto kCheckInterval = std::chrono::seconds( 30 );
1692 bool userAbandoned = false;
1693
1694 while( !lock.owns_lock() && !userAbandoned )
1695 {
1696 auto intervalEnd = std::chrono::steady_clock::now() + kCheckInterval;
1697
1698 {
1699 wxProgressDialog progress( _( "Please wait" ),
1700 _( "Closing project..." ),
1701 100, this,
1702 wxPD_APP_MODAL | wxPD_SMOOTH );
1703
1704 while( !lock.try_lock()
1705 && std::chrono::steady_clock::now() < intervalEnd )
1706 {
1707 progress.Pulse();
1708 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1709 wxYield();
1710 }
1711 }
1712
1713 if( lock.owns_lock() )
1714 break;
1715
1716 wxMessageDialog ask( this,
1717 _( "A Git operation is still running.\n"
1718 "Keep waiting, or abandon?" ),
1719 _( "Git Operation Delayed" ),
1720 wxYES_NO | wxICON_QUESTION );
1721 ask.SetYesNoLabels( _( "Keep Waiting" ), _( "Abandon" ) );
1722
1723 if( ask.ShowModal() == wxID_NO )
1724 userAbandoned = true;
1725 }
1726
1727 if( userAbandoned )
1728 {
1729 git_repository* orphan = m_TreeProject->GetGitRepo();
1730 std::unique_ptr<KIGIT_COMMON> oldCommon = m_TreeProject->TakeGitCommon();
1731 m_TreeProject->SetGitRepo( nullptr );
1732
1733 // Register the cleanup thread with the orphan registry so the
1734 // shutdown path can wait for it before tearing down libgit2.
1735 // Cancellation has already been requested via SetCancelled() above,
1736 // and progress_cb/transfer_progress_cb honour it so long-running
1737 // fetches will exit with GIT_EUSER as soon as they hit a callback.
1738
1739 GIT_BACKEND* backend = GetGitBackend();
1740 wxString projectDir = oldCommon ? oldCommon->GetProjectDir() : wxString();
1741
1742 auto cleanup = [orphan, old = std::move( oldCommon )]() mutable
1743 {
1744 std::lock_guard<std::mutex> g( old->m_gitActionMutex );
1745 git_repository_free( orphan );
1746 };
1747
1748 bool registered = false;
1749
1750 if( backend )
1751 {
1752 std::string label = "abandon close " + projectDir.ToStdString();
1753 registered = backend->OrphanRegistry().Register( label, std::move( cleanup ) );
1754 }
1755
1756 if( !registered )
1757 {
1758 // Either no backend is available, or the registry has already
1759 // entered shutdown. Fall back to freeing synchronously so the
1760 // repository handle is not leaked; this blocks on the git
1761 // action mutex but is safe because libgit2 shutdown is gated
1762 // on the same registry.
1763
1764 cleanup();
1765 }
1766 }
1767 else
1768 {
1769 git_repository* repo = m_TreeProject->GetGitRepo();
1771 m_TreeProject->SetGitRepo( nullptr );
1772 }
1773 }
1774}
1775
1776
1777void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1778{
1780 m_TreeProject->LoadIcons();
1781 m_TreeProject->Refresh();
1782
1783 aEvent.Skip();
1784}
1785
1786
1787void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1788{
1789 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1790 wxPaintDC dc( this );
1791
1792 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1793 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1794
1795 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1796 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1797}
1798
1799
1800void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1801{
1802 m_projectTreePane->FileWatcherReset();
1803}
1804
1805
1806// Prompts for git credentials after an auth failure. Returns false if cancelled.
1807static bool promptForGitCredentials( wxWindow* aParent, KIGIT_COMMON* aCommon )
1808{
1809 DIALOG_GIT_CREDENTIALS dlg( aParent, aCommon->GetRemote(), aCommon->GetConnType(), aCommon->GetUsername(),
1810 wxEmptyString );
1811
1812 if( dlg.ShowModal() != wxID_OK )
1813 return false;
1814
1815 aCommon->SetUsername( dlg.GetUsername() );
1816 aCommon->SetPassword( dlg.GetPassword() );
1817
1818 if( dlg.GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH && !dlg.GetSSHKey().IsEmpty() )
1819 {
1820 aCommon->SetSSHKey( dlg.GetSSHKey() );
1821 }
1822
1823 if( dlg.SaveCredentials() )
1824 {
1825 KIPLATFORM::SECRETS::StoreSecret( aCommon->GetRemote(), aCommon->GetUsername(), dlg.GetPassword() );
1826 }
1827
1828 aCommon->ClearAuthFailure();
1829 aCommon->TestedTypes() = 0;
1830 aCommon->ResetNextKey();
1831 return true;
1832}
1833
1834
1835void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1836{
1837 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1838
1839 wxString dir = tree_data->GetDir();
1840
1841 if( dir.empty() )
1842 {
1843 wxLogError( "Failed to initialize git project: project directory is empty." );
1844 return;
1845 }
1846
1847 GIT_INIT_HANDLER initHandler( m_TreeProject->GitCommon() );
1848 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1849
1850 if( initHandler.IsRepository( dir ) )
1851 {
1852 DisplayInfoMessage( topLevelParent,
1853 _( "The selected directory is already a Git project." ) );
1854 return;
1855 }
1856
1857 InitResult result = initHandler.InitializeRepository( dir );
1859 {
1860 DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ),
1861 initHandler.GetErrorString() );
1862 return;
1863 }
1864
1865 m_gitLastError = GIT_ERROR_NONE;
1866
1867 m_TreeProject->GitCommon()->SetCancelled( false );
1868
1869 const char* canonicalWorkDir = git_repository_workdir( initHandler.GetRepo() );
1870
1871 if( canonicalWorkDir )
1872 {
1874 dir, wxString::FromUTF8( canonicalWorkDir ) );
1875 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
1876 }
1877
1878 DIALOG_GIT_REPOSITORY dlg( topLevelParent, initHandler.GetRepo() );
1879 dlg.SetTitle( _( "Set default remote" ) );
1880 dlg.SetSkipButtonLabel( _( "Skip" ) );
1881
1882 if( dlg.ShowModal() != wxID_OK )
1883 return;
1884
1885 // Set up the remote
1886 RemoteConfig remoteConfig;
1887 remoteConfig.url = dlg.GetRepoURL();
1888 remoteConfig.username = dlg.GetUsername();
1889 remoteConfig.password = dlg.GetPassword();
1890 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1891 remoteConfig.connType = dlg.GetRepoType();
1892
1893 if( !initHandler.SetupRemote( remoteConfig ) )
1894 {
1895 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1896 initHandler.GetErrorString() );
1897 return;
1898 }
1899
1900 m_gitLastError = GIT_ERROR_NONE;
1901
1902 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1903
1904 while( true )
1905 {
1906 common->ClearAuthFailure();
1907
1908 GIT_PULL_HANDLER handler( common );
1909 handler.SetProgressReporter(
1910 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1911
1912 if( handler.PerformFetch() )
1913 break;
1914
1915 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
1916 continue;
1917
1918 break;
1919 }
1920
1924
1928 Prj().GetLocalSettings().m_GitRepoType = "https";
1929 else
1930 Prj().GetLocalSettings().m_GitRepoType = "local";
1931}
1932
1933
1934void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
1935{
1936
1937}
1938
1939
1940void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
1941{
1942 git_repository* repo = m_TreeProject->GetGitRepo();
1943
1944 if( !repo )
1945 return;
1946
1947 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1948
1949 while( true )
1950 {
1951 common->ClearAuthFailure();
1952
1953 GIT_PULL_HANDLER handler( common );
1954 handler.SetProgressReporter(
1955 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1956
1957 if( handler.PerformPull() >= PullResult::Success )
1958 break;
1959
1960 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
1961 continue;
1962
1963 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), handler.GetErrorString() );
1964 break;
1965 }
1966
1967 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1968}
1969
1970
1971void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
1972{
1973 git_repository* repo = m_TreeProject->GetGitRepo();
1974
1975 if( !repo )
1976 return;
1977
1978 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1979
1980 while( true )
1981 {
1982 common->ClearAuthFailure();
1983
1984 GIT_PUSH_HANDLER handler( common );
1985 handler.SetProgressReporter(
1986 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1987
1988 if( handler.PerformPush() == PushResult::Success )
1989 break;
1990
1991 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
1992 continue;
1993
1994 DisplayErrorMessage( m_parent, _( "Failed to push project" ), handler.GetErrorString() );
1995 break;
1996 }
1997
1998 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1999}
2000
2001
2002void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
2003{
2004 if( !m_TreeProject->GetGitRepo() )
2005 return;
2006
2007 GIT_BRANCH_HANDLER branchHandler( m_TreeProject->GitCommon() );
2008 wxString branchName;
2009
2010 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
2011 {
2012 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), m_TreeProject->GetGitRepo() );
2013
2014 int retval = dlg.ShowModal();
2015 branchName = dlg.GetBranchName();
2016
2017 if( retval == wxID_ADD )
2018 KIGIT::PROJECT_GIT_UTILS::CreateBranch( m_TreeProject->GetGitRepo(), branchName );
2019 else if( retval != wxID_OK )
2020 return;
2021 }
2022 else
2023 {
2024 std::vector<wxString> branches = m_TreeProject->GitCommon()->GetBranchNames();
2025 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_BRANCH;
2026
2027 if( branchIndex < 0 || static_cast<size_t>( branchIndex ) >= branches.size() )
2028 return;
2029
2030 branchName = branches[branchIndex];
2031 }
2032
2033 wxLogTrace( traceGit, wxS( "onGitSwitchBranch: Switching to branch '%s'" ), branchName );
2034 if( branchHandler.SwitchToBranch( branchName ) != BranchResult::Success )
2035 {
2036 DisplayError( m_parent, branchHandler.GetErrorString() );
2037 }
2038}
2039
2040
2041void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
2042{
2043 PROJECT_LOCAL_SETTINGS& localSettings = Prj().GetLocalSettings();
2044
2045 // Toggle the Git integration disabled preference
2046 localSettings.m_GitIntegrationDisabled = !localSettings.m_GitIntegrationDisabled;
2047
2048 wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Git integration %s" ),
2049 localSettings.m_GitIntegrationDisabled ? wxS( "disabled" ) : wxS( "enabled" ) );
2050
2051 if( localSettings.m_GitIntegrationDisabled )
2052 {
2053 // Disabling Git integration - clear the repo reference and item states
2054 m_TreeProject->SetGitRepo( nullptr );
2055 m_gitIconsInitialized = false;
2056
2057 // Clear all item states to remove git status icons
2058 std::stack<wxTreeItemId> items;
2059 items.push( m_TreeProject->GetRootItem() );
2060
2061 while( !items.empty() )
2062 {
2063 wxTreeItemId current = items.top();
2064 items.pop();
2065
2066 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
2067
2068 wxTreeItemIdValue cookie;
2069 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2070
2071 while( child.IsOk() )
2072 {
2073 items.push( child );
2074 child = m_TreeProject->GetNextChild( current, cookie );
2075 }
2076 }
2077 }
2078 else
2079 {
2080 // Re-enabling Git integration - try to find and connect to the repository
2081 wxFileName fn( Prj().GetProjectPath() );
2082 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
2083
2084 if( m_TreeProject->GetGitRepo() )
2085 {
2086 m_TreeProject->GitCommon()->SetCancelled( false );
2087
2088 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
2089
2090 if( canonicalWorkDir )
2091 {
2093 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
2094 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
2095 }
2096
2097 m_TreeProject->GitCommon()->SetUsername( localSettings.m_GitRepoUsername );
2098 m_TreeProject->GitCommon()->SetSSHKey( localSettings.m_GitSSHKey );
2099 }
2100 }
2101
2102 // Save the preference to the project local settings file
2103 localSettings.SaveToFile( Prj().GetProjectPath() );
2104}
2105
2106
2108{
2109 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
2110 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
2111
2112 if( !lock.owns_lock() )
2113 {
2114 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
2115 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2116 return;
2117 }
2118
2119 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2120 {
2121 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
2122 return;
2123 }
2124
2125 std::stack<wxTreeItemId> items;
2126 items.push(m_TreeProject->GetRootItem());
2127
2128 while( !items.empty() )
2129 {
2130 wxTreeItemId current = items.top();
2131 items.pop();
2132
2133 if( m_TreeProject->ItemHasChildren( current ) )
2134 {
2135 wxTreeItemIdValue cookie;
2136 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2137
2138 while( child.IsOk() )
2139 {
2140 items.push( child );
2141
2142 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
2143 {
2144 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
2145 }
2146
2147 child = m_TreeProject->GetNextChild( current, cookie );
2148 }
2149 }
2150 }
2151
2152 if( !m_gitCurrentBranchName.empty() )
2153 {
2154 wxTreeItemId kid = m_TreeProject->GetRootItem();
2155 PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid );
2156
2157 if( rootItem )
2158 {
2159 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
2160 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
2161 m_gitIconsInitialized = true;
2162 }
2163 }
2164
2165 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
2166}
2167
2168
2170{
2171 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
2172
2173 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
2174
2175 if( !lock.owns_lock() )
2176 {
2177 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
2178 return;
2179 }
2180
2181 if( !m_TreeProject )
2182 {
2183 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
2184 return;
2185 }
2186
2187 wxTreeItemId kid = m_TreeProject->GetRootItem();
2188
2189 if( !kid.IsOk() )
2190 return;
2191
2192 // Collect a map to easily set the state of each item
2193 m_gitTreeCache.clear();
2194 std::stack<wxTreeItemId> items;
2195 items.push( kid );
2196
2197 while( !items.empty() )
2198 {
2199 kid = items.top();
2200 items.pop();
2201
2202 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
2203
2204 if( !nextItem )
2205 continue;
2206
2207 wxString gitAbsPath = nextItem->GetFileName();
2208#ifdef _WIN32
2209 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2210#endif
2211 m_gitTreeCache[gitAbsPath] = kid;
2212
2213 wxTreeItemIdValue cookie;
2214 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2215
2216 while( child.IsOk() )
2217 {
2218 items.push( child );
2219 child = m_TreeProject->GetNextChild( kid, cookie );
2220 }
2221 }
2222}
2223
2224
2226{
2227 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2228#if defined( _WIN32 )
2230
2231 if( refresh != 0
2232 && KIPLATFORM::ENV::IsNetworkPath( wxPathOnly( m_Parent->GetProjectFileName() ) ) )
2233 {
2234 // Need to treat windows network paths special here until we get the samba bug fixed
2235 // https://github.com/wxWidgets/wxWidgets/issues/18953
2236 CallAfter(
2237 [this, refresh]()
2238 {
2239 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2240 } );
2241 }
2242
2243#endif
2244
2245 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2246 return;
2247
2248 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2249 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2250
2251 if( !lock1.owns_lock() || !lock2.owns_lock() )
2252 {
2253 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2254 return;
2255 }
2256
2257 if( !m_TreeProject->GetGitRepo() )
2258 {
2259 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2260 return;
2261 }
2262
2263 // Acquire the git action mutex to synchronize with EmptyTreePrj() shutdown.
2264 // This ensures the repository isn't freed while we're using it.
2265 std::unique_lock<std::mutex> gitLock( m_TreeProject->GitCommon()->m_gitActionMutex, std::try_to_lock );
2266
2267 if( !gitLock.owns_lock() )
2268 {
2269 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire git action mutex" ) );
2270 return;
2271 }
2272
2273 // Check if cancellation was requested (e.g., during shutdown)
2274 if( m_TreeProject->GitCommon()->IsCancelled() )
2275 {
2276 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Cancelled" ) );
2277 return;
2278 }
2279
2280 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2281
2282 // Set up pathspec for project files
2283 wxFileName rootFilename( Prj().GetProjectFullName() );
2284 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2285
2286 wxFileName relative = rootFilename;
2287 relative.MakeRelativeTo( repoWorkDir );
2288 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2289
2290#ifdef _WIN32
2291 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2292#endif
2293
2294 // Get file status
2295 auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr );
2296 auto [localChanges, remoteChanges] = m_TreeProject->GitCommon()->GetDifferentFiles();
2297 statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap );
2298
2299 bool updated = false;
2300
2301 // Update status icons based on file status
2302 for( const auto& [absPath, fileStatus] : fileStatusMap )
2303 {
2304 auto iter = m_gitTreeCache.find( absPath );
2305 if( iter == m_gitTreeCache.end() )
2306 {
2307 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2308 continue;
2309 }
2310
2311 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status );
2312 if( inserted || it->second != fileStatus.status )
2313 updated = true;
2314 it->second = fileStatus.status;
2315 }
2316
2317 // Get the current branch name
2319
2320 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2321
2322 // Update UI if icons changed
2323 if( updated || !m_gitIconsInitialized )
2324 {
2325 CallAfter(
2326 [this]()
2327 {
2329 } );
2330 }
2331}
2332
2333
2334void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2335{
2336 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2337
2338 git_repository* repo = m_TreeProject->GetGitRepo();
2339
2340 if( repo == nullptr )
2341 {
2342 wxMessageBox( _( "The selected directory is not a Git project." ) );
2343 return;
2344 }
2345
2346 // Get git configuration
2347 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2348 GitUserConfig userConfig = configHandler.GetUserConfig();
2349
2350 // Collect modified files in the repository
2351 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2352 auto fileStatusMap = statusHandler.GetFileStatus();
2353
2354 std::map<wxString, int> modifiedFiles;
2355 std::set<wxString> selected_files;
2356
2357 for( PROJECT_TREE_ITEM* item : tree_data )
2358 {
2359 if( item->GetType() == TREE_FILE_TYPE::DIRECTORY )
2360 continue;
2361
2362 wxString itemPath = item->GetFileName();
2363#ifdef _WIN32
2364 itemPath.Replace( wxS( "\\" ), wxS( "/" ) );
2365#endif
2366 selected_files.emplace( itemPath );
2367 }
2368
2369 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2370
2371 wxString projectPath = Prj().GetProjectPath();
2372#ifdef _WIN32
2373 projectPath.Replace( wxS( "\\" ), wxS( "/" ) );
2374#endif
2375
2376 for( const auto& [absPath, fileStatus] : fileStatusMap )
2377 {
2378 // Skip current, conflicted, or ignored files
2379 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2381 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2382 {
2383 continue;
2384 }
2385
2386 wxFileName fn( absPath );
2387
2388 // Convert to relative path for the modifiedFiles map
2389 wxString relativePath = absPath;
2390 if( relativePath.StartsWith( repoWorkDir ) )
2391 {
2392 relativePath = relativePath.Mid( repoWorkDir.length() );
2393#ifdef _WIN32
2394 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2395#endif
2396 }
2397
2398 // Do not commit files outside the project directory
2399 if( !absPath.StartsWith( projectPath ) )
2400 continue;
2401
2402 // Skip lock files
2403 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2404 continue;
2405
2406 // Skip autosave, lock, and backup files
2407 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix )
2408 || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2409 {
2410 continue;
2411 }
2412
2413 // Skip archived project backups
2414 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2415 continue;
2416
2417 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2418 {
2419 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2420 }
2421 else if( selected_files.count( absPath ) )
2422 {
2423 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2424 }
2425 }
2426
2427 // Create a commit dialog
2428 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
2429 modifiedFiles );
2430 auto ret = dlg.ShowModal();
2431
2432 if( ret != wxID_OK )
2433 return;
2434
2435 std::vector<wxString> files = dlg.GetSelectedFiles();
2436
2437 if( dlg.GetCommitMessage().IsEmpty() )
2438 {
2439 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2440 return;
2441 }
2442
2443 if( files.empty() )
2444 {
2445 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2446 return;
2447 }
2448
2449 GIT_COMMIT_HANDLER commitHandler( repo );
2450 auto result = commitHandler.PerformCommit( files, dlg.GetCommitMessage(),
2451 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
2452
2454 {
2455 wxMessageBox( wxString::Format( _( "Failed to create commit: %s" ),
2456 commitHandler.GetErrorString() ) );
2457 return;
2458 }
2459
2460 wxLogTrace( traceGit, wxS( "Created commit" ) );
2461 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2462}
2463
2464
2465void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
2466{
2467
2468}
2469
2470
2471bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
2472{
2473 if( !m_TreeProject->GetGitRepo() )
2474 return false;
2475
2476 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2477 auto fileStatusMap = statusHandler.GetFileStatus();
2478
2479 // Check if file is already tracked or staged
2480 for( const auto& [filePath, fileStatus] : fileStatusMap )
2481 {
2482 if( filePath.EndsWith( aFile ) || filePath == aFile )
2483 {
2484 // File can be added if it's untracked
2485 return fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED;
2486 }
2487 }
2488
2489 // If file not found in status, it might be addable
2490 return true;
2491}
2492
2493
2494void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
2495{
2496 wxLogTrace( traceGit, "Syncing project" );
2497 git_repository* repo = m_TreeProject->GetGitRepo();
2498
2499 if( !repo )
2500 {
2501 wxLogTrace( traceGit, "sync: No git repository found" );
2502 return;
2503 }
2504
2505 GIT_SYNC_HANDLER handler( repo );
2506 handler.PerformSync();
2507}
2508
2509
2510void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
2511{
2512 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2513
2514 if( !gitCommon )
2515 return;
2516
2517 while( true )
2518 {
2519 gitCommon->ClearAuthFailure();
2520
2521 GIT_PULL_HANDLER handler( gitCommon );
2522
2523 if( handler.PerformFetch() )
2524 break;
2525
2526 if( gitCommon->WasAuthFailure() && promptForGitCredentials( m_parent, gitCommon ) )
2527 continue;
2528
2529 break;
2530 }
2531
2532 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2533}
2534
2535
2536void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
2537{
2538 git_repository* repo = m_TreeProject->GetGitRepo();
2539
2540 if( !repo )
2541 return;
2542
2543 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
2544 handler.PerformResolveConflict();
2545}
2546
2547
2548void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
2549{
2550 git_repository* repo = m_TreeProject->GetGitRepo();
2551
2552 if( !repo )
2553 return;
2554
2555 GIT_REVERT_HANDLER handler( repo );
2556 handler.PerformRevert();
2557}
2558
2559
2560void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
2561{
2562 git_repository* repo = m_TreeProject->GetGitRepo();
2563
2564 if( !repo )
2565 return;
2566
2567 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
2568 handler.PerformRemoveFromIndex();
2569}
2570
2571
2573{
2574
2575}
2576
2577
2578void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
2579{
2580 wxLogTrace( traceGit, "onGitSyncTimer" );
2581 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
2582
2583 if( !gitSettings.enableGit || !m_TreeProject )
2584 return;
2585
2587
2588 m_gitSyncTask = tp.submit_task( [this]()
2589 {
2590 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2591
2592 if( !gitCommon )
2593 {
2594 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
2595 return;
2596 }
2597
2598 // Check if cancellation was requested (e.g., during shutdown)
2599 if( gitCommon->IsCancelled() )
2600 {
2601 wxLogTrace( traceGit, "onGitSyncTimer: Cancelled" );
2602 return;
2603 }
2604
2605 GIT_PULL_HANDLER handler( gitCommon );
2606 handler.PerformFetch();
2607
2608 // Only schedule the follow-up work if not cancelled
2609 if( !gitCommon->IsCancelled() )
2610 CallAfter( [this]() { gitStatusTimerHandler(); } );
2611 } );
2612
2613 if( gitSettings.updatInterval > 0 )
2614 {
2615 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
2616 // We store the timer interval in minutes but wxTimer uses milliseconds
2617 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
2618 }
2619}
2620
2621
2623{
2624 // Check if git is still available and not cancelled before spawning background work
2625 KIGIT_COMMON* gitCommon = m_TreeProject ? m_TreeProject->GitCommon() : nullptr;
2626
2627 if( !gitCommon || gitCommon->IsCancelled() )
2628 return;
2629
2632
2633 m_gitStatusIconTask = tp.submit_task( [this]() { updateGitStatusIconMap(); } );
2634}
2635
2636void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
2637{
2638 wxLogTrace( traceGit, "onGitStatusTimer" );
2639
2640 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2641 return;
2642
2644}
const char * name
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
@ directory_browser
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 GetConnType() const
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
wxString GetRepoSSHPath() const
void SetSkipButtonLabel(const wxString &aLabel)
wxString GetBranchName() const
int ShowModal() override
virtual bool IsLibraryAvailable()=0
KIGIT_ORPHAN_REGISTRY & OrphanRegistry()
Return the process-wide orphan thread registry owned by this backend.
Definition git_backend.h:77
BranchResult SwitchToBranch(const wxString &aBranchName)
Switch to the specified branch.
CommitResult PerformCommit(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
wxString GetErrorString() const
GitUserConfig GetUserConfig()
Get user configuration (name and email) from git config Falls back to common settings if not found in...
bool IsRepository(const wxString &aPath)
Check if a directory is already a git repository.
InitResult InitializeRepository(const wxString &aPath)
Initialize a new git repository in the specified directory.
bool SetupRemote(const RemoteConfig &aConfig)
Set up a remote for the repository.
void SetProgressReporter(std::unique_ptr< WX_PROGRESS_REPORTER > aProgressReporter)
bool PerformFetch(bool aSkipLock=false)
PullResult PerformPull()
PushResult PerformPush()
wxString GetCurrentBranchName()
Get the current branch name.
void UpdateRemoteStatus(const std::set< wxString > &aLocalChanges, const std::set< wxString > &aRemoteChanges, std::map< wxString, FileStatus > &aFileStatus)
Get status for modified files based on local/remote changes.
wxString GetWorkingDirectory()
Get the repository working directory path.
bool HasChangedFiles()
Check if the repository has any changed files.
std::map< wxString, FileStatus > GetFileStatus(const wxString &aPathspec=wxEmptyString)
Get detailed file status for all files in the specified path.
The main KiCad project manager frame.
void OnChangeWatchedPaths(wxCommandEvent &aEvent)
Called by sending a event with id = ID_INIT_WATCHED_PATHS rebuild the list of watched paths.
PROJECT_TREE_PANE * m_projectTreePane
static git_repository * GetRepositoryForFile(const char *aFilename)
Discover and open the repository that contains the given file.
static wxString ComputeSymlinkPreservingWorkDir(const wxString &aUserProjectPath, const wxString &aCanonicalWorkDir)
Compute a working directory path that preserves symlinks from the user's project path.
static bool RemoveVCS(git_repository *&aRepo, const wxString &aProjectPath=wxEmptyString, bool aRemoveGitDir=false, wxString *aErrors=nullptr)
Remove version control from a directory by freeing the repository and optionally removing the ....
static int CreateBranch(git_repository *aRepo, const wxString &aBranchName)
Create a new branch based on HEAD.
std::mutex m_gitActionMutex
const wxString & GetRemote() const
GIT_CONN_TYPE GetConnType() const
bool WasAuthFailure() const
void SetSSHKey(const wxString &aSSHKey)
bool IsCancelled() const
void SetUsername(const wxString &aUsername)
bool HasPushAndPullRemote() const
wxString GetUsername() const
bool HasLocalCommits() const
void SetCancelled(bool aCancel)
unsigned & TestedTypes()
void SetPassword(const wxString &aPassword)
wxString GetErrorString()
bool Register(const std::string &aLabel, F &&aWork)
Spawn a tracked orphan thread running aWork.
git_repository * GetRepo() const
Get a pointer to the git repository.
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:137
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:541
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition pgm_base.cpp:225
The project local settings are things that are attached to a particular project, but also might be pa...
bool m_GitIntegrationDisabled
If true, KiCad will not use Git integration for this project even if a .git directory exists.
bool SaveToFile(const wxString &aDirectory="", bool aForce=false) override
Calls Store() and then writes the contents of the JSON document to a file.
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
std::unordered_map< wxString, wxTreeItemId > m_gitTreeCache
void onGitFetch(wxCommandEvent &event)
Fetch the latest changes from the git repository.
std::map< wxTreeItemId, KIGIT_COMMON::GIT_STATUS > m_gitStatusIcons
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
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
PROJECT_TREE_PANE(KICAD_MANAGER_FRAME *parent)
KICAD_MANAGER_FRAME * m_Parent
std::future< void > m_gitSyncTask
void onExpand(wxTreeEvent &Event)
Called on a click on the + or - button of an item with children.
void onGitSyncTimer(wxTimerEvent &event)
void onIdle(wxIdleEvent &aEvent)
Idle event handler, used process the selected items at a point in time when all other events have bee...
void updateTreeCache()
Updates the map of the wxtreeitemid to the name of each file for use in the thread.
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 onGitStatusTimer(wxTimerEvent &event)
void shutdownFileWatcher()
Shutdown the file watcher.
std::mutex m_gitTreeCacheMutex
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.
std::future< void > m_gitStatusIconTask
void updateGitStatusIconMap()
This is a threaded call that will change the map of git status icons for use in the main thread.
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.
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:187
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition project.h:210
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:249
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:221
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition confirm.cpp:196
This file is part of the common library.
static bool registered
#define _(s)
EVT_MENU_RANGE(ID_GERBVIEW_DRILL_FILE1, ID_GERBVIEW_DRILL_FILEMAX, GERBVIEW_FRAME::OnDrlFileHistory) EVT_MENU_RANGE(ID_GERBVIEW_ZIP_FILE1
int ExecuteFile(const wxString &aEditorName, const wxString &aFileName, wxProcess *aCallback, bool aFileForKicad)
Call the executable file aEditorName with the parameter aFileName.
Definition gestfich.cpp:161
GIT_BACKEND * GetGitBackend()
InitResult
int m_GitIconRefreshInterval
The interval in milliseconds to refresh the git icons in the project tree.
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 LockFileExtension
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 LockFilePrefix
static const std::string CsvFileExtension
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 BackupFileSuffix
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:655
@ ID_PROJECT_TREE
Definition kicad_id.h:36
@ ID_LEFT_FRAME
Definition kicad_id.h:35
bool LaunchExternal(const wxString &aPath)
Launches the given file or folder in the host OS.
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:109
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.
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:100
wxDECLARE_EVENT(wxCUSTOM_PANEL_SHOWN_EVENT, wxCommandEvent)
PGM_BASE & Pgm()
The global program "get" accessor.
#define NAMELESS_PROJECT
default name for nameless projects
Definition project.h:44
project_tree_ids
The frame that shows the tree list of files and subdirectories inside the working directory.
@ ID_PROJECT_OPEN_DIR
@ ID_GIT_PUSH
@ ID_GIT_REVERT_LOCAL
@ ID_JOBS_RUN
@ ID_PROJECT_SWITCH_TO_OTHER
@ ID_PROJECT_NEWDIR
@ ID_PROJECT_RENAME
@ ID_GIT_SWITCH_QUICK5
@ ID_GIT_REMOVE_VCS
@ ID_GIT_COMMIT_FILE
@ ID_GIT_PULL
@ ID_GIT_COMPARE
@ ID_PROJECT_TXTEDIT
@ ID_GIT_SWITCH_QUICK2
@ ID_GIT_REMOVE_FROM_INDEX
@ ID_GIT_RESOLVE_CONFLICT
@ ID_GIT_CLONE_PROJECT
@ ID_GIT_SWITCH_QUICK4
@ ID_GIT_SWITCH_QUICK3
@ ID_GIT_COMMIT_PROJECT
@ ID_GIT_SWITCH_BRANCH
@ ID_GIT_ADD_TO_INDEX
@ ID_GIT_SYNC_PROJECT
@ ID_GIT_FETCH
@ ID_GIT_SWITCH_QUICK1
@ ID_GIT_INITIALIZE_PROJECT
@ ID_PROJECT_DELETE
std::vector< wxString > getProjects(const wxDir &dir)
static const wxChar * s_allowedExtensionsToList[]
static bool promptForGitCredentials(wxWindow *aParent, KIGIT_COMMON *aCommon)
#define wxFileSystemWatcher
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
KIGIT_COMMON::GIT_CONN_TYPE connType
std::string path
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:31
wxLogTrace helper definitions.
TREE_FILE_TYPE
Definition of file extensions used in Kicad.
#define PR_NO_ABORT