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, see <https://www.gnu.org/licenses/>.
20 */
21
22#include <stack>
25#include <git/git_backend.h>
26
27#include <wx/regex.h>
28#include <wx/richmsgdlg.h>
29#include <wx/stdpaths.h>
30#include <wx/string.h>
31#include <wx/msgdlg.h>
32#include <wx/textdlg.h>
33#include <wx/timer.h>
34#include <wx/wupdlock.h>
35#include <wx/log.h>
36#include <wx/dcclient.h>
37#include <wx/progdlg.h>
38#include <wx/settings.h>
39#include <wx/weakref.h>
40
41#include <frame_type.h>
42#include <kiway.h>
43#include <mail_type.h>
45
47#include <advanced_config.h>
48#include <bitmaps.h>
49#include <bitmap_store.h>
50#include <confirm.h>
56#include <gestfich.h>
57#include <macros.h>
58#include <trace_helpers.h>
61#include <core/kicad_algo.h>
62#include <paths.h>
64#include <scoped_set_reset.h>
65#include <string_utils.h>
66#include <thread_pool.h>
67#include <launch_ext.h>
68#include <wx_log_utils.h>
69
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// The pool's future does not block on destruction, so replacing one still running would abandon
105// the task rather than wait for it.
106static bool gitTaskStillRunning( const std::future<void>& aTask )
107{
108 return aTask.valid() && aTask.wait_for( std::chrono::seconds( 0 ) ) != std::future_status::ready;
109}
110
111
112// list of files extensions listed in the tree project window
113// Add extensions in a compatible regex format to see others files types
114static const wxChar* s_allowedExtensionsToList[] =
115{
116 wxT( "^.*\\.pro$" ),
117 wxT( "^.*\\.kicad_pro$" ),
118 wxT( "^.*\\.pdf$" ),
119 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
120 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
121 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
122 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
123 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
124 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
125 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
126 wxT( "^.*\\.net$" ), // pcbnew netlist file
127 wxT( "^.*\\.cir$" ), // Spice netlist file
128 wxT( "^.*\\.lib$" ), // Legacy schematic library file
129 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
130 wxT( "^.*\\.txt$" ), // Text files
131 wxT( "^.*\\.md$" ), // Markdown files
132 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
133 wxT( "^.*\\.gbr$" ), // Gerber file
134 wxT( "^.*\\.gbrjob$" ), // Gerber job file
135 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
136 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
137 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
138 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
139 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
140 wxT( "^.*\\.odt$" ),
141 wxT( "^.*\\.htm$" ),
142 wxT( "^.*\\.html$" ),
143 wxT( "^.*\\.rpt$" ), // Report files
144 wxT( "^.*\\.csv$" ), // Report files in comma separated format
145 wxT( "^.*\\.pos$" ), // Footprint position files
146 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
147 wxT( "^.*\\.drl$" ), // Excellon drill files
148 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
149 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
150 wxT( "^.*\\.svg$" ), // SVG print/plot files
151 wxT( "^.*\\.ps$" ), // PostScript plot files
152 wxT( "^.*\\.zip$" ), // Zip archive files
153 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
154 nullptr // end of list
155};
156
157
163
164
166{
167 ID_PROJECT_TXTEDIT = 8700, // Start well above wxIDs
173
174 ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project
175 ID_GIT_REMOTE_SETTINGS, // Configure (or set) the default remote on an existing repo
176 ID_GIT_REMOVE_VCS, // Toggle Git integration for this project (preference in kicad_prl)
177 ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch
178 ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch
179 ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch
180 ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch
181 ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch
182 ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch
183
185};
186
187
188BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
189 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
190 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
191 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
198
204
206
207 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
208 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
209END_EVENT_TABLE()
210
211
212wxDECLARE_EVENT( UPDATE_ICONS, wxCommandEvent );
213
215 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxTAB_TRAVERSAL ),
216 m_branchQuery( std::make_unique<KIGIT_QUERY>() )
217{
218 m_Parent = parent;
219 m_TreeProject = nullptr;
220 m_isRenaming = false;
221 m_selectedItem = nullptr;
222 m_watcherNeedReset = false;
223 m_watcher = nullptr;
224 m_gitIconsInitialized = false;
225
226 Bind( wxEVT_FSWATCHER,
227 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
228
229 Bind( wxEVT_SYS_COLOUR_CHANGED,
230 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
231
232 m_gitSyncTimer.SetOwner( this );
233 m_gitStatusTimer.SetOwner( this );
234 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
235 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
236 /*
237 * Filtering is now inverted: the filters are actually used to _enable_ support
238 * for a given file type.
239 */
240 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
241 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
242
243 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
244
246}
247
248
250{
251 m_branchQuery->Cancel();
252
253 Unbind( wxEVT_FSWATCHER,
254 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
255 Unbind( wxEVT_SYS_COLOUR_CHANGED,
256 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
257
258 m_gitSyncTimer.Stop();
259 m_gitStatusTimer.Stop();
260 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
261 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
263
264 if( std::shared_ptr<KIGIT_COMMON> common = m_gitListenerSession.lock() )
265 common->RemoveRefreshListener( this );
266
267 if( m_gitSyncTask.valid() )
268 m_gitSyncTask.wait();
269
270 if( m_gitStatusIconTask.valid() )
271 m_gitStatusIconTask.wait();
272}
273
274
276{
277 if( m_watcher )
278 {
279 m_watcher->RemoveAll();
280 m_watcher->SetOwner( nullptr );
281 delete m_watcher;
282 m_watcher = nullptr;
283 }
284}
285
286
288{
289 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
290
291 if( tree_data.size() != 1 )
292 return;
293
294 wxString prj_filename = tree_data[0]->GetFileName();
295
296 m_Parent->LoadProject( prj_filename );
297}
298
299
300void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
301{
302 // Get the root directory name:
303 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
304
305 for( PROJECT_TREE_ITEM* item_data : tree_data )
306 {
307 // Ask for the new sub directory name
308 wxString curr_dir = item_data->GetDir();
309
310 if( curr_dir.IsEmpty() )
311 {
312 // Use project path if the tree view path was empty.
313 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
314
315 // As a last resort use the user's documents folder.
316 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
318
319 if( !curr_dir.IsEmpty() )
320 curr_dir += wxFileName::GetPathSeparator();
321 }
322
323 LaunchExternal( curr_dir );
324 }
325}
326
327
328void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
329{
330 // Get the root directory name:
331 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
332
333 for( PROJECT_TREE_ITEM* item_data : tree_data )
334 {
335 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
336
337 // Ask for the new sub directory name
338 wxString curr_dir = item_data->GetDir();
339
340 if( curr_dir.IsEmpty() )
341 curr_dir = prj_dir;
342
343 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
344
345 if( new_dir.IsEmpty() )
346 return;
347
348 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
349
350 if( !wxMkdir( full_dirname ) )
351 return;
352
353 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
354 }
355}
356
357
359{
360 switch( type )
361 {
380 case TREE_FILE_TYPE::DRILL_NC: return "nc";
381 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
391
395 case TREE_FILE_TYPE::DIRECTORY: break;
396 }
397
398 return wxEmptyString;
399}
400
401
402std::vector<wxString> getProjects( const wxDir& dir )
403{
404 std::vector<wxString> projects;
405 wxString dir_filename;
406 bool haveFile = dir.GetFirst( &dir_filename );
407
408 while( haveFile )
409 {
410 wxFileName file( dir_filename );
411
412 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
413 || file.GetExt() == FILEEXT::ProjectFileExtension )
414 projects.push_back( file.GetName() );
415
416 haveFile = dir.GetNext( &dir_filename );
417 }
418
419 return projects;
420}
421
422
423
424
425
426wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
427 const wxTreeItemId& aParent,
428 std::vector<wxString>* aProjectNames,
429 bool aRecurse )
430{
432 wxFileName fn( aName );
433
434 if( KIPLATFORM::IO::IsFileHidden( aName ) )
435 return wxTreeItemId();
436
437 // Windows only hides .git when it carries the hidden attribute
438 if( fn.GetFullName().IsSameAs( wxS( ".git" ), false ) )
439 return wxTreeItemId();
440
441 if( wxDirExists( aName ) )
442 {
444 }
445 else
446 {
447 // Filter
448 wxRegEx reg;
449 bool addFile = false;
450
451 for( const wxString& m_filter : m_filters )
452 {
453 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
454 wxString::Format( "Regex %s failed to compile.", m_filter ) );
455
456 if( reg.Matches( aName ) )
457 {
458 addFile = true;
459 break;
460 }
461 }
462
463 if( !addFile )
464 return wxTreeItemId();
465
466 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
467 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
468 {
469 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
470
471 if( ext == wxT( "" ) )
472 continue;
473
474 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ), wxRE_ICASE )
475 && reg.Matches( aName ) )
476 {
477 type = (TREE_FILE_TYPE) i;
478 break;
479 }
480 }
481 }
482
483 wxString file = wxFileNameFromPath( aName );
484 wxFileName currfile( file );
485 wxFileName project( m_Parent->GetProjectFileName() );
486 bool showAllSchematics = gitRepo() != nullptr;
487
488 // Ignore legacy projects with the same name as the current project
489 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
490 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
491 {
492 return wxTreeItemId();
493 }
494
495 if( !showAllSchematics && ( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
496 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) ) )
497 {
498 if( aProjectNames )
499 {
500 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
501 return wxTreeItemId();
502 }
503 else
504 {
505 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
506
507 if( !parentTreeItem )
508 return wxTreeItemId();
509
510 wxDir parentDir( parentTreeItem->GetDir() );
511 std::vector<wxString> projects = getProjects( parentDir );
512
513 if( !alg::contains( projects, currfile.GetName() ) )
514 return wxTreeItemId();
515 }
516 }
517
518 // also check to see if it is already there.
519 wxTreeItemIdValue cookie;
520 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
521
522 while( kid.IsOk() )
523 {
524 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
525
526 if( itemData && itemData->GetFileName() == aName )
527 return itemData->GetId(); // well, we would have added it, but it is already here!
528
529 kid = m_TreeProject->GetNextChild( aParent, cookie );
530 }
531
532 // Only show current files if both legacy and current files are present
537 {
538 kid = m_TreeProject->GetFirstChild( aParent, cookie );
539
540 while( kid.IsOk() )
541 {
542 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
543
544 if( itemData )
545 {
546 wxFileName fname( itemData->GetFileName() );
547
548 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
549 {
550 switch( type )
551 {
553 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
554 return wxTreeItemId();
555
556 break;
557
559 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
560 return wxTreeItemId();
561
562 break;
563
565 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
566 m_TreeProject->Delete( kid );
567
568 break;
569
571 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
572 m_TreeProject->Delete( kid );
573
574 break;
575
576 default:
577 break;
578 }
579 }
580 }
581
582 kid = m_TreeProject->GetNextChild( aParent, cookie );
583 }
584 }
585
586 // Append the item (only appending the filename not the full path):
587 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
588 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
589
590 m_TreeProject->SetItemData( newItemId, data );
591 data->SetState( 0 );
592
593 // Mark root files (files which have the same aName as the project)
594 wxString fileName = currfile.GetName().Lower();
595 wxString projName = project.GetName().Lower();
596
597 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
598 data->SetRootFile( true );
599
600#ifndef __WINDOWS__
601 bool subdir_populated = false;
602#endif
603
604 // This section adds dirs and files found in the subdirs
605 // in this case AddFile is recursive, but for the first level only.
606 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
607 {
608 wxDir dir( aName );
609
610 if( dir.IsOpened() ) // protected dirs will not open properly.
611 {
612 std::vector<wxString> projects = getProjects( dir );
613 wxString dir_filename;
614 bool haveFile = dir.GetFirst( &dir_filename );
615
616 data->SetPopulated( true );
617
618#ifndef __WINDOWS__
619 subdir_populated = aRecurse;
620#endif
621
622 while( haveFile )
623 {
624 // Add name in tree, but do not recurse
625 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
626 addItemToProjectTree( path, newItemId, &projects, false );
627
628 haveFile = dir.GetNext( &dir_filename );
629 }
630 }
631
632 // Sort filenames by alphabetic order
633 m_TreeProject->SortChildren( newItemId );
634 }
635
636#ifndef __WINDOWS__
637 if( subdir_populated )
638 m_watcherNeedReset = true;
639#endif
640
641 return newItemId;
642}
643
644
646{
647 std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
648 std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
649
650 m_gitStatusTimer.Stop();
651 m_gitSyncTimer.Stop();
652
653 m_gitTreeCache.clear();
654 m_gitStatusIcons.clear();
655
656 wxString pro_dir = m_Parent->GetProjectFileName();
657
658 if( !m_TreeProject )
659 m_TreeProject = new PROJECT_TREE( this );
660 else
661 m_TreeProject->DeleteAllItems();
662
663 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
664 return;
665
666 // Releasing here would cancel a push or pull running from an editor panel on every tree
667 // refresh, and a project switch already disposes of the session.
668 if( gitRepo() )
669 {
670 if( !Pgm().GetCommonSettings()->m_Git.enableGit )
672
673 m_gitIconsInitialized = false;
674 }
675
676 wxFileName fn = pro_dir;
677 bool prjReset = false;
678
679 if( !fn.IsOk() )
680 {
681 fn.Clear();
682 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
683 fn.SetName( NAMELESS_PROJECT );
685 prjReset = true;
686 }
687
688 bool prjOpened = fn.FileExists();
689
690 if( Pgm().GetCommonSettings()->m_Git.enableGit )
691 {
694 }
695
696 // We may have opened a legacy project, in which case GetProjectFileName will return the
697 // name of the migrated (new format) file, which may not have been saved to disk yet.
698 if( !prjOpened && !prjReset )
699 {
701 prjOpened = fn.FileExists();
702
703 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
705 }
706
707 // root tree:
708 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
709 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
710 m_TreeProject->SetItemBold( m_root, true );
711
712 // The main project file is now a JSON file
715
716 m_TreeProject->SetItemData( m_root, data );
717
718 // Now adding all current files if available
719 if( prjOpened )
720 {
721 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
722 wxDir dir( pro_dir );
723
724 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
725 {
726 std::vector<wxString> projects = getProjects( dir );
727 wxString filename;
728 bool haveFile = dir.GetFirst( &filename );
729
730 while( haveFile )
731 {
732 if( filename != fn.GetFullName() )
733 {
734 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
735
736 // Add items living in the project directory, and populate the item
737 // if it is a directory (sub directories will be not populated)
738 addItemToProjectTree( name, m_root, &projects, true );
739 }
740
741 haveFile = dir.GetNext( &filename );
742 }
743 }
744 }
745 else
746 {
747 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
748 }
749
750 m_TreeProject->Expand( m_root );
751
752 // Sort filenames by alphabetic order
753 m_TreeProject->SortChildren( m_root );
754
755 CallAfter(
756 [this] ()
757 {
758 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
759 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
760 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
761 } );
762}
763
764
765void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
766{
767 wxTreeItemId curr_item = Event.GetItem();
768
769 // Ensure item is selected (Under Windows right click does not select the item)
770 m_TreeProject->SelectItem( curr_item );
771
772 // The tree control still uses the clicked item after this returns, so a menu command
773 // that closes the project must not run inside the click.
774 CallAfter(
775 [this]
776 {
778 } );
779}
780
781
783{
784 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
785
786 bool can_switch_to_project = true;
787 bool can_create_new_directory = true;
788 bool can_open_this_directory = true;
789 bool can_edit = true;
790 bool can_rename = true;
791 bool can_delete = true;
792 bool run_jobs = false;
793
794 bool gitIntegrationDisabled = Prj().GetLocalSettings().m_GitIntegrationDisabled;
795 bool vcs_enabled = !gitIntegrationDisabled;
796 bool vcs_has_repo = gitRepo() != nullptr;
797 bool vcs_can_init = vcs_enabled && !vcs_has_repo;
798 bool vcs_can_set_remote = vcs_enabled && vcs_has_repo;
799 // Disable/Enable is a per-project preference toggle, so it's available whenever we
800 // detected a repository for this project or integration is currently disabled.
801 bool vcs_can_remove = vcs_has_repo || gitIntegrationDisabled;
802 bool vcs_can_switch = vcs_enabled && vcs_has_repo;
803 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
804
805 // Check if the libgit2 library is available via backend
806 bool libgit_init = GetGitBackend() && GetGitBackend()->IsLibraryAvailable();
807
808 vcs_menu &= libgit_init;
809
810 if( selection.size() == 0 )
811 return;
812
813 // Remove things that don't make sense for multiple selections
814 if( selection.size() != 1 )
815 {
816 can_switch_to_project = false;
817 can_create_new_directory = false;
818 can_rename = false;
819 }
820
821 for( PROJECT_TREE_ITEM* item : selection )
822 {
823 // Check for empty project
824 if( !item )
825 {
826 can_switch_to_project = false;
827 can_edit = false;
828 can_rename = false;
829 continue;
830 }
831
832 can_delete = item->CanDelete();
833 can_rename = item->CanRename();
834
835 switch( item->GetType() )
836 {
839 can_rename = false;
840
841 if( item->GetId() == m_TreeProject->GetRootItem() )
842 {
843 can_switch_to_project = false;
844 }
845 else
846 {
847 can_create_new_directory = false;
848 can_open_this_directory = false;
849 }
850 break;
851
853 can_switch_to_project = false;
854 can_edit = false;
855 break;
856
859 can_edit = false;
860 can_switch_to_project = false;
861 can_create_new_directory = false;
862 can_open_this_directory = false;
863 break;
864
866 run_jobs = true;
867 can_edit = false;
869
873
874 default:
875 can_switch_to_project = false;
876 can_create_new_directory = false;
877 can_open_this_directory = false;
878
879 break;
880 }
881 }
882
883 wxMenu popup_menu;
884 wxString text;
885 wxString help_text;
886
887 if( can_switch_to_project )
888 {
889 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
890 _( "Close all editors, and switch to the selected project" ),
892 popup_menu.AppendSeparator();
893 }
894
895 if( can_create_new_directory )
896 {
897 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
898 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
899 }
900
901 if( can_open_this_directory )
902 {
903 if( selection.size() == 1 )
904 {
905#ifdef __APPLE__
906 text = _( "Reveal in Finder" );
907 help_text = _( "Reveals the directory in a Finder window" );
908#else
909 text = _( "Open Directory in File Explorer" );
910 help_text = _( "Opens the directory in the default system file manager" );
911#endif
912 }
913 else
914 {
915#ifdef __APPLE__
916 text = _( "Reveal in Finder" );
917 help_text = _( "Reveals the directories in a Finder window" );
918#else
919 text = _( "Open Directories in File Explorer" );
920 help_text = _( "Opens the directories in the default system file manager" );
921#endif
922 }
923
924 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
926 }
927
928 if( can_edit )
929 {
930 if( selection.size() == 1 )
931 help_text = _( "Open the file in a Text Editor" );
932 else
933 help_text = _( "Open files in a Text Editor" );
934
935 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
937 }
938
939 if( run_jobs && selection.size() == 1 )
940 {
941 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
943 }
944
945 if( can_rename )
946 {
947 if( selection.size() == 1 )
948 {
949 text = _( "Rename File..." );
950 help_text = _( "Rename file" );
951 }
952 else
953 {
954 text = _( "Rename Files..." );
955 help_text = _( "Rename files" );
956 }
957
958 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
960 }
961
962 if( can_delete )
963 {
964 if( selection.size() == 1 )
965 help_text = _( "Delete the file and its content" );
966 else
967 help_text = _( "Delete the files and their contents" );
968
969 if( can_switch_to_project
970 || can_create_new_directory
971 || can_open_this_directory
972 || can_edit
973 || can_rename )
974 {
975 popup_menu.AppendSeparator();
976 }
977
978#ifdef __WINDOWS__
979 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
981#else
982 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
984#endif
985 }
986
987 if( vcs_menu )
988 {
989 wxMenu* vcs_submenu = new wxMenu();
990 wxMenu* branch_submenu = new wxMenu();
991 wxMenuItem* vcs_menuitem = nullptr;
992
993 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
994 _( "Add Project to Version Control..." ),
995 _( "Initialize a new repository" ) );
996 vcs_menuitem->Enable( vcs_can_init );
997
998 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOTE_SETTINGS, _( "Configure Default Remote..." ),
999 _( "Set or change the default remote URL and credentials" ) );
1000 vcs_menuitem->Enable( vcs_can_set_remote );
1001
1002 // Committing, pushing and pulling are done from the docked Version Control panel, so
1003 // the version-control submenu keeps only repository setup and branch operations.
1004 vcs_submenu->AppendSeparator();
1005
1006 // Reading the branch list walks the repository. On contention the submenu is built
1007 // without the shortcuts rather than reading it concurrently.
1008 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
1009 m_gitMenuBranches.clear();
1010 m_gitMenuSession = common;
1011 std::vector<KIGIT_BRANCH_INFO> branches;
1012
1013 {
1014 std::unique_lock<std::mutex> branchLock = common->TryLockAction();
1015
1016 if( branchLock.owns_lock() )
1017 branches = common->GetBranches();
1018 }
1019
1021
1022 for( const KIGIT_BRANCH_INFO& branch : branches )
1023 {
1024 if( branch.isRemote || branch.isCurrent )
1025 continue;
1026
1027 m_gitMenuBranches.push_back( branch.name );
1028
1029 if( m_gitMenuBranches.size() == 5 )
1030 break;
1031 }
1032
1033 for( size_t ii = 0; ii < m_gitMenuBranches.size(); ++ii )
1034 {
1035 wxString msg = _( "Switch to branch " ) + m_gitMenuBranches[ii];
1036 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_QUICK1 + ii, m_gitMenuBranches[ii], msg );
1037 vcs_menuitem->Enable( vcs_can_switch );
1038 }
1039
1040 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
1041 _( "Switch to a different branch" ) );
1042 vcs_menuitem->Enable( vcs_can_switch );
1043
1044 vcs_menuitem = vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1045 vcs_menuitem->Enable( vcs_can_switch );
1046
1047 vcs_submenu->AppendSeparator();
1048
1049 if( gitIntegrationDisabled )
1050 {
1051 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Enable Git Integration" ),
1052 _( "Re-enable Git integration for this project" ) );
1053 }
1054 else
1055 {
1056 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Disable Git Integration" ),
1057 _( "Disable Git integration for this project" ) );
1058 }
1059
1060 vcs_menuitem->Enable( vcs_can_remove );
1061
1062 popup_menu.AppendSeparator();
1063 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1064 }
1065
1066 if( popup_menu.GetMenuItemCount() > 0 )
1067 PopupMenu( &popup_menu );
1068}
1069
1070
1072{
1073 wxString editorname = Pgm().GetTextEditor();
1074
1075 if( editorname.IsEmpty() )
1076 {
1077 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1078 return;
1079 }
1080
1081 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1082
1083 for( PROJECT_TREE_ITEM* item_data : tree_data )
1084 {
1085 wxString fullFileName = item_data->GetFileName();
1086
1087 if( !fullFileName.IsEmpty() )
1088 {
1089 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1090 }
1091 }
1092}
1093
1094
1095void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1096{
1097 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1098
1099 for( PROJECT_TREE_ITEM* item_data : tree_data )
1100 item_data->Delete();
1101}
1102
1103
1104void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1105{
1106 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1107 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1108
1109 // XXX: Unnecessary?
1110 if( tree_data.size() != 1 )
1111 return;
1112
1113 wxString buffer = m_TreeProject->GetItemText( curr_item );
1114 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1115 tree_data[0]->GetFileName() );
1116 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1117
1118 if( dlg.ShowModal() != wxID_OK )
1119 return; // canceled by user
1120
1121 buffer = dlg.GetValue();
1122 buffer.Trim( true );
1123 buffer.Trim( false );
1124
1125 if( buffer.IsEmpty() )
1126 return; // empty file name not allowed
1127
1128 tree_data[0]->Rename( buffer, true );
1129 m_isRenaming = true;
1130}
1131
1132
1133void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1134{
1135 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1136
1137 if( tree_data.size() != 1 )
1138 return;
1139
1140 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1141 // there will be more events at least on Windows in this frame that will steal focus from
1142 // any newly launched windows
1143 m_selectedItem = tree_data[0];
1144}
1145
1146
1147void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1148{
1149 // Idle executes once all other events finished processing. This makes it ideal to launch
1150 // a new window without starting Focus wars.
1151 if( m_watcherNeedReset )
1152 {
1153 m_selectedItem = nullptr;
1155 }
1156
1157 if( m_selectedItem != nullptr && m_TreeProject->GetRootItem().IsOk() )
1158 {
1159 // Make sure m_selectedItem still exists in the tree before activating it.
1160 std::vector<wxTreeItemId> validItemIds;
1161 m_TreeProject->GetItemsRecursively( m_TreeProject->GetRootItem(), validItemIds );
1162
1163 for( wxTreeItemId id : validItemIds )
1164 {
1165 if( GetItemIdData( id ) == m_selectedItem )
1166 {
1167 // Activate launches a window which may run the event loop on top of us and cause
1168 // onIdle to get called again, so be sure to null out m_selectedItem first.
1170 m_selectedItem = nullptr;
1171
1172 item->Activate( this );
1173 break;
1174 }
1175 }
1176 }
1177}
1178
1179
1180void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1181{
1182 wxTreeItemId itemId = Event.GetItem();
1183 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1184
1185 if( !tree_data )
1186 return;
1187
1188 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1189 return;
1190
1191 // explore list of non populated subdirs, and populate them
1192 wxTreeItemIdValue cookie;
1193 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1194
1195#ifndef __WINDOWS__
1196 bool subdir_populated = false;
1197#endif
1198
1199 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1200 {
1201 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1202
1203 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1204 continue;
1205
1206 if( itemData->IsPopulated() )
1207 continue;
1208
1209 wxString fileName = itemData->GetFileName();
1210 wxDir dir( fileName );
1211
1212 if( dir.IsOpened() )
1213 {
1214 std::vector<wxString> projects = getProjects( dir );
1215 wxString dir_filename;
1216 bool haveFile = dir.GetFirst( &dir_filename );
1217
1218 while( haveFile )
1219 {
1220 // Add name to tree item, but do not recurse in subdirs:
1221 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1222 addItemToProjectTree( name, kid, &projects, false );
1223
1224 haveFile = dir.GetNext( &dir_filename );
1225 }
1226
1227 itemData->SetPopulated( true ); // set state to populated
1228
1229#ifndef __WINDOWS__
1230 subdir_populated = true;
1231#endif
1232 }
1233
1234 // Sort filenames by alphabetic order
1235 m_TreeProject->SortChildren( kid );
1236 }
1237
1238#ifndef __WINDOWS__
1239 if( subdir_populated )
1240 m_watcherNeedReset = true;
1241#endif
1242}
1243
1244
1245std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1246{
1247 wxArrayTreeItemIds selection;
1248 std::vector<PROJECT_TREE_ITEM*> data;
1249
1250 m_TreeProject->GetSelections( selection );
1251
1252 for( const wxTreeItemId itemId : selection )
1253 {
1254 PROJECT_TREE_ITEM* item = GetItemIdData( itemId );
1255
1256 if( !item )
1257 {
1258 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast failed?" ) );
1259 continue;
1260 }
1261
1262 data.push_back( item );
1263 }
1264
1265 return data;
1266}
1267
1268
1270{
1271 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1272}
1273
1274
1275wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1276{
1277 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1278
1279 // If the subdir is the current working directory, return m_root
1280 // in main list:
1281 if( prj_dir == aSubDir )
1282 return m_root;
1283
1284 // The subdir is in the main tree or in a subdir: Locate it
1285 wxTreeItemIdValue cookie;
1286 wxTreeItemId root_id = m_root;
1287 std::stack<wxTreeItemId> subdirs_id;
1288
1289 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1290
1291 while( true )
1292 {
1293 if( ! child.IsOk() )
1294 {
1295 if( subdirs_id.empty() ) // all items were explored
1296 {
1297 root_id = child; // Not found: return an invalid wxTreeItemId
1298 break;
1299 }
1300 else
1301 {
1302 root_id = subdirs_id.top();
1303 subdirs_id.pop();
1304 child = m_TreeProject->GetFirstChild( root_id, cookie );
1305
1306 if( !child.IsOk() )
1307 continue;
1308 }
1309 }
1310
1311 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1312
1313 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1314 {
1315 if( itemData->GetFileName() == aSubDir ) // Found!
1316 {
1317 root_id = child;
1318 break;
1319 }
1320
1321 // child is a subdir, push in list to explore it later
1322 if( itemData->IsPopulated() )
1323 subdirs_id.push( child );
1324 }
1325
1326 child = m_TreeProject->GetNextChild( root_id, cookie );
1327 }
1328
1329 return root_id;
1330}
1331
1332
1333void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1334{
1335 // No need to process events when we're shutting down
1336 if( !m_watcher )
1337 return;
1338
1339 // Ignore events that are not file creation, deletion, renaming or modification because
1340 // they are not relevant to the project tree.
1341 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1342 wxFSW_EVENT_DELETE |
1343 wxFSW_EVENT_RENAME |
1344 wxFSW_EVENT_MODIFY ) ) )
1345 {
1346 return;
1347 }
1348
1349 const wxFileName& pathModified = event.GetPath();
1350
1351 // Ignore events from .history directory (local backup)
1352 if( pathModified.GetFullPath().Contains( wxS( ".history" ) ) )
1353 return;
1354
1355 wxString subdir = pathModified.GetPath();
1356 wxString fn = pathModified.GetFullPath();
1357
1358 // Adjust directories to look like a file item (path and name).
1359 if( pathModified.GetFullName().IsEmpty() )
1360 {
1361 subdir = subdir.BeforeLast( '/' );
1362 fn = fn.BeforeLast( '/' );
1363 }
1364
1365 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1366
1367 if( !root_id.IsOk() )
1368 return;
1369
1370 CallAfter( [this] ()
1371 {
1372 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1373 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1374 } );
1375
1376 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1377 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1378
1379 switch( event.GetChangeType() )
1380 {
1381 case wxFSW_EVENT_CREATE:
1382 {
1383 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1384
1385 // If we are in the process of renaming a file, select the new one
1386 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1387 // pair of DELETE and CREATE events.
1388 if( m_isRenaming && newitem.IsOk() )
1389 {
1390 m_TreeProject->SelectItem( newitem );
1391 m_isRenaming = false;
1392 }
1393 }
1394 break;
1395
1396 case wxFSW_EVENT_DELETE:
1397 while( kid.IsOk() )
1398 {
1399 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1400
1401 if( itemData && itemData->GetFileName() == fn )
1402 {
1403 m_TreeProject->Delete( kid );
1404 return;
1405 }
1406 kid = m_TreeProject->GetNextChild( root_id, cookie );
1407 }
1408 break;
1409
1410 case wxFSW_EVENT_RENAME :
1411 {
1412 const wxFileName& newpath = event.GetNewPath();
1413 wxString newdir = newpath.GetPath();
1414 wxString newfn = newpath.GetFullPath();
1415
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 break;
1424 }
1425
1426 kid = m_TreeProject->GetNextChild( root_id, cookie );
1427 }
1428
1429 // Add the new item only if it is not the current project file (root item).
1430 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1431 // called after an actual file rename, and the cleanup code does not explore the
1432 // root item, because it cannot be renamed by the user. Also, ensure the new file
1433 // actually exists on the file system before it is readded. On Linux, moving a file
1434 // to the trash can cause the same path to be returned in both the old and new paths
1435 // of the event, even though the file isn't there anymore.
1436 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1437
1438 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1439 {
1440 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1441 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1442
1443 // If the item exists, select it
1444 if( newitem.IsOk() )
1445 m_TreeProject->SelectItem( newitem );
1446 }
1447
1448 m_isRenaming = false;
1449 }
1450 break;
1451
1452 default:
1453 return;
1454 }
1455
1456 // Sort filenames by alphabetic order
1457 m_TreeProject->SortChildren( root_id );
1458}
1459
1460
1462{
1463 m_watcherNeedReset = false;
1464
1465 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1466
1467#if defined( _WIN32 )
1468 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1469 {
1470 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1471 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1472 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1473 // Windows Server shares :/
1474 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1475 m_Parent->SetStatusText( m_Parent->m_FileWatcherInfo, 1 );
1476 return;
1477 }
1478 else
1479 {
1480 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1481 m_Parent->SetStatusText( m_Parent->m_FileWatcherInfo, 1 );
1482 }
1483#endif
1484
1485 // Prepare file watcher:
1486 if( m_watcher )
1487 {
1488 m_watcher->RemoveAll();
1489 }
1490 else
1491 {
1492 // Create a wxWidgets log handler to catch errors during watcher creation
1493 // We need to to this because we cannot get error codes from the wxFileSystemWatcher
1494 // constructor. On Linux, if inotify cannot be initialized (usually due to resource limits),
1495 // wxWidgets will throw a system error and then, we throw another error below, trying to
1496 // add paths to a null watcher. We skip this by installing a temporary log handler that
1497 // catches errors during watcher creation and aborts if any error is detected.
1498 class WatcherLogHandler : public wxLog
1499 {
1500 public:
1501 explicit WatcherLogHandler( bool* err ) :
1502 m_err( err )
1503 {
1504 if( m_err )
1505 *m_err = false;
1506 }
1507
1508 protected:
1509 void DoLogTextAtLevel( wxLogLevel level, const wxString& text ) override
1510 {
1511 if( m_err && ( level == wxLOG_Error || level == wxLOG_FatalError ) )
1512 *m_err = true;
1513 }
1514
1515 private:
1516 bool* m_err;
1517 };
1518
1519 bool watcherHasError = false;
1520 WatcherLogHandler tmpLog( &watcherHasError );
1521
1522 {
1523 SCOPED_WXLOG_TARGET logOverride( &tmpLog );
1524
1526 m_watcher->SetOwner( this );
1527 }
1528
1529 if( watcherHasError )
1530 {
1531 return;
1532 }
1533 }
1534
1535 // We can see wxString under a debugger, not a wxFileName
1536 wxFileName fn;
1537 fn.AssignDir( prj_dir );
1538 fn.DontFollowLink();
1539
1540 // Add directories which should be monitored.
1541 // under windows, we add the curr dir and all subdirs
1542 // under unix, we add only the curr dir and the populated subdirs
1543 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1544 // under unix, the file watcher needs more work to be efficient
1545 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1546 {
1547 wxLogNull logNo; // avoid log messages
1548#ifdef __WINDOWS__
1549 if( ! m_watcher->AddTree( fn ) )
1550 {
1551 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1552 TO_UTF8( fn.GetFullPath() ) );
1553 return;
1554 }
1555 }
1556#else
1557 if( !m_watcher->Add( fn ) )
1558 {
1559 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1560 TO_UTF8( fn.GetFullPath() ) );
1561 return;
1562 }
1563 }
1564
1565 if( m_TreeProject->IsEmpty() )
1566 return;
1567
1568 // Add subdirs
1569 wxTreeItemIdValue cookie;
1570 wxTreeItemId root_id = m_root;
1571
1572 std::stack < wxTreeItemId > subdirs_id;
1573
1574 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1575 int total_watch_count = 0;
1576
1577 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1578 {
1579 if( !kid.IsOk() )
1580 {
1581 if( subdirs_id.empty() ) // all items were explored
1582 {
1583 break;
1584 }
1585 else
1586 {
1587 root_id = subdirs_id.top();
1588 subdirs_id.pop();
1589 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1590
1591 if( !kid.IsOk() )
1592 continue;
1593 }
1594 }
1595
1596 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1597
1598 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1599 {
1600 // we can see wxString under a debugger, not a wxFileName
1601 const wxString& path = itemData->GetFileName();
1602
1603 // Skip .history directory and its descendants (local backup)
1604 if( path.Contains( wxS( ".history" ) ) )
1605 {
1606 kid = m_TreeProject->GetNextChild( root_id, cookie );
1607 continue;
1608 }
1609
1610 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1611
1612 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1613 {
1614 {
1615 // Silence OS errors that come from the watcher
1616 wxLogNull silence;
1617 fn.AssignDir( path );
1618 m_watcher->Add( fn );
1619 total_watch_count++;
1620 }
1621
1622 // if kid is a subdir, push in list to explore it later
1623 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1624 subdirs_id.push( kid );
1625 }
1626 }
1627
1628 kid = m_TreeProject->GetNextChild( root_id, cookie );
1629 }
1630
1631 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1632 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1633#endif
1634
1635#if defined(DEBUG) && 1
1636 wxArrayString paths;
1637 m_watcher->GetWatchedPaths( &paths );
1638 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1639
1640 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1641 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1642#endif
1643 }
1644
1645
1647{
1648 m_branchQuery->Cancel();
1649 m_gitMenuBranches.clear();
1650 m_gitMenuSession.reset();
1652
1653 // Make sure we don't try to inspect the tree after we've deleted its items.
1655
1656 m_TreeProject->DeleteAllItems();
1657}
1658
1659
1661{
1662 m_branchQuery->Cancel();
1663
1664 if( gitRepo() )
1665 {
1666 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
1667 common->SetShuttingDown( true );
1668
1669 std::unique_lock<std::mutex> lock = common->TryLockAction();
1670
1671 constexpr auto kGraceMs = std::chrono::seconds( 2 );
1672 auto graceEnd = std::chrono::steady_clock::now() + kGraceMs;
1673
1674 while( !lock.owns_lock() && std::chrono::steady_clock::now() < graceEnd )
1675 {
1676 if( lock.try_lock() )
1677 break;
1678 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1679 }
1680
1681 constexpr auto kCheckInterval = std::chrono::seconds( 30 );
1682 bool userAbandoned = false;
1683
1684 while( !lock.owns_lock() && !userAbandoned )
1685 {
1686 auto intervalEnd = std::chrono::steady_clock::now() + kCheckInterval;
1687
1688 {
1689 wxProgressDialog progress( _( "Please wait" ),
1690 _( "Closing project..." ),
1691 100, this,
1692 wxPD_APP_MODAL | wxPD_SMOOTH );
1693
1694 while( !lock.try_lock()
1695 && std::chrono::steady_clock::now() < intervalEnd )
1696 {
1697 progress.Pulse();
1698 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1699 wxYield();
1700 }
1701 }
1702
1703 if( lock.owns_lock() )
1704 break;
1705
1706 wxMessageDialog ask( this,
1707 _( "A Git operation is still running.\n"
1708 "Keep waiting, or abandon?" ),
1709 _( "Git Operation Delayed" ),
1710 wxYES_NO | wxICON_QUESTION );
1711 ask.SetYesNoLabels( _( "Keep Waiting" ), _( "Abandon" ) );
1712
1713 if( ask.ShowModal() == wxID_NO )
1714 userAbandoned = true;
1715 }
1716
1717 if( userAbandoned )
1718 {
1719 // Cancellation was requested above, so a running fetch exits at its next
1720 // callback. The release defers the actual free to the orphan registry.
1721 common.reset();
1723 }
1724 else
1725 {
1726 git_repository* repo = common->GetRepo();
1728 common->SetRepo( nullptr );
1729 }
1730 }
1731}
1732
1733
1735{
1736 m_branchQuery->Cancel();
1737 m_gitMenuBranches.clear();
1738 m_gitMenuSession.reset();
1740}
1741
1742
1743std::shared_ptr<KIGIT_COMMON> PROJECT_TREE_PANE::gitCommon() const
1744{
1745 return Prj().GetGitCommon();
1746}
1747
1748
1750{
1751 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
1752
1753 if( m_gitListenerSession.lock() == common )
1754 return;
1755
1756 m_branchQuery->Cancel();
1757 m_gitMenuBranches.clear();
1758 m_gitMenuSession.reset();
1760
1761 if( std::shared_ptr<KIGIT_COMMON> previous = m_gitListenerSession.lock() )
1762 previous->RemoveRefreshListener( this );
1763
1764 // The status timer coalesces several notifications into one pass, and reads the repository
1765 // locally without fetching.
1766 common->AddRefreshListener( this,
1767 [this]()
1768 {
1769 if( m_TreeProject )
1770 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1771 } );
1772
1773 m_gitListenerSession = common;
1774}
1775
1776
1778{
1779 return gitCommon()->GetRepo();
1780}
1781
1782
1783void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1784{
1786 m_TreeProject->LoadIcons();
1787 m_TreeProject->Refresh();
1788
1789 aEvent.Skip();
1790}
1791
1792
1793void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1794{
1795 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1796 wxPaintDC dc( this );
1797
1798 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1799 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1800
1801 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1802 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1803}
1804
1805
1806void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1807{
1808 m_projectTreePane->FileWatcherReset();
1809}
1810
1811
1812// Prompts for git credentials after an auth failure. Returns false if cancelled.
1813static bool promptForGitCredentials( wxWindow* aParent, KIGIT_COMMON* aCommon, const wxString& aExplanation )
1814{
1815 return KIGIT::PromptForCredentials( aParent, aCommon, &Prj(), aExplanation );
1816}
1817
1818
1819// Persist a remote configuration the user accepted, to the secret store and the project
1820// local settings. Runs only after the remote itself was set up successfully.
1821static void saveAcceptedRemoteSettings( PROJECT& aProject, const RemoteConfig& aConfig )
1822{
1823 // Keyed like the attach-time lookup, which never sees credentials embedded in the address
1825 aConfig.password );
1826 aProject.GetLocalSettings().m_GitRepoUsername = aConfig.username;
1827 aProject.GetLocalSettings().m_GitSSHKey = aConfig.sshKey;
1828
1830 aProject.GetLocalSettings().m_GitRepoType = "ssh";
1832 aProject.GetLocalSettings().m_GitRepoType = "https";
1833 else
1834 aProject.GetLocalSettings().m_GitRepoType = "local";
1835}
1836
1837
1838void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1839{
1840 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1841
1842 wxString dir = tree_data->GetDir();
1843
1844 if( dir.empty() )
1845 {
1846 wxLogError( "Failed to initialize git project: project directory is empty." );
1847 return;
1848 }
1849
1850 GIT_INIT_HANDLER initHandler( gitCommon().get() );
1851 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1852
1853 if( initHandler.IsRepository( dir ) )
1854 {
1855 DisplayInfoMessage( topLevelParent,
1856 _( "The selected directory is already a Git project." ) );
1857 return;
1858 }
1859
1860 InitResult result = initHandler.InitializeRepository( dir );
1862 {
1863 KIGIT_ERROR_MESSAGE error = initHandler.PopError();
1864 DisplayErrorMessage( m_parent, error.summary, error.details );
1865 return;
1866 }
1867
1869
1870 gitCommon()->SetShuttingDown( false );
1871
1872 const char* canonicalWorkDir = git_repository_workdir( initHandler.GetRepo() );
1873
1874 if( canonicalWorkDir )
1875 {
1877 dir, wxString::FromUTF8( canonicalWorkDir ) );
1878 gitCommon()->SetProjectDir( symlinkWorkDir );
1879 }
1880
1881 DIALOG_GIT_REPOSITORY dlg( topLevelParent, initHandler.GetRepo() );
1882 dlg.SetTitle( _( "Set default remote" ) );
1883 dlg.SetSkipButtonLabel( _( "Skip" ) );
1884
1885 if( dlg.ShowModal() != wxID_OK )
1886 return;
1887
1888 // Set up the remote
1889 RemoteConfig remoteConfig;
1890 remoteConfig.url = dlg.GetRepoURL();
1891 remoteConfig.username = dlg.GetUsername();
1892 remoteConfig.password = dlg.GetPassword();
1893 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1894 remoteConfig.connType = dlg.GetRepoType();
1895
1896 if( !initHandler.SetupRemote( remoteConfig ) )
1897 {
1898 KIGIT_ERROR_MESSAGE error = initHandler.PopError();
1899 DisplayErrorMessage( m_parent, error.summary, error.details );
1900 return;
1901 }
1902
1904
1905 // Before the fetch, so a corrected login from the prompt below is the one that stays saved
1906 saveAcceptedRemoteSettings( Prj(), remoteConfig );
1907
1908 KIGIT_COMMON* common = gitCommon().get();
1909 bool retriedAfterTrust = false;
1910
1911 while( true )
1912 {
1913 GIT_PULL_HANDLER handler( common );
1914 handler.SetProgressReporter(
1915 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1916
1917 // A status read can hold the lock briefly, and the login or key just confirmed must not be dropped
1919
1920 if( handler.PerformFetch() )
1921 break;
1922
1923 if( std::optional<KIGIT::HOST_KEY_FAILURE> hostKey = handler.GetHostKeyFailure() )
1924 {
1925 if( !KIGIT::PromptToTrustHost( m_parent, *hostKey, retriedAfterTrust ) )
1926 break;
1927
1928 retriedAfterTrust = true;
1929 continue;
1930 }
1931
1932 KIGIT_ERROR_MESSAGE error = handler.PopError();
1933
1934 if( handler.WasAuthFailure() )
1935 {
1936 if( promptForGitCredentials( m_parent, common, error.summary ) )
1937 continue;
1938
1939 break;
1940 }
1941
1942 // The remote is saved either way, so this only says why its first fetch did not happen. A cancelled
1943 // fetch needs no dialog
1944 if( !error.summary.IsEmpty() && handler.GetFailureKind() != KIGIT_FAILURE::CANCELLED )
1945 DisplayErrorMessage( m_parent, error.summary, error.details );
1946
1947 break;
1948 }
1949}
1950
1951
1952void PROJECT_TREE_PANE::onGitRemoteSettings( wxCommandEvent& aEvent )
1953{
1954 KIGIT_COMMON* common = gitCommon().get();
1955 git_repository* repo = common ? common->GetRepo() : nullptr;
1956
1957 if( !repo )
1958 return;
1959
1960 wxString existingURL;
1961
1962 {
1963 // Read the remote under the lock, then release it before opening the dialog.
1964 std::unique_lock<std::mutex> lock = KIGIT::TryLockAction( *common, this );
1965
1966 if( !lock.owns_lock() )
1967 return;
1968
1969 git_remote* remote = nullptr;
1970
1971 if( git_remote_lookup( &remote, repo, "origin" ) == GIT_OK )
1972 {
1973 KIGIT::GitRemotePtr remotePtr( remote );
1974
1975 if( const char* url = git_remote_url( remote ) )
1976 existingURL = wxString::FromUTF8( url );
1977 }
1978 }
1979
1980 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1981 DIALOG_GIT_REPOSITORY dlg( topLevelParent, repo, existingURL );
1982 dlg.SetTitle( _( "Configure Default Remote" ) );
1983
1984 // Credentials are stored separately from the remote URL.
1985 KIGIT_COMMON credentials( nullptr );
1986 credentials.SetUsername( common->GetUsername() );
1987 credentials.SetRemote( existingURL );
1988 dlg.SetUsername( credentials.GetUsername() );
1989 dlg.SetPassword( credentials.GetPassword() );
1990 dlg.SetRepoSSHPath( Prj().GetLocalSettings().m_GitSSHKey );
1991
1992 if( dlg.ShowModal() != wxID_OK )
1993 return;
1994
1995 GIT_INIT_HANDLER initHandler( common );
1996
1997 RemoteConfig remoteConfig;
1998 remoteConfig.url = dlg.GetRepoURL();
1999 remoteConfig.username = dlg.GetUsername();
2000 remoteConfig.password = dlg.GetPassword();
2001 remoteConfig.sshKey = dlg.GetRepoSSHPath();
2002 remoteConfig.connType = dlg.GetRepoType();
2003
2004 bool setupOk = false;
2005 wxString setupError;
2006 wxString setupDetails;
2007
2008 {
2009 // Update the remote config and cached branch info under the same lock.
2010 std::unique_lock<std::mutex> lock = KIGIT::WaitForActionLock( *common, this );
2011
2012 if( !lock.owns_lock() )
2013 return;
2014
2015 setupOk = initHandler.SetupRemote( remoteConfig );
2016 KIGIT_ERROR_MESSAGE error = initHandler.PopError();
2017 setupError = error.summary;
2018 setupDetails = error.details;
2019
2020 if( setupOk )
2021 common->UpdateCurrentBranchInfo();
2022 }
2023
2024 if( !setupOk )
2025 {
2026 DisplayErrorMessage( topLevelParent, setupError, setupDetails );
2027 return;
2028 }
2029
2030 saveAcceptedRemoteSettings( Prj(), remoteConfig );
2031
2032 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2033}
2034
2035
2036void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
2037{
2038 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
2039
2040 if( !common->GetRepo() || common->IsShuttingDown() )
2041 return;
2042
2043 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
2044 {
2045 struct BRANCHES
2046 {
2047 std::vector<KIGIT_BRANCH_INFO> entries;
2048 bool loaded = false;
2049 };
2050
2051 auto branches = std::make_shared<BRANCHES>();
2052 wxWeakRef<PROJECT_TREE_PANE> self( this );
2053
2054 m_branchQuery->Run(
2055 common,
2056 [branches]( KIGIT_COMMON& aCommon )
2057 {
2058 branches->entries = aCommon.GetBranches();
2059 branches->loaded = true;
2060 },
2061 [self, common, branches]()
2062 {
2063 if( !branches->loaded || !self || self->IsBeingDeleted() || self->m_Parent->IsBeingDeleted()
2064 || common->IsShuttingDown() || self->gitCommon() != common
2065 || !Pgm().GetCommonSettings()->m_Git.enableGit
2066 || Prj().GetLocalSettings().m_GitIntegrationDisabled )
2067 {
2068 return;
2069 }
2070
2071 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( self->m_Parent ), std::move( branches->entries ) );
2072 int retval = dlg.ShowModal();
2073
2074 if( !self || self->IsBeingDeleted() || common->IsShuttingDown() || self->gitCommon() != common )
2075 return;
2076
2077 if( retval == wxID_OK || retval == wxID_ADD )
2078 self->switchGitBranch( common, dlg.GetBranchName(), retval == wxID_ADD );
2079 } );
2080
2081 return;
2082 }
2083
2084 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_QUICK1;
2085
2086 if( m_gitMenuSession.lock() != common || branchIndex < 0
2087 || static_cast<size_t>( branchIndex ) >= m_gitMenuBranches.size() )
2088 {
2089 return;
2090 }
2091
2092 switchGitBranch( common, m_gitMenuBranches[branchIndex], false );
2093}
2094
2095
2096void PROJECT_TREE_PANE::switchGitBranch( std::shared_ptr<KIGIT_COMMON> aCommon, const wxString& aBranchName,
2097 bool aCreate )
2098{
2099 if( IsBeingDeleted() || m_Parent->IsBeingDeleted() || gitCommon() != aCommon || aCommon->IsShuttingDown()
2100 || !Pgm().GetCommonSettings()->m_Git.enableGit || Prj().GetLocalSettings().m_GitIntegrationDisabled )
2101 {
2102 return;
2103 }
2104
2105 if( !aCreate )
2106 {
2107 SwitchProjectToBranch( m_Parent, this, aBranchName, *m_branchQuery );
2108 return;
2109 }
2110
2111 if( wxString nameError = KIGIT::BranchNameError( aBranchName ); !nameError.IsEmpty() )
2112 {
2113 DisplayErrorMessage( this, nameError );
2114 return;
2115 }
2116
2117 // Keep Add subject to the same worktree checks before creating the branch. The shared
2118 // switch helper then owns confirmation, editor checks, checkout, and reload.
2119 auto createBranch = [this, aCommon, aBranchName]( const GIT_WORKTREE_STATUS& )
2120 {
2121 if( IsBeingDeleted() || m_Parent->IsBeingDeleted() || gitCommon() != aCommon || aCommon->IsShuttingDown()
2122 || !Pgm().GetCommonSettings()->m_Git.enableGit || Prj().GetLocalSettings().m_GitIntegrationDisabled )
2123 {
2124 return;
2125 }
2126
2127 int result;
2128 wxString error;
2129
2130 {
2131 std::unique_lock<std::mutex> lock = KIGIT::WaitForActionLock( *aCommon, this );
2132
2133 if( !lock.owns_lock() )
2134 return;
2135
2136 result = KIGIT::PROJECT_GIT_UTILS::CreateBranch( aCommon->GetRepo(), aBranchName );
2138 }
2139
2140 if( result != GIT_OK )
2141 {
2142 DisplayErrorMessage( this, _( "Could not create the branch." ), error );
2143 return;
2144 }
2145
2146 SwitchProjectToBranch( m_Parent, this, aBranchName, *m_branchQuery );
2147 aCommon->NotifyRefreshListeners();
2148 };
2149
2150 ProjectClearOfUncommittedWork( m_Parent, this, *m_branchQuery, std::move( createBranch ) );
2151}
2152
2153
2154void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
2155{
2156 PROJECT_LOCAL_SETTINGS& localSettings = Prj().GetLocalSettings();
2157
2158 // Toggle the Git integration disabled preference
2159 localSettings.m_GitIntegrationDisabled = !localSettings.m_GitIntegrationDisabled;
2160
2161 wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Git integration %s" ),
2162 localSettings.m_GitIntegrationDisabled ? wxS( "disabled" ) : wxS( "enabled" ) );
2163
2164 if( localSettings.m_GitIntegrationDisabled )
2165 {
2166 // Disabling Git integration - release the repo and clear item states
2168 m_gitIconsInitialized = false;
2169
2170 // Clear all item states to remove git status icons
2171 std::stack<wxTreeItemId> items;
2172 items.push( m_TreeProject->GetRootItem() );
2173
2174 while( !items.empty() )
2175 {
2176 wxTreeItemId current = items.top();
2177 items.pop();
2178
2179 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
2180
2181 wxTreeItemIdValue cookie;
2182 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2183
2184 while( child.IsOk() )
2185 {
2186 items.push( child );
2187 child = m_TreeProject->GetNextChild( current, cookie );
2188 }
2189 }
2190 }
2191 else
2192 {
2193 // Re-enabling Git integration - try to find and connect to the repository
2195 }
2196
2197 // Save the preference to the project local settings file
2198 localSettings.SaveToFile( Prj().GetProjectPath() );
2199}
2200
2201
2203{
2204 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
2205 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
2206
2207 if( !lock.owns_lock() )
2208 {
2209 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
2210 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2211 return;
2212 }
2213
2214 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2215 {
2216 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
2217 return;
2218 }
2219
2220 std::stack<wxTreeItemId> items;
2221 items.push(m_TreeProject->GetRootItem());
2222
2223 while( !items.empty() )
2224 {
2225 wxTreeItemId current = items.top();
2226 items.pop();
2227
2228 if( current.IsOk() && m_TreeProject->ItemHasChildren( current ) )
2229 {
2230 wxTreeItemIdValue cookie;
2231 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2232
2233 while( child.IsOk() )
2234 {
2235 items.push( child );
2236
2237 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
2238 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
2239
2240 child = m_TreeProject->GetNextChild( current, cookie );
2241 }
2242 }
2243 }
2244
2245 if( !m_gitCurrentBranchName.empty() )
2246 {
2247 wxTreeItemId kid = m_TreeProject->GetRootItem();
2248 PROJECT_TREE_ITEM* rootItem = kid.IsOk() ? GetItemIdData( kid ) : nullptr;
2249
2250 if( rootItem )
2251 {
2252 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
2253 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
2254 m_gitIconsInitialized = true;
2255 }
2256 }
2257
2258 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
2259}
2260
2261
2263{
2264 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
2265
2266 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
2267
2268 if( !lock.owns_lock() )
2269 {
2270 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
2271 return;
2272 }
2273
2274 if( !m_TreeProject )
2275 {
2276 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
2277 return;
2278 }
2279
2280 wxTreeItemId kid = m_TreeProject->GetRootItem();
2281
2282 if( !kid.IsOk() )
2283 return;
2284
2285 // Collect a map to easily set the state of each item
2286 m_gitTreeCache.clear();
2287 std::stack<wxTreeItemId> items;
2288 items.push( kid );
2289
2290 while( !items.empty() )
2291 {
2292 kid = items.top();
2293 items.pop();
2294
2295 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
2296
2297 if( !nextItem )
2298 continue;
2299
2300 wxString gitAbsPath = nextItem->GetFileName();
2301#ifdef _WIN32
2302 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2303#endif
2304 m_gitTreeCache[gitAbsPath] = kid;
2305
2306 wxTreeItemIdValue cookie;
2307 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2308
2309 while( child.IsOk() )
2310 {
2311 items.push( child );
2312 child = m_TreeProject->GetNextChild( kid, cookie );
2313 }
2314 }
2315}
2316
2317
2318void PROJECT_TREE_PANE::updateGitStatusIconMap( std::shared_ptr<KIGIT_COMMON> aGitCommon,
2319 const wxString& aProjectFullName )
2320{
2321 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2322#if defined( _WIN32 )
2324
2325 if( refresh != 0 && KIPLATFORM::ENV::IsNetworkPath( wxPathOnly( aProjectFullName ) ) )
2326 {
2327 // Need to treat windows network paths special here until we get the samba bug fixed
2328 // https://github.com/wxWidgets/wxWidgets/issues/18953
2329 CallAfter(
2330 [this, refresh]()
2331 {
2332 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2333 } );
2334 }
2335
2336#endif
2337
2338 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2339 return;
2340
2341 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2342 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2343
2344 if( !lock1.owns_lock() || !lock2.owns_lock() )
2345 {
2346 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2347 return;
2348 }
2349
2350 // The caller pinned the session on the UI thread, so a concurrent release cannot
2351 // free it under us.
2352 if( !aGitCommon || !aGitCommon->GetRepo() )
2353 {
2354 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2355 return;
2356 }
2357
2358 // Acquire the git action mutex to synchronize with EmptyTreePrj() shutdown.
2359 // This ensures the repository isn't freed while we're using it.
2360 std::unique_lock<std::mutex> gitLock = aGitCommon->TryLockAction();
2361
2362 if( !gitLock.owns_lock() )
2363 {
2364 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire git action mutex" ) );
2365 return;
2366 }
2367
2368 // Check if cancellation was requested (e.g., during shutdown)
2369 if( aGitCommon->IsShuttingDown() )
2370 {
2371 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Cancelled" ) );
2372 return;
2373 }
2374
2375 GIT_STATUS_HANDLER statusHandler( aGitCommon.get() );
2376
2377 // Set up pathspec for project files. The name is passed in from the UI thread
2378 // because Prj() must not be read here, the project can be unloaded concurrently.
2379 wxFileName rootFilename( aProjectFullName );
2380 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2381
2382 wxFileName relative = rootFilename;
2383 relative.MakeRelativeTo( repoWorkDir );
2384 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2385
2386#ifdef _WIN32
2387 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2388#endif
2389
2390 // Get file status
2391 auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr );
2392 auto [localChanges, remoteChanges] = aGitCommon->GetDifferentFiles();
2393 statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap );
2394
2395 bool updated = false;
2396
2397 // Update status icons based on file status
2398 for( const auto& [absPath, fileStatus] : fileStatusMap )
2399 {
2400 auto iter = m_gitTreeCache.find( absPath );
2401 if( iter == m_gitTreeCache.end() )
2402 {
2403 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2404 continue;
2405 }
2406
2407 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status );
2408 if( inserted || it->second != fileStatus.status )
2409 updated = true;
2410 it->second = fileStatus.status;
2411 }
2412
2413 // Get the current branch name
2414 wxString branchName = statusHandler.GetCurrentBranchName();
2415
2416 if( branchName != m_gitCurrentBranchName )
2417 updated = true;
2418
2419 m_gitCurrentBranchName = branchName;
2420
2421 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2422
2423 // Update UI if icons changed
2424 if( updated || !m_gitIconsInitialized )
2425 {
2426 CallAfter(
2427 [this, aGitCommon]()
2428 {
2430
2431 // The docked panels read the same status, so they are stale until they
2432 // are told. Without this a file edited outside KiCad shows as changed in
2433 // the tree while the panel still says there is nothing to commit.
2434 aGitCommon->NotifyRefreshListeners();
2435 } );
2436 }
2437}
2438
2439
2440void PROJECT_TREE_PANE::onRunSelectedJobsFile( wxCommandEvent& event )
2441{
2442}
2443
2444
2445void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
2446{
2447 wxLogTrace( traceGit, "onGitSyncTimer" );
2448 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
2449
2450 if( !gitSettings.enableGit || gitSettings.updatInterval <= 0 || !m_TreeProject )
2451 return;
2452
2453 // Skip the tick rather than stack a second fetch. Assigning over a running std::async
2454 // future blocks in its destructor, here on the UI thread.
2456 {
2457 wxLogTrace( traceGit, "onGitSyncTimer: previous fetch still running, skipping" );
2458 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
2459 return;
2460 }
2461
2462 // Pin the session on the UI thread, so a concurrent release cannot free it while
2463 // PerformFetch() is inside it.
2464 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
2465
2466 // Never the shared compute pool. A libgit2 network read parked there stalls DRC, zone
2467 // filling and library loading.
2468 m_gitSyncTask = std::async( std::launch::async,
2469 [this, common]()
2470 {
2471 if( !common->GetRepo() )
2472 {
2473 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
2474 return;
2475 }
2476
2477 // Check if cancellation was requested (e.g., during shutdown)
2478 if( common->IsShuttingDown() )
2479 {
2480 wxLogTrace( traceGit, "onGitSyncTimer: Cancelled" );
2481 return;
2482 }
2483
2484 GIT_PULL_HANDLER handler( common.get() );
2485 bool fetched = handler.PerformFetch();
2486 std::optional<KIGIT_FAILURE> kind = handler.GetFailureKind();
2487 wxString summary = handler.PopError().summary;
2488
2489 // Only schedule the follow-up work if not cancelled
2490 if( !common->IsShuttingDown() )
2491 CallAfter(
2492 [this, common, fetched, kind, summary]()
2493 {
2494 // The project may have closed or changed while queued
2495 if( common->IsShuttingDown() || common != gitCommon() )
2496 return;
2497
2498 reportGitSyncResult( fetched, kind, summary );
2500
2501 // The panels show whether the remote is ahead, and the fetch
2502 // just moved the remote refs.
2503 common->NotifyRefreshListeners();
2504 } );
2505 } );
2506
2507 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
2508 // We store the timer interval in minutes but wxTimer uses milliseconds
2509 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
2510}
2511
2512
2513void PROJECT_TREE_PANE::reportGitSyncResult( bool aFetched, std::optional<KIGIT_FAILURE> aKind,
2514 const wxString& aSummary )
2515{
2516 KISTATUSBAR* statusBar = dynamic_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
2517
2518 if( aFetched )
2519 {
2521 return;
2522 }
2523
2524 // A busy repository or a closing project is not something to warn about
2525 if( !statusBar || !aKind || *aKind == KIGIT_FAILURE::CANCELLED )
2526 return;
2527
2528 // The timer fires every few minutes, and the same failure each time would bury every other warning. The list
2529 // itself is the record, so anything that clears it lets the next failure show again
2530 std::map<wxString, std::vector<KI_ERROR>> shown = statusBar->GetWarningMessages();
2531
2532 for( const KI_ERROR& existing : shown[KIGIT::SYNC_WARNING_SOURCE] )
2533 {
2534 if( existing.GetTitle() == aSummary )
2535 return;
2536 }
2537
2538 KI_ERROR warning;
2539 warning.SetTitle( aSummary );
2541 statusBar->AddWarningMessages( KIGIT::SYNC_WARNING_SOURCE, { warning } );
2542}
2543
2544
2546{
2547 if( !m_TreeProject )
2548 return;
2549
2550 // Pin the session on the UI thread, so a concurrent release cannot free it while
2551 // the status worker is using it.
2552 std::shared_ptr<KIGIT_COMMON> common = gitCommon();
2553
2554 if( common->IsShuttingDown() )
2555 return;
2556
2557 // See onGitSyncTimer: overwriting a still-running task's future abandons a task that holds
2558 // a raw pointer to this pane, and the destructor would no longer wait for it.
2560 return;
2561
2564
2565 wxString projectFullName = Prj().GetProjectFullName();
2566
2567 m_gitStatusIconTask = tp.submit_task(
2568 [this, common, projectFullName]()
2569 {
2570 updateGitStatusIconMap( common, projectFullName );
2571 } );
2572}
2573
2574void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
2575{
2576 wxLogTrace( traceGit, "onGitStatusTimer" );
2577
2578 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2579 return;
2580
2582}
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:100
BITMAP_STORE * GetBitmapStore()
Definition bitmap.cpp:88
@ 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.
void SetRepoSSHPath(const wxString &aPath)
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
void SetPassword(const wxString &aPassword)
wxString GetRepoSSHPath() const
void SetUsername(const wxString &aUsername)
void SetSkipButtonLabel(const wxString &aLabel)
wxString GetBranchName() const
int ShowModal() override
virtual bool IsLibraryAvailable()=0
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< PROGRESS_REPORTER > aProgressReporter)
bool PerformFetch(bool aSkipLock=false)
std::map< wxString, FileStatus > GetFileStatus(const wxString &aPathspec=wxEmptyString, bool aChangedOnly=false)
Get detailed file status for all files in the specified path.
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.
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 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.
The project's git session.
void SetUsername(const wxString &aUsername)
git_repository * GetRepo() const
void SetProjectDir(const wxString &aProjectDir)
Keep symlinks in the repository path so Git status paths match the project tree.
static constexpr std::chrono::milliseconds ACTION_LOCK_WAIT
Long enough for a status read to finish, short enough that a window does not look hung.
wxString GetUsername() const
void SetShuttingDown(bool aCancel)
Stop the operation running now and keep later ones from starting.
static wxString GetLastGitError()
libgit2's last message, without credentials from any URL it quotes, so it is safe to show or trace.
static REMOTE_URL ParseRemoteURL(const wxString &aURL)
wxString GetPassword()
std::vector< KIGIT_BRANCH_INFO > GetBranches() const
Local and remote branches with their tip commits.
void SetRemote(const wxString &aRemote)
KIGIT_ERROR_MESSAGE PopError()
Take the newest error with its details. Both are empty when none is queued.
Read repository state without blocking the UI.
std::optional< KIGIT::HOST_KEY_FAILURE > GetHostKeyFailure() const
The host key this operation's transfer refused, for the UI to offer to trust or to report.
git_repository * GetRepo() const
Get a pointer to the git repository.
void SetActionLockWait(std::chrono::milliseconds aWait)
A UI caller retrying confirmed input waits this long for a background read instead of failing at once...
std::optional< KIGIT_FAILURE > GetFailureKind() const
The kind of the last failure ReportTransferFailure classified, or none.
bool WasAuthFailure() const
Set when the server refused every credential offered, so the caller can prompt and retry.
KISTATUSBAR is a wxStatusBar suitable for Kicad manager.
Definition kistatusbar.h:50
void AddWarningMessages(const wxString &aSource, const wxString &aMessages)
Add warning/error messages (not thread-safe, use the std::vector<KI_ERROR> variant from other threads...
std::map< wxString, std::vector< KI_ERROR > > GetWarningMessages() const
Get a copy of all stored warning/error messages, grouped by source and sorted by source name for stab...
Holds a structured error message.
Definition ki_error.h:37
KI_ERROR & SetSeverity(SEVERITY aSeverity)
Definition ki_error.h:75
KI_ERROR & SetTitle(const wxString &aTitle)
Definition ki_error.h:57
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:562
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition pgm_base.cpp:233
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)
Window to display the tree files.
PROJECT_TREE_ITEM * m_selectedItem
void switchGitBranch(std::shared_ptr< KIGIT_COMMON > aCommon, const wxString &aBranchName, bool aCreate)
void syncGitListener()
Follow the project's git session with a refresh listener, so operations started from the version cont...
std::unordered_map< wxString, wxTreeItemId > m_gitTreeCache
std::map< wxTreeItemId, KIGIT_COMMON::GIT_STATUS > m_gitStatusIcons
void onGitInitializeProject(wxCommandEvent &event)
Initialize a new git repository in the current project directory.
git_repository * gitRepo() const
void onDeleteFile(wxCommandEvent &event)
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()
Return the item data from item currently selected (highlighted)
void onOpenDirectory(wxCommandEvent &event)
Handles the right-click menu for opening a directory in the current system file browser.
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 WaitForGitOperations()
Give a running background git operation a chance to finish, asking the user to keep waiting or abando...
void onRight(wxTreeEvent &Event)
Called on a right click on an item.
void onSelect(wxTreeEvent &Event)
Called on a double click on an item.
PROJECT_TREE * m_TreeProject
PROJECT_TREE_PANE(KICAD_MANAGER_FRAME *parent)
std::weak_ptr< KIGIT_COMMON > m_gitMenuSession
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.
std::weak_ptr< KIGIT_COMMON > m_gitListenerSession
std::vector< wxString > m_gitMenuBranches
Preserve the shortcut targets while the context menu runs its event loop.
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.
static wxString GetFileExt(TREE_FILE_TYPE type)
friend class PROJECT_TREE_ITEM
wxTreeItemId findSubdirTreeItem(const wxString &aSubDir)
Search for the item in tree project which is the node of the subdirectory aSubDir.
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 onGitStatusTimer(wxTimerEvent &event)
void shutdownFileWatcher()
Shutdown the file watcher.
std::mutex m_gitTreeCacheMutex
std::shared_ptr< KIGIT_COMMON > gitCommon() const
The current project's git session, created empty on first use.
void updateGitStatusIconMap(std::shared_ptr< KIGIT_COMMON > aGitCommon, const wxString &aProjectFullName)
This is a threaded call that will change the map of git status icons for use in the main thread.
wxTreeItemId addItemToProjectTree(const wxString &aName, const wxTreeItemId &aParent, std::vector< wxString > *aProjectNames, bool aRecurse)
Add the file or directory aName to the project tree.
void onGitRemoteSettings(wxCommandEvent &event)
Configure (or change) the default remote on an already-initialized repository.
void onOpenSelectedFileWithTextEditor(wxCommandEvent &event)
Call the text editor to open the selected file in the tree project.
void updateGitStatusIcons()
Updates the icons shown in the tree project to reflect the current git status.
std::future< void > m_gitStatusIconTask
PROJECT_TREE_ITEM * GetItemIdData(wxTreeItemId aId)
Return the item data corresponding to a wxTreeItemId identifier.
std::unique_ptr< KIGIT_QUERY > m_branchQuery
Owns the branch-switch preflight and suppresses callbacks when the project closes.
void reportGitSyncResult(bool aFetched, std::optional< KIGIT_FAILURE > aKind, const wxString &aSummary)
Background fetches have no window to answer, so each failure goes to the status bar's warnings once,...
void onGitSwitchBranch(wxCommandEvent &event)
Switch to a different branch in the git repository.
void onCreateNewDirectory(wxCommandEvent &event)
Creates a new subdirectory inside the current kicad project directory the user is prompted to enter a...
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)
Rename the selected file or directory in the tree project.
void releaseGitRepo()
Releases the git repository handle, deferring the free to the orphan registry while a background git ...
std::vector< wxString > m_filters
PROJECT_TREE This is the class to show (as a tree) the files in the project directory.
Container for project specific data.
Definition project.h:63
virtual std::shared_ptr< KIGIT_COMMON > GetGitCommon() const
This project's Git session, created empty on first use.
Definition project.cpp:84
virtual const wxString GetProjectFullName() const
Return the full path and name of the project.
Definition project.cpp:270
virtual bool AttachGitRepository()
Find the Git repository containing the project directory and bind it to the session.
Definition project.cpp:93
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition project.h:207
virtual void ReleaseGitCommon()
Safely release the session's repository handle and drop the session.
Definition project.cpp:165
A scoped application of a wxLog target.
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
This file is part of the common library.
#define _(s)
wxDECLARE_EVENT(wxEVT_REFRESH_CUSTOM_COMMAND, wxCommandEvent)
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:166
GIT_BACKEND * GetGitBackend()
InitResult
void ProjectClearOfUncommittedWork(EDA_BASE_FRAME *aFrame, wxWindow *aParent, KIGIT_QUERY &aQuery, std::function< void(const GIT_WORKTREE_STATUS &)> aReady)
Check the working tree and Git LFS on a worker, then run aReady on the UI thread when the operation c...
void SwitchProjectToBranch(EDA_BASE_FRAME *aFrame, wxWindow *aParent, const wxString &aBranch, KIGIT_QUERY &aQuery, std::function< void()> aFinished)
Check the working tree on a worker, then confirm switching to aBranch and recheck unsaved editors.
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 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 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 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.
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition idf_helpers.h:37
PROJECT & Prj()
Definition kicad.cpp:737
@ ID_PROJECT_TREE
Definition kicad_id.h:32
@ ID_LEFT_FRAME
Definition kicad_id.h:31
EVT_MENU(ID_COMPARE_PROJECT_BRANCHES, KICAD_MANAGER_FRAME::OnCompareProjectBranches) KICAD_MANAGER_FRAME
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:79
void ClearSyncWarnings(wxWindow *aWindow)
Clear the background fetch's warnings from the frame holding aWindow.
void SortBranchesForDisplay(std::vector< KIGIT_BRANCH_INFO > &aBranches)
Put the current branch first, then local and remote branches with the newest tips first.
wxString BranchNameError(const wxString &aName)
Empty when aName is a branch name git would accept, otherwise why it would not, in the words to show ...
constexpr const wxChar * SYNC_WARNING_SOURCE
The status bar source under which background fetch failures are listed.
std::unique_lock< std::mutex > WaitForActionLock(KIGIT_COMMON &aCommon, wxWindow *aParent)
TryLockAction that first waits a moment for a status read or fetch to let go.
bool PromptToTrustHost(wxWindow *aParent, const HOST_KEY_FAILURE &aFailure, bool aRetriedAfterTrust)
Ask whether to trust a server seen for the first time, and remember its key when the user agrees.
std::unique_lock< std::mutex > TryLockAction(KIGIT_COMMON &aCommon, wxWindow *aParent)
Try the action lock and show a busy message if it is unavailable.
bool PromptForCredentials(wxWindow *aParent, KIGIT_COMMON *aCommon, PROJECT *aProject=nullptr, const wxString &aExplanation=wxEmptyString)
Ask the user for the credentials of the session's remote and apply them to the session.
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.
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:96
STL namespace.
PGM_BASE & Pgm()
The global program "get" accessor.
#define NAMELESS_PROJECT
default name for nameless projects
Definition project.h:40
static bool gitTaskStillRunning(const std::future< void > &aTask)
project_tree_ids
The frame that shows the tree list of files and subdirectories inside the working directory.
@ ID_PROJECT_OPEN_DIR
@ ID_JOBS_RUN
@ ID_PROJECT_SWITCH_TO_OTHER
@ ID_PROJECT_NEWDIR
@ ID_PROJECT_RENAME
@ ID_GIT_SWITCH_QUICK5
@ ID_GIT_REMOVE_VCS
@ ID_PROJECT_TXTEDIT
@ ID_GIT_SWITCH_QUICK2
@ ID_GIT_SWITCH_QUICK4
@ ID_GIT_SWITCH_QUICK3
@ ID_GIT_SWITCH_BRANCH
@ ID_GIT_SWITCH_QUICK1
@ ID_GIT_INITIALIZE_PROJECT
@ ID_PROJECT_DELETE
@ ID_GIT_REMOTE_SETTINGS
static bool promptForGitCredentials(wxWindow *aParent, KIGIT_COMMON *aCommon, const wxString &aExplanation)
static void saveAcceptedRemoteSettings(PROJECT &aProject, const RemoteConfig &aConfig)
std::vector< wxString > getProjects(const wxDir &dir)
static const wxChar * s_allowedExtensionsToList[]
@ RPT_SEVERITY_WARNING
#define wxFileSystemWatcher
Repository state used before an operation that requires a clean working tree.
A summary the user can act on, with the raw text that explains it kept apart for the details.
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:27
wxLogTrace helper definitions.
TREE_FILE_TYPE
Definition of file extensions used in Kicad.
#define PR_NO_ABORT