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
38#include <advanced_config.h>
39#include <bitmaps.h>
40#include <bitmap_store.h>
41#include <confirm.h>
44#include <gestfich.h>
45#include <macros.h>
46#include <trace_helpers.h>
49#include <core/kicad_algo.h>
50#include <paths.h>
52#include <scoped_set_reset.h>
53#include <string_utils.h>
54#include <thread_pool.h>
55#include <launch_ext.h>
56#include <wx/dcclient.h>
57#include <wx/progdlg.h>
58#include <wx/settings.h>
59
75
77
78#include "project_tree_item.h"
79#include "project_tree.h"
80#include "pgm_kicad.h"
81#include "kicad_id.h"
82#include "kicad_manager_frame.h"
83
84#include "project_tree_pane.h"
85#include <widgets/kistatusbar.h>
86
87#include <kiplatform/io.h>
88#include <kiplatform/secrets.h>
89
90
91/* Note about the project tree build process:
92 * Building the project tree can be *very* long if there are a lot of subdirectories in the
93 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
94 * user's home directory.
95 * So the tree project is built "on demand":
96 * First the tree is built from the current directory and shows files and subdirs.
97 * > First level subdirs trees are built (i.e subdirs contents are not read)
98 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
99 * populated on the fly.
100 */
101
102// list of files extensions listed in the tree project window
103// Add extensions in a compatible regex format to see others files types
104static const wxChar* s_allowedExtensionsToList[] =
105{
106 wxT( "^.*\\.pro$" ),
107 wxT( "^.*\\.kicad_pro$" ),
108 wxT( "^.*\\.pdf$" ),
109 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
110 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
111 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
112 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
113 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
114 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
115 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
116 wxT( "^.*\\.net$" ), // pcbnew netlist file
117 wxT( "^.*\\.cir$" ), // Spice netlist file
118 wxT( "^.*\\.lib$" ), // Legacy schematic library file
119 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
120 wxT( "^.*\\.txt$" ), // Text files
121 wxT( "^.*\\.md$" ), // Markdown files
122 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
123 wxT( "^.*\\.gbr$" ), // Gerber file
124 wxT( "^.*\\.gbrjob$" ), // Gerber job file
125 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
126 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
127 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
128 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
129 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
130 wxT( "^.*\\.odt$" ),
131 wxT( "^.*\\.htm$" ),
132 wxT( "^.*\\.html$" ),
133 wxT( "^.*\\.rpt$" ), // Report files
134 wxT( "^.*\\.csv$" ), // Report files in comma separated format
135 wxT( "^.*\\.pos$" ), // Footprint position files
136 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
137 wxT( "^.*\\.drl$" ), // Excellon drill files
138 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
139 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
140 wxT( "^.*\\.svg$" ), // SVG print/plot files
141 wxT( "^.*\\.ps$" ), // PostScript plot files
142 wxT( "^.*\\.zip$" ), // Zip archive files
143 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
144 nullptr // end of list
145};
146
147
153
154
156{
157 ID_PROJECT_TXTEDIT = 8700, // Start well above wxIDs
163
164 ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project
165 ID_GIT_CLONE_PROJECT, // Clone a project from a remote repository
166 ID_GIT_COMMIT_PROJECT, // Commit all files in the project
167 ID_GIT_COMMIT_FILE, // Commit a single file
168 ID_GIT_SYNC_PROJECT, // Sync the project with the remote repository (pull and push -- same as Update)
169 ID_GIT_FETCH, // Fetch the remote repository (without merging -- this is the same as Refresh)
170 ID_GIT_PUSH, // Push the local repository to the remote repository
171 ID_GIT_PULL, // Pull the remote repository to the local repository
172 ID_GIT_RESOLVE_CONFLICT, // Present the user with a resolve conflicts dialog (ours/theirs/merge)
173 ID_GIT_REVERT_LOCAL, // Revert the local repository to the last commit
174 ID_GIT_COMPARE, // Compare the current project to a different branch or commit in the git repository
175 ID_GIT_REMOVE_VCS, // Remove the git repository data from the project directory (rm .git)
176 ID_GIT_ADD_TO_INDEX, // Add a file to the git index
177 ID_GIT_REMOVE_FROM_INDEX, // Remove a file from the git index
178 ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch
179 ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch
180 ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch
181 ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch
182 ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch
183 ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch
184
186};
187
188
189BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
190 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
191 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
192 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
199
215
217
218 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
219 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
220END_EVENT_TABLE()
221
222
223wxDECLARE_EVENT( UPDATE_ICONS, wxCommandEvent );
224
226 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxTAB_TRAVERSAL )
227{
228 m_Parent = parent;
229 m_TreeProject = nullptr;
230 m_isRenaming = false;
231 m_selectedItem = nullptr;
232 m_watcherNeedReset = false;
233 m_gitLastError = GIT_ERROR_NONE;
234 m_watcher = nullptr;
235 m_gitIconsInitialized = false;
236
237 Bind( wxEVT_FSWATCHER,
238 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
239
240 Bind( wxEVT_SYS_COLOUR_CHANGED,
241 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
242
243 m_gitSyncTimer.SetOwner( this );
244 m_gitStatusTimer.SetOwner( this );
245 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
246 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
247 /*
248 * Filtering is now inverted: the filters are actually used to _enable_ support
249 * for a given file type.
250 */
251 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
252 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
253
254 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
255
257}
258
259
261{
262 Unbind( wxEVT_FSWATCHER,
263 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
264 Unbind( wxEVT_SYS_COLOUR_CHANGED,
265 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
266
267 m_gitSyncTimer.Stop();
268 m_gitStatusTimer.Stop();
269 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
270 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
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";
390
394 case TREE_FILE_TYPE::DIRECTORY: break;
395 }
396
397 return wxEmptyString;
398}
399
400
401std::vector<wxString> getProjects( const wxDir& dir )
402{
403 std::vector<wxString> projects;
404 wxString dir_filename;
405 bool haveFile = dir.GetFirst( &dir_filename );
406
407 while( haveFile )
408 {
409 wxFileName file( dir_filename );
410
411 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
412 || file.GetExt() == FILEEXT::ProjectFileExtension )
413 projects.push_back( file.GetName() );
414
415 haveFile = dir.GetNext( &dir_filename );
416 }
417
418 return projects;
419}
420
421
422
423
424
425wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
426 const wxTreeItemId& aParent,
427 std::vector<wxString>* aProjectNames,
428 bool aRecurse )
429{
431 wxFileName fn( aName );
432
433 if( KIPLATFORM::IO::IsFileHidden( aName ) )
434 return wxTreeItemId();
435
436 if( wxDirExists( aName ) )
437 {
439 }
440 else
441 {
442 // Filter
443 wxRegEx reg;
444 bool addFile = false;
445
446 for( const wxString& m_filter : m_filters )
447 {
448 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
449 wxString::Format( "Regex %s failed to compile.", m_filter ) );
450
451 if( reg.Matches( aName ) )
452 {
453 addFile = true;
454 break;
455 }
456 }
457
458 if( !addFile )
459 return wxTreeItemId();
460
461 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
462 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
463 {
464 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
465
466 if( ext == wxT( "" ) )
467 continue;
468
469 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ), wxRE_ICASE )
470 && reg.Matches( aName ) )
471 {
472 type = (TREE_FILE_TYPE) i;
473 break;
474 }
475 }
476 }
477
478 wxString file = wxFileNameFromPath( aName );
479 wxFileName currfile( file );
480 wxFileName project( m_Parent->GetProjectFileName() );
481 bool showAllSchematics = m_TreeProject->GetGitRepo() != nullptr;
482
483 // Ignore legacy projects with the same name as the current project
484 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
485 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
486 {
487 return wxTreeItemId();
488 }
489
490 if( !showAllSchematics && ( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
491 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) ) )
492 {
493 if( aProjectNames )
494 {
495 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
496 return wxTreeItemId();
497 }
498 else
499 {
500 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
501 wxDir parentDir( parentTreeItem->GetDir() );
502 std::vector<wxString> projects = getProjects( parentDir );
503
504 if( !alg::contains( projects, currfile.GetName() ) )
505 return wxTreeItemId();
506 }
507 }
508
509 // also check to see if it is already there.
510 wxTreeItemIdValue cookie;
511 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
512
513 while( kid.IsOk() )
514 {
515 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
516
517 if( itemData && itemData->GetFileName() == aName )
518 return itemData->GetId(); // well, we would have added it, but it is already here!
519
520 kid = m_TreeProject->GetNextChild( aParent, cookie );
521 }
522
523 // Only show current files if both legacy and current files are present
528 {
529 kid = m_TreeProject->GetFirstChild( aParent, cookie );
530
531 while( kid.IsOk() )
532 {
533 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
534
535 if( itemData )
536 {
537 wxFileName fname( itemData->GetFileName() );
538
539 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
540 {
541 switch( type )
542 {
544 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
545 return wxTreeItemId();
546
547 break;
548
550 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
551 return wxTreeItemId();
552
553 break;
554
556 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
557 m_TreeProject->Delete( kid );
558
559 break;
560
562 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
563 m_TreeProject->Delete( kid );
564
565 break;
566
567 default:
568 break;
569 }
570 }
571 }
572
573 kid = m_TreeProject->GetNextChild( aParent, cookie );
574 }
575 }
576
577 // Append the item (only appending the filename not the full path):
578 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
579 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
580
581 m_TreeProject->SetItemData( newItemId, data );
582 data->SetState( 0 );
583
584 // Mark root files (files which have the same aName as the project)
585 wxString fileName = currfile.GetName().Lower();
586 wxString projName = project.GetName().Lower();
587
588 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
589 data->SetRootFile( true );
590
591#ifndef __WINDOWS__
592 bool subdir_populated = false;
593#endif
594
595 // This section adds dirs and files found in the subdirs
596 // in this case AddFile is recursive, but for the first level only.
597 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
598 {
599 wxDir dir( aName );
600
601 if( dir.IsOpened() ) // protected dirs will not open properly.
602 {
603 std::vector<wxString> projects = getProjects( dir );
604 wxString dir_filename;
605 bool haveFile = dir.GetFirst( &dir_filename );
606
607 data->SetPopulated( true );
608
609#ifndef __WINDOWS__
610 subdir_populated = aRecurse;
611#endif
612
613 while( haveFile )
614 {
615 // Add name in tree, but do not recurse
616 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
617 addItemToProjectTree( path, newItemId, &projects, false );
618
619 haveFile = dir.GetNext( &dir_filename );
620 }
621 }
622
623 // Sort filenames by alphabetic order
624 m_TreeProject->SortChildren( newItemId );
625 }
626
627#ifndef __WINDOWS__
628 if( subdir_populated )
629 m_watcherNeedReset = true;
630#endif
631
632 return newItemId;
633}
634
635
637{
638 std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
639 std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
641
642 while( tp.get_tasks_running() )
643 {
644 tp.wait_for( std::chrono::milliseconds( 250 ) );
645 }
646
647 m_gitStatusTimer.Stop();
648 m_gitSyncTimer.Stop();
649 m_gitTreeCache.clear();
650 m_gitStatusIcons.clear();
651
652 wxString pro_dir = m_Parent->GetProjectFileName();
653
654 if( !m_TreeProject )
655 m_TreeProject = new PROJECT_TREE( this );
656 else
657 m_TreeProject->DeleteAllItems();
658
659 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
660 return;
661
662 if( m_TreeProject->GetGitRepo() )
663 {
664 git_repository* repo = m_TreeProject->GetGitRepo();
666 m_TreeProject->SetGitRepo( nullptr );
667 m_gitIconsInitialized = false;
668 }
669
670 wxFileName fn = pro_dir;
671 bool prjReset = false;
672
673 if( !fn.IsOk() )
674 {
675 fn.Clear();
676 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
677 fn.SetName( NAMELESS_PROJECT );
679 prjReset = true;
680 }
681
682 bool prjOpened = fn.FileExists();
683
684 // Bind the git repository to the project tree (if it exists)
685 if( Pgm().GetCommonSettings()->m_Git.enableGit )
686 {
687 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
688
689 if( m_TreeProject->GetGitRepo() )
690 {
691 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
692 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
693 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
694 }
695 }
696
697 // We may have opened a legacy project, in which case GetProjectFileName will return the
698 // name of the migrated (new format) file, which may not have been saved to disk yet.
699 if( !prjOpened && !prjReset )
700 {
702 prjOpened = fn.FileExists();
703
704 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
706 }
707
708 // root tree:
709 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
710 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
711 m_TreeProject->SetItemBold( m_root, true );
712
713 // The main project file is now a JSON file
716
717 m_TreeProject->SetItemData( m_root, data );
718
719 // Now adding all current files if available
720 if( prjOpened )
721 {
722 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
723 wxDir dir( pro_dir );
724
725 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
726 {
727 std::vector<wxString> projects = getProjects( dir );
728 wxString filename;
729 bool haveFile = dir.GetFirst( &filename );
730
731 while( haveFile )
732 {
733 if( filename != fn.GetFullName() )
734 {
735 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
736
737 // Add items living in the project directory, and populate the item
738 // if it is a directory (sub directories will be not populated)
739 addItemToProjectTree( name, m_root, &projects, true );
740 }
741
742 haveFile = dir.GetNext( &filename );
743 }
744 }
745 }
746 else
747 {
748 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
749 }
750
751 m_TreeProject->Expand( m_root );
752
753 // Sort filenames by alphabetic order
754 m_TreeProject->SortChildren( m_root );
755
756 CallAfter(
757 [this] ()
758 {
759 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
760 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
761 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
762 } );
763}
764
765
767{
768 if( !m_TreeProject->GetGitRepo() )
769 return false;
770
771 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
772 return statusHandler.HasChangedFiles();
773}
774
775
776void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
777{
778 wxTreeItemId curr_item = Event.GetItem();
779
780 // Ensure item is selected (Under Windows right click does not select the item)
781 m_TreeProject->SelectItem( curr_item );
782
783 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
784 KIGIT_COMMON* git = m_TreeProject->GitCommon();
785 wxFileName prj_dir( Prj().GetProjectPath(), wxEmptyString );
786 wxFileName git_dir( git->GetGitRootDirectory(), wxEmptyString );
787 prj_dir.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_CASE | wxPATH_NORM_DOTS
788 | wxPATH_NORM_ENV_VARS | wxPATH_NORM_TILDE );
789 git_dir.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_CASE | wxPATH_NORM_DOTS
790 | wxPATH_NORM_ENV_VARS | wxPATH_NORM_TILDE );
791 wxString prj_name = prj_dir.GetFullPath();
792 wxString git_name = git_dir.GetFullPath();
793
794 bool can_switch_to_project = true;
795 bool can_create_new_directory = true;
796 bool can_open_this_directory = true;
797 bool can_edit = true;
798 bool can_rename = true;
799 bool can_delete = true;
800 bool run_jobs = false;
801
802 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
803 bool vcs_can_commit = hasChangedFiles();
804 bool vcs_can_init = !vcs_has_repo;
805 bool vcs_can_remove = vcs_has_repo && git_name.StartsWith( prj_name ); // This means the .git is a subdirectory of the project
806 bool vcs_can_fetch = vcs_has_repo && git->HasPushAndPullRemote();
807 bool vcs_can_push = vcs_can_fetch && git->HasLocalCommits();
808 bool vcs_can_pull = vcs_can_fetch;
809 bool vcs_can_switch = vcs_has_repo;
810 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
811
812 // Check if the libgit2 library is available via backend
813 bool libgit_init = GetGitBackend() && GetGitBackend()->IsLibraryAvailable();
814
815 vcs_menu &= libgit_init;
816
817 if( selection.size() == 0 )
818 return;
819
820 // Remove things that don't make sense for multiple selections
821 if( selection.size() != 1 )
822 {
823 can_switch_to_project = false;
824 can_create_new_directory = false;
825 can_rename = false;
826 }
827
828 for( PROJECT_TREE_ITEM* item : selection )
829 {
830 // Check for empty project
831 if( !item )
832 {
833 can_switch_to_project = false;
834 can_edit = false;
835 can_rename = false;
836 continue;
837 }
838
839 can_delete = item->CanDelete();
840 can_rename = item->CanRename();
841
842 switch( item->GetType() )
843 {
846 can_rename = false;
847
848 if( item->GetId() == m_TreeProject->GetRootItem() )
849 {
850 can_switch_to_project = false;
851 }
852 else
853 {
854 can_create_new_directory = false;
855 can_open_this_directory = false;
856 }
857 break;
858
860 can_switch_to_project = false;
861 can_edit = false;
862 break;
863
866 can_edit = false;
867 can_switch_to_project = false;
868 can_create_new_directory = false;
869 can_open_this_directory = false;
870 break;
871
873 run_jobs = true;
874 can_edit = false;
876
880
881 default:
882 can_switch_to_project = false;
883 can_create_new_directory = false;
884 can_open_this_directory = false;
885
886 break;
887 }
888 }
889
890 wxMenu popup_menu;
891 wxString text;
892 wxString help_text;
893
894 if( can_switch_to_project )
895 {
896 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
897 _( "Close all editors, and switch to the selected project" ),
899 popup_menu.AppendSeparator();
900 }
901
902 if( can_create_new_directory )
903 {
904 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
905 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
906 }
907
908 if( can_open_this_directory )
909 {
910 if( selection.size() == 1 )
911 {
912#ifdef __APPLE__
913 text = _( "Reveal in Finder" );
914 help_text = _( "Reveals the directory in a Finder window" );
915#else
916 text = _( "Open Directory in File Explorer" );
917 help_text = _( "Opens the directory in the default system file manager" );
918#endif
919 }
920 else
921 {
922#ifdef __APPLE__
923 text = _( "Reveal in Finder" );
924 help_text = _( "Reveals the directories in a Finder window" );
925#else
926 text = _( "Open Directories in File Explorer" );
927 help_text = _( "Opens the directories in the default system file manager" );
928#endif
929 }
930
931 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
933 }
934
935 if( can_edit )
936 {
937 if( selection.size() == 1 )
938 help_text = _( "Open the file in a Text Editor" );
939 else
940 help_text = _( "Open files in a Text Editor" );
941
942 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
944 }
945
946 if( run_jobs && selection.size() == 1 )
947 {
948 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
950 }
951
952 if( can_rename )
953 {
954 if( selection.size() == 1 )
955 {
956 text = _( "Rename File..." );
957 help_text = _( "Rename file" );
958 }
959 else
960 {
961 text = _( "Rename Files..." );
962 help_text = _( "Rename files" );
963 }
964
965 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
967 }
968
969 if( can_delete )
970 {
971 if( selection.size() == 1 )
972 help_text = _( "Delete the file and its content" );
973 else
974 help_text = _( "Delete the files and their contents" );
975
976 if( can_switch_to_project
977 || can_create_new_directory
978 || can_open_this_directory
979 || can_edit
980 || can_rename )
981 {
982 popup_menu.AppendSeparator();
983 }
984
985#ifdef __WINDOWS__
986 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
988#else
989 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
991#endif
992 }
993
994 if( vcs_menu )
995 {
996 wxMenu* vcs_submenu = new wxMenu();
997 wxMenu* branch_submenu = new wxMenu();
998 wxMenuItem* vcs_menuitem = nullptr;
999
1000 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
1001 _( "Add Project to Version Control..." ),
1002 _( "Initialize a new repository" ) );
1003 vcs_menuitem->Enable( vcs_can_init );
1004
1005
1006 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
1007 _( "Commit changes to the local repository" ) );
1008 vcs_menuitem->Enable( vcs_can_commit );
1009
1010 vcs_menuitem = vcs_submenu->Append( ID_GIT_PUSH, _( "Push" ),
1011 _( "Push committed local changes to remote repository" ) );
1012 vcs_menuitem->Enable( vcs_can_push );
1013
1014 vcs_menuitem = vcs_submenu->Append( ID_GIT_PULL, _( "Pull" ),
1015 _( "Pull changes from remote repository into local" ) );
1016 vcs_menuitem->Enable( vcs_can_pull );
1017
1018 vcs_submenu->AppendSeparator();
1019
1020 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
1021 _( "Commit changes to the local repository" ) );
1022 vcs_menuitem->Enable( vcs_can_commit );
1023
1024 vcs_submenu->AppendSeparator();
1025
1026 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
1027 // _( "Show changes between the repository and working tree" ) );
1028 // vcs_menuitem->Enable( vcs_can_diff );
1029
1030 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
1031
1032 // Skip the first one (that is the current branch)
1033 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
1034 {
1035 wxString msg = _( "Switch to branch " ) + branchNames[ii];
1036 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[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_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1045
1046 vcs_submenu->AppendSeparator();
1047
1048 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Remove Version Control" ),
1049 _( "Delete all version control files from the project directory." ) );
1050 vcs_menuitem->Enable( vcs_can_remove );
1051
1052 popup_menu.AppendSeparator();
1053 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1054 }
1055
1056 if( popup_menu.GetMenuItemCount() > 0 )
1057 PopupMenu( &popup_menu );
1058}
1059
1060
1062{
1063 wxString editorname = Pgm().GetTextEditor();
1064
1065 if( editorname.IsEmpty() )
1066 {
1067 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1068 return;
1069 }
1070
1071 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1072
1073 for( PROJECT_TREE_ITEM* item_data : tree_data )
1074 {
1075 wxString fullFileName = item_data->GetFileName();
1076
1077 if( !fullFileName.IsEmpty() )
1078 {
1079 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1080 }
1081 }
1082}
1083
1084
1085void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1086{
1087 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1088
1089 for( PROJECT_TREE_ITEM* item_data : tree_data )
1090 item_data->Delete();
1091}
1092
1093
1094void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1095{
1096 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1097 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1098
1099 // XXX: Unnecessary?
1100 if( tree_data.size() != 1 )
1101 return;
1102
1103 wxString buffer = m_TreeProject->GetItemText( curr_item );
1104 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1105 tree_data[0]->GetFileName() );
1106 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1107
1108 if( dlg.ShowModal() != wxID_OK )
1109 return; // canceled by user
1110
1111 buffer = dlg.GetValue();
1112 buffer.Trim( true );
1113 buffer.Trim( false );
1114
1115 if( buffer.IsEmpty() )
1116 return; // empty file name not allowed
1117
1118 tree_data[0]->Rename( buffer, true );
1119 m_isRenaming = true;
1120}
1121
1122
1123void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1124{
1125 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1126
1127 if( tree_data.size() != 1 )
1128 return;
1129
1130 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1131 // there will be more events at least on Windows in this frame that will steal focus from
1132 // any newly launched windows
1133 m_selectedItem = tree_data[0];
1134}
1135
1136
1137void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1138{
1139 // Idle executes once all other events finished processing. This makes it ideal to launch
1140 // a new window without starting Focus wars.
1141 if( m_watcherNeedReset )
1142 {
1143 m_selectedItem = nullptr;
1145 }
1146
1147 if( m_selectedItem != nullptr && m_TreeProject->GetRootItem().IsOk() )
1148 {
1149 // Make sure m_selectedItem still exists in the tree before activating it.
1150 std::vector<wxTreeItemId> validItemIds;
1151 m_TreeProject->GetItemsRecursively( m_TreeProject->GetRootItem(), validItemIds );
1152
1153 for( wxTreeItemId id : validItemIds )
1154 {
1155 if( GetItemIdData( id ) == m_selectedItem )
1156 {
1157 // Activate launches a window which may run the event loop on top of us and cause
1158 // onIdle to get called again, so be sure to null out m_selectedItem first.
1160 m_selectedItem = nullptr;
1161
1162 item->Activate( this );
1163 break;
1164 }
1165 }
1166 }
1167}
1168
1169
1170void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1171{
1172 wxTreeItemId itemId = Event.GetItem();
1173 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1174
1175 if( !tree_data )
1176 return;
1177
1178 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1179 return;
1180
1181 // explore list of non populated subdirs, and populate them
1182 wxTreeItemIdValue cookie;
1183 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1184
1185#ifndef __WINDOWS__
1186 bool subdir_populated = false;
1187#endif
1188
1189 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1190 {
1191 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1192
1193 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1194 continue;
1195
1196 if( itemData->IsPopulated() )
1197 continue;
1198
1199 wxString fileName = itemData->GetFileName();
1200 wxDir dir( fileName );
1201
1202 if( dir.IsOpened() )
1203 {
1204 std::vector<wxString> projects = getProjects( dir );
1205 wxString dir_filename;
1206 bool haveFile = dir.GetFirst( &dir_filename );
1207
1208 while( haveFile )
1209 {
1210 // Add name to tree item, but do not recurse in subdirs:
1211 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1212 addItemToProjectTree( name, kid, &projects, false );
1213
1214 haveFile = dir.GetNext( &dir_filename );
1215 }
1216
1217 itemData->SetPopulated( true ); // set state to populated
1218
1219#ifndef __WINDOWS__
1220 subdir_populated = true;
1221#endif
1222 }
1223
1224 // Sort filenames by alphabetic order
1225 m_TreeProject->SortChildren( kid );
1226 }
1227
1228#ifndef __WINDOWS__
1229 if( subdir_populated )
1230 m_watcherNeedReset = true;
1231#endif
1232}
1233
1234
1235std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1236{
1237 wxArrayTreeItemIds selection;
1238 std::vector<PROJECT_TREE_ITEM*> data;
1239
1240 m_TreeProject->GetSelections( selection );
1241
1242 for( const wxTreeItemId itemId : selection )
1243 {
1244 PROJECT_TREE_ITEM* item = GetItemIdData( itemId );
1245
1246 if( !item )
1247 {
1248 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast failed?" ) );
1249 continue;
1250 }
1251
1252 data.push_back( item );
1253 }
1254
1255 return data;
1256}
1257
1258
1260{
1261 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1262}
1263
1264
1265wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1266{
1267 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1268
1269 // If the subdir is the current working directory, return m_root
1270 // in main list:
1271 if( prj_dir == aSubDir )
1272 return m_root;
1273
1274 // The subdir is in the main tree or in a subdir: Locate it
1275 wxTreeItemIdValue cookie;
1276 wxTreeItemId root_id = m_root;
1277 std::stack<wxTreeItemId> subdirs_id;
1278
1279 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1280
1281 while( true )
1282 {
1283 if( ! child.IsOk() )
1284 {
1285 if( subdirs_id.empty() ) // all items were explored
1286 {
1287 root_id = child; // Not found: return an invalid wxTreeItemId
1288 break;
1289 }
1290 else
1291 {
1292 root_id = subdirs_id.top();
1293 subdirs_id.pop();
1294 child = m_TreeProject->GetFirstChild( root_id, cookie );
1295
1296 if( !child.IsOk() )
1297 continue;
1298 }
1299 }
1300
1301 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1302
1303 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1304 {
1305 if( itemData->GetFileName() == aSubDir ) // Found!
1306 {
1307 root_id = child;
1308 break;
1309 }
1310
1311 // child is a subdir, push in list to explore it later
1312 if( itemData->IsPopulated() )
1313 subdirs_id.push( child );
1314 }
1315
1316 child = m_TreeProject->GetNextChild( root_id, cookie );
1317 }
1318
1319 return root_id;
1320}
1321
1322
1323void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1324{
1325 // No need to process events when we're shutting down
1326 if( !m_watcher )
1327 return;
1328
1329 // Ignore events that are not file creation, deletion, renaming or modification because
1330 // they are not relevant to the project tree.
1331 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1332 wxFSW_EVENT_DELETE |
1333 wxFSW_EVENT_RENAME |
1334 wxFSW_EVENT_MODIFY ) ) )
1335 {
1336 return;
1337 }
1338
1339 const wxFileName& pathModified = event.GetPath();
1340 wxString subdir = pathModified.GetPath();
1341 wxString fn = pathModified.GetFullPath();
1342
1343 // Adjust directories to look like a file item (path and name).
1344 if( pathModified.GetFullName().IsEmpty() )
1345 {
1346 subdir = subdir.BeforeLast( '/' );
1347 fn = fn.BeforeLast( '/' );
1348 }
1349
1350 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1351
1352 if( !root_id.IsOk() )
1353 return;
1354
1355 CallAfter( [this] ()
1356 {
1357 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1358 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1359 } );
1360
1361 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1362 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1363
1364 switch( event.GetChangeType() )
1365 {
1366 case wxFSW_EVENT_CREATE:
1367 {
1368 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1369
1370 // If we are in the process of renaming a file, select the new one
1371 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1372 // pair of DELETE and CREATE events.
1373 if( m_isRenaming && newitem.IsOk() )
1374 {
1375 m_TreeProject->SelectItem( newitem );
1376 m_isRenaming = false;
1377 }
1378 }
1379 break;
1380
1381 case wxFSW_EVENT_DELETE:
1382 while( kid.IsOk() )
1383 {
1384 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1385
1386 if( itemData && itemData->GetFileName() == fn )
1387 {
1388 m_TreeProject->Delete( kid );
1389 return;
1390 }
1391 kid = m_TreeProject->GetNextChild( root_id, cookie );
1392 }
1393 break;
1394
1395 case wxFSW_EVENT_RENAME :
1396 {
1397 const wxFileName& newpath = event.GetNewPath();
1398 wxString newdir = newpath.GetPath();
1399 wxString newfn = newpath.GetFullPath();
1400
1401 while( kid.IsOk() )
1402 {
1403 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1404
1405 if( itemData && itemData->GetFileName() == fn )
1406 {
1407 m_TreeProject->Delete( kid );
1408 break;
1409 }
1410
1411 kid = m_TreeProject->GetNextChild( root_id, cookie );
1412 }
1413
1414 // Add the new item only if it is not the current project file (root item).
1415 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1416 // called after an actual file rename, and the cleanup code does not explore the
1417 // root item, because it cannot be renamed by the user. Also, ensure the new file
1418 // actually exists on the file system before it is readded. On Linux, moving a file
1419 // to the trash can cause the same path to be returned in both the old and new paths
1420 // of the event, even though the file isn't there anymore.
1421 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1422
1423 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1424 {
1425 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1426 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1427
1428 // If the item exists, select it
1429 if( newitem.IsOk() )
1430 m_TreeProject->SelectItem( newitem );
1431 }
1432
1433 m_isRenaming = false;
1434 }
1435 break;
1436
1437 default:
1438 return;
1439 }
1440
1441 // Sort filenames by alphabetic order
1442 m_TreeProject->SortChildren( root_id );
1443}
1444
1445
1447{
1448 m_watcherNeedReset = false;
1449
1450 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1451
1452#if defined( _WIN32 )
1453 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1454
1455 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1456 {
1457 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1458 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1459 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1460 // Windows Server shares :/
1461 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1462 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1463 return;
1464 }
1465 else
1466 {
1467 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1468 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1469 }
1470#endif
1471
1472 // Prepare file watcher:
1473 if( m_watcher )
1474 {
1475 m_watcher->RemoveAll();
1476 }
1477 else
1478 {
1479 // Create a wxWidgets log handler to catch errors during watcher creation
1480 // We need to to this because we cannot get error codes from the wxFileSystemWatcher
1481 // constructor. On Linux, if inotify cannot be initialized (usually due to resource limits),
1482 // wxWidgets will throw a system error and then, we throw another error below, trying to
1483 // add paths to a null watcher. We skip this by installing a temporary log handler that
1484 // catches errors during watcher creation and aborts if any error is detected.
1485 class WatcherLogHandler : public wxLog
1486 {
1487 public:
1488 explicit WatcherLogHandler( bool* err ) :
1489 m_err( err )
1490 {
1491 if( m_err )
1492 *m_err = false;
1493 }
1494
1495 protected:
1496 void DoLogTextAtLevel( wxLogLevel level, const wxString& text ) override
1497 {
1498 if( m_err && ( level == wxLOG_Error || level == wxLOG_FatalError ) )
1499 *m_err = true;
1500 }
1501
1502 private:
1503 bool* m_err;
1504 };
1505
1506 bool watcherHasError = false;
1507 WatcherLogHandler tmpLog( &watcherHasError );
1508 wxLog* oldLog = wxLog::SetActiveTarget( &tmpLog );
1509
1511 m_watcher->SetOwner( this );
1512
1513 // Restore previous log handler
1514 wxLog::SetActiveTarget( oldLog );
1515
1516 if( watcherHasError )
1517 {
1518 return;
1519 }
1520 }
1521
1522 // We can see wxString under a debugger, not a wxFileName
1523 wxFileName fn;
1524 fn.AssignDir( prj_dir );
1525 fn.DontFollowLink();
1526
1527 // Add directories which should be monitored.
1528 // under windows, we add the curr dir and all subdirs
1529 // under unix, we add only the curr dir and the populated subdirs
1530 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1531 // under unix, the file watcher needs more work to be efficient
1532 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1533 {
1534 wxLogNull logNo; // avoid log messages
1535#ifdef __WINDOWS__
1536 if( ! m_watcher->AddTree( fn ) )
1537 {
1538 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1539 TO_UTF8( fn.GetFullPath() ) );
1540 return;
1541 }
1542 }
1543#else
1544 if( !m_watcher->Add( fn ) )
1545 {
1546 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1547 TO_UTF8( fn.GetFullPath() ) );
1548 return;
1549 }
1550 }
1551
1552 if( m_TreeProject->IsEmpty() )
1553 return;
1554
1555 // Add subdirs
1556 wxTreeItemIdValue cookie;
1557 wxTreeItemId root_id = m_root;
1558
1559 std::stack < wxTreeItemId > subdirs_id;
1560
1561 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1562 int total_watch_count = 0;
1563
1564 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1565 {
1566 if( !kid.IsOk() )
1567 {
1568 if( subdirs_id.empty() ) // all items were explored
1569 {
1570 break;
1571 }
1572 else
1573 {
1574 root_id = subdirs_id.top();
1575 subdirs_id.pop();
1576 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1577
1578 if( !kid.IsOk() )
1579 continue;
1580 }
1581 }
1582
1583 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1584
1585 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1586 {
1587 // we can see wxString under a debugger, not a wxFileName
1588 const wxString& path = itemData->GetFileName();
1589
1590 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1591
1592 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1593 {
1594 {
1595 // Silence OS errors that come from the watcher
1596 wxLogNull silence;
1597 fn.AssignDir( path );
1598 m_watcher->Add( fn );
1599 total_watch_count++;
1600 }
1601
1602 // if kid is a subdir, push in list to explore it later
1603 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1604 subdirs_id.push( kid );
1605 }
1606 }
1607
1608 kid = m_TreeProject->GetNextChild( root_id, cookie );
1609 }
1610
1611 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1612 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1613#endif
1614
1615#if defined(DEBUG) && 1
1616 wxArrayString paths;
1617 m_watcher->GetWatchedPaths( &paths );
1618 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1619
1620 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1621 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1622#endif
1623 }
1624
1625
1627{
1628 // Make sure we don't try to inspect the tree after we've deleted its items.
1630
1631 m_TreeProject->DeleteAllItems();
1632
1633 // Remove the git repository when the project is unloaded
1634 if( m_TreeProject->GetGitRepo() )
1635 {
1636 m_TreeProject->GitCommon()->SetCancelled( true );
1637
1638 // We need to lock the mutex to ensure that no other thread is using the git repository
1639 std::unique_lock<std::mutex> lock( m_TreeProject->GitCommon()->m_gitActionMutex, std::try_to_lock );
1640
1641 if( !lock.owns_lock() )
1642 {
1643 // Block until any in-flight Git actions complete, showing a pulsing progress dialog
1644 {
1645 wxProgressDialog progress( _( "Please wait" ), _( "Waiting for Git operations to finish..." ),
1646 100, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH );
1647
1648 // Keep trying to acquire the lock, pulsing the dialog every 100 ms
1649 while ( !lock.try_lock() )
1650 {
1651 progress.Pulse();
1652 std::this_thread::sleep_for( std::chrono::milliseconds(100) );
1653 // allow UI events to process so dialog remains responsive
1654 wxYield();
1655 }
1656 }
1657 }
1658
1659 git_repository* repo = m_TreeProject->GetGitRepo();
1661 m_TreeProject->SetGitRepo( nullptr );
1662 }
1663}
1664
1665
1666void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1667{
1669 m_TreeProject->LoadIcons();
1670 m_TreeProject->Refresh();
1671
1672 aEvent.Skip();
1673}
1674
1675
1676void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1677{
1678 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1679 wxPaintDC dc( this );
1680
1681 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1682 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1683
1684 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1685 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1686}
1687
1688
1689void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1690{
1691 m_leftWin->FileWatcherReset();
1692}
1693
1694
1695void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1696{
1697 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1698
1699 wxString dir = tree_data->GetDir();
1700
1701 if( dir.empty() )
1702 {
1703 wxLogError( "Failed to initialize git project: project directory is empty." );
1704 return;
1705 }
1706
1707 GIT_INIT_HANDLER initHandler( m_TreeProject->GitCommon() );
1708 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1709
1710 if( initHandler.IsRepository( dir ) )
1711 {
1712 DisplayInfoMessage( topLevelParent,
1713 _( "The selected directory is already a Git project." ) );
1714 return;
1715 }
1716
1717 InitResult result = initHandler.InitializeRepository( dir );
1719 {
1720 DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ),
1721 initHandler.GetErrorString() );
1722 return;
1723 }
1724
1725 m_gitLastError = GIT_ERROR_NONE;
1726
1727 DIALOG_GIT_REPOSITORY dlg( topLevelParent, initHandler.GetRepo() );
1728 dlg.SetTitle( _( "Set default remote" ) );
1729 dlg.SetSkipButtonLabel( _( "Skip" ) );
1730
1731 if( dlg.ShowModal() != wxID_OK )
1732 return;
1733
1734 // Set up the remote
1735 RemoteConfig remoteConfig;
1736 remoteConfig.url = dlg.GetRepoURL();
1737 remoteConfig.username = dlg.GetUsername();
1738 remoteConfig.password = dlg.GetPassword();
1739 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1740 remoteConfig.connType = dlg.GetRepoType();
1741
1742 if( !initHandler.SetupRemote( remoteConfig ) )
1743 {
1744 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1745 initHandler.GetErrorString() );
1746 return;
1747 }
1748
1749 m_gitLastError = GIT_ERROR_NONE;
1750
1751 GIT_PULL_HANDLER handler( m_TreeProject->GitCommon() );
1752 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1753 handler.PerformFetch();
1754
1758
1762 Prj().GetLocalSettings().m_GitRepoType = "https";
1763 else
1764 Prj().GetLocalSettings().m_GitRepoType = "local";
1765}
1766
1767
1768void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
1769{
1770
1771}
1772
1773
1774void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
1775{
1776 git_repository* repo = m_TreeProject->GetGitRepo();
1777
1778 if( !repo )
1779 return;
1780
1781 GIT_PULL_HANDLER handler( m_TreeProject->GitCommon() );
1782
1783 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1,
1784 PR_NO_ABORT ) );
1785
1786 if( handler.PerformPull() < PullResult::Success )
1787 {
1788 wxString errorMessage = handler.GetErrorString();
1789
1790 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), errorMessage );
1791 }
1792
1793 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1794}
1795
1796
1797void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
1798{
1799 git_repository* repo = m_TreeProject->GetGitRepo();
1800
1801 if( !repo )
1802 return;
1803
1804 GIT_PUSH_HANDLER handler( m_TreeProject->GitCommon() );
1805
1806 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1,
1807 PR_NO_ABORT ) );
1808
1809 if( handler.PerformPush() != PushResult::Success )
1810 {
1811 wxString errorMessage = handler.GetErrorString();
1812
1813 DisplayErrorMessage( m_parent, _( "Failed to push project" ), errorMessage );
1814 }
1815
1816 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1817}
1818
1819
1820void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
1821{
1822 if( !m_TreeProject->GetGitRepo() )
1823 return;
1824
1825 GIT_BRANCH_HANDLER branchHandler( m_TreeProject->GitCommon() );
1826 wxString branchName;
1827
1828 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
1829 {
1830 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), m_TreeProject->GetGitRepo() );
1831
1832 int retval = dlg.ShowModal();
1833 branchName = dlg.GetBranchName();
1834
1835 if( retval == wxID_ADD )
1836 KIGIT::PROJECT_GIT_UTILS::CreateBranch( m_TreeProject->GetGitRepo(), branchName );
1837 else if( retval != wxID_OK )
1838 return;
1839 }
1840 else
1841 {
1842 std::vector<wxString> branches = m_TreeProject->GitCommon()->GetBranchNames();
1843 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_BRANCH;
1844
1845 if( branchIndex < 0 || static_cast<size_t>( branchIndex ) >= branches.size() )
1846 return;
1847
1848 branchName = branches[branchIndex];
1849 }
1850
1851 wxLogTrace( traceGit, wxS( "onGitSwitchBranch: Switching to branch '%s'" ), branchName );
1852 if( branchHandler.SwitchToBranch( branchName ) != BranchResult::Success )
1853 {
1854 DisplayError( m_parent, branchHandler.GetErrorString() );
1855 }
1856}
1857
1858
1859void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
1860{
1861 git_repository* repo = m_TreeProject->GetGitRepo();
1862
1863 if( !repo
1864 || !IsOK( wxGetTopLevelParent( this ),
1865 _( "Are you sure you want to remove Git tracking from this project?" ) ) )
1866 {
1867 return;
1868 }
1869
1870 // Fully delete the git repository on disk
1871 wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Removing VCS from project" ) );
1872 wxString errors;
1873
1874 if( !KIGIT::PROJECT_GIT_UTILS::RemoveVCS( repo, Prj().GetProjectPath(), true, &errors ) )
1875 {
1876 DisplayErrorMessage( m_parent, _( "Failed to remove VCS" ), errors );
1877 return;
1878 }
1879
1880 m_TreeProject->SetGitRepo( nullptr );
1881
1882 // Clear all item states
1883 std::stack<wxTreeItemId> items;
1884 items.push( m_TreeProject->GetRootItem() );
1885
1886 while( !items.empty() )
1887 {
1888 wxTreeItemId current = items.top();
1889 items.pop();
1890
1891 // Process the current item
1892 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
1893
1894 wxTreeItemIdValue cookie;
1895 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
1896
1897 while( child.IsOk() )
1898 {
1899 items.push( child );
1900 child = m_TreeProject->GetNextChild( current, cookie );
1901 }
1902 }
1903}
1904
1905
1907{
1908 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
1909 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
1910
1911 if( !lock.owns_lock() )
1912 {
1913 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
1914 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1915 return;
1916 }
1917
1918 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
1919 {
1920 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
1921 return;
1922 }
1923
1924 std::stack<wxTreeItemId> items;
1925 items.push(m_TreeProject->GetRootItem());
1926
1927 while( !items.empty() )
1928 {
1929 wxTreeItemId current = items.top();
1930 items.pop();
1931
1932 if( m_TreeProject->ItemHasChildren( current ) )
1933 {
1934 wxTreeItemIdValue cookie;
1935 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
1936
1937 while( child.IsOk() )
1938 {
1939 items.push( child );
1940
1941 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
1942 {
1943 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
1944 }
1945
1946 child = m_TreeProject->GetNextChild( current, cookie );
1947 }
1948 }
1949 }
1950
1951 if (!m_gitCurrentBranchName.empty())
1952 {
1953 wxTreeItemId kid = m_TreeProject->GetRootItem();
1954 PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid );
1955 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
1956 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
1957 m_gitIconsInitialized = true;
1958 }
1959
1960 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
1961}
1962
1963
1965{
1966 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
1967
1968 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
1969
1970 if( !lock.owns_lock() )
1971 {
1972 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
1973 return;
1974 }
1975
1976 if( !m_TreeProject )
1977 {
1978 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
1979 return;
1980 }
1981
1982 wxTreeItemId kid = m_TreeProject->GetRootItem();
1983
1984 if( !kid.IsOk() )
1985 return;
1986
1987 // Collect a map to easily set the state of each item
1988 m_gitTreeCache.clear();
1989 std::stack<wxTreeItemId> items;
1990 items.push( kid );
1991
1992 while( !items.empty() )
1993 {
1994 kid = items.top();
1995 items.pop();
1996
1997 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
1998
1999 if( !nextItem )
2000 continue;
2001
2002 wxString gitAbsPath = nextItem->GetFileName();
2003#ifdef _WIN32
2004 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2005#endif
2006 m_gitTreeCache[gitAbsPath] = kid;
2007
2008 wxTreeItemIdValue cookie;
2009 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2010
2011 while( child.IsOk() )
2012 {
2013 items.push( child );
2014 child = m_TreeProject->GetNextChild( kid, cookie );
2015 }
2016 }
2017}
2018
2019
2021{
2022 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2023#if defined( _WIN32 )
2025
2026 if( refresh != 0
2027 && KIPLATFORM::ENV::IsNetworkPath( wxPathOnly( m_Parent->GetProjectFileName() ) ) )
2028 {
2029 // Need to treat windows network paths special here until we get the samba bug fixed
2030 // https://github.com/wxWidgets/wxWidgets/issues/18953
2031 CallAfter(
2032 [this, refresh]()
2033 {
2034 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2035 } );
2036 }
2037
2038#endif
2039
2040 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2041 return;
2042
2043 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2044 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2045
2046 if( !lock1.owns_lock() || !lock2.owns_lock() )
2047 {
2048 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2049 return;
2050 }
2051
2052 if( !m_TreeProject->GetGitRepo() )
2053 {
2054 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2055 return;
2056 }
2057
2058 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2059
2060 // Set up pathspec for project files
2061 wxFileName rootFilename( Prj().GetProjectFullName() );
2062 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2063
2064 wxFileName relative = rootFilename;
2065 relative.MakeRelativeTo( repoWorkDir );
2066 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2067
2068#ifdef _WIN32
2069 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2070#endif
2071
2072 // Get file status
2073 auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr );
2074 auto [localChanges, remoteChanges] = m_TreeProject->GitCommon()->GetDifferentFiles();
2075 statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap );
2076
2077 bool updated = false;
2078
2079 // Update status icons based on file status
2080 for( const auto& [absPath, fileStatus] : fileStatusMap )
2081 {
2082 auto iter = m_gitTreeCache.find( absPath );
2083 if( iter == m_gitTreeCache.end() )
2084 {
2085 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2086 continue;
2087 }
2088
2089 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status );
2090 if( inserted || it->second != fileStatus.status )
2091 updated = true;
2092 it->second = fileStatus.status;
2093 }
2094
2095 // Get the current branch name
2097
2098 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2099
2100 // Update UI if icons changed
2101 if( updated || !m_gitIconsInitialized )
2102 {
2103 CallAfter(
2104 [this]()
2105 {
2107 } );
2108 }
2109}
2110
2111
2112void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2113{
2114 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2115
2116 git_repository* repo = m_TreeProject->GetGitRepo();
2117
2118 if( repo == nullptr )
2119 {
2120 wxMessageBox( _( "The selected directory is not a Git project." ) );
2121 return;
2122 }
2123
2124 // Get git configuration
2125 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2126 GitUserConfig userConfig = configHandler.GetUserConfig();
2127
2128 // Collect modified files in the repository
2129 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2130 auto fileStatusMap = statusHandler.GetFileStatus();
2131
2132 std::map<wxString, int> modifiedFiles;
2133 std::set<wxString> selected_files;
2134
2135 for( PROJECT_TREE_ITEM* item : tree_data )
2136 {
2137 if( item->GetType() != TREE_FILE_TYPE::DIRECTORY )
2138 selected_files.emplace( item->GetFileName() );
2139 }
2140
2141 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2142
2143 for( const auto& [absPath, fileStatus] : fileStatusMap )
2144 {
2145 // Skip current, conflicted, or ignored files
2146 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2148 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2149 {
2150 continue;
2151 }
2152
2153 wxFileName fn( absPath );
2154
2155 // Convert to relative path for the modifiedFiles map
2156 wxString relativePath = absPath;
2157 if( relativePath.StartsWith( repoWorkDir ) )
2158 {
2159 relativePath = relativePath.Mid( repoWorkDir.length() );
2160#ifdef _WIN32
2161 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2162#endif
2163 }
2164
2165 // Do not commit files outside the project directory
2166 wxString projectPath = Prj().GetProjectPath();
2167 if( !absPath.StartsWith( projectPath ) )
2168 continue;
2169
2170 // Skip lock files
2171 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2172 continue;
2173
2174 // Skip autosave, lock, and backup files
2175 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix )
2176 || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2177 {
2178 continue;
2179 }
2180
2181 // Skip archived project backups
2182 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2183 continue;
2184
2185 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2186 {
2187 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2188 }
2189 else if( selected_files.count( absPath ) )
2190 {
2191 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2192 }
2193 }
2194
2195 // Create a commit dialog
2196 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
2197 modifiedFiles );
2198 auto ret = dlg.ShowModal();
2199
2200 if( ret != wxID_OK )
2201 return;
2202
2203 std::vector<wxString> files = dlg.GetSelectedFiles();
2204
2205 if( dlg.GetCommitMessage().IsEmpty() )
2206 {
2207 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2208 return;
2209 }
2210
2211 if( files.empty() )
2212 {
2213 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2214 return;
2215 }
2216
2217 GIT_COMMIT_HANDLER commitHandler( repo );
2218 auto result = commitHandler.PerformCommit( files, dlg.GetCommitMessage(),
2219 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
2220
2222 {
2223 wxMessageBox( wxString::Format( _( "Failed to create commit: %s" ),
2224 commitHandler.GetErrorString() ) );
2225 return;
2226 }
2227
2228 wxLogTrace( traceGit, wxS( "Created commit" ) );
2229 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2230}
2231
2232
2233void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
2234{
2235
2236}
2237
2238
2239bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
2240{
2241 if( !m_TreeProject->GetGitRepo() )
2242 return false;
2243
2244 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2245 auto fileStatusMap = statusHandler.GetFileStatus();
2246
2247 // Check if file is already tracked or staged
2248 for( const auto& [filePath, fileStatus] : fileStatusMap )
2249 {
2250 if( filePath.EndsWith( aFile ) || filePath == aFile )
2251 {
2252 // File can be added if it's untracked
2253 return fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED;
2254 }
2255 }
2256
2257 // If file not found in status, it might be addable
2258 return true;
2259}
2260
2261
2262void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
2263{
2264 wxLogTrace( traceGit, "Syncing project" );
2265 git_repository* repo = m_TreeProject->GetGitRepo();
2266
2267 if( !repo )
2268 {
2269 wxLogTrace( traceGit, "sync: No git repository found" );
2270 return;
2271 }
2272
2273 GIT_SYNC_HANDLER handler( repo );
2274 handler.PerformSync();
2275}
2276
2277
2278void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
2279{
2280 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2281
2282 if( !gitCommon )
2283 return;
2284
2285 GIT_PULL_HANDLER handler( gitCommon );
2286 handler.PerformFetch();
2287
2288 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2289}
2290
2291
2292void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
2293{
2294 git_repository* repo = m_TreeProject->GetGitRepo();
2295
2296 if( !repo )
2297 return;
2298
2299 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
2300 handler.PerformResolveConflict();
2301}
2302
2303
2304void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
2305{
2306 git_repository* repo = m_TreeProject->GetGitRepo();
2307
2308 if( !repo )
2309 return;
2310
2311 GIT_REVERT_HANDLER handler( repo );
2312 handler.PerformRevert();
2313}
2314
2315
2316void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
2317{
2318 git_repository* repo = m_TreeProject->GetGitRepo();
2319
2320 if( !repo )
2321 return;
2322
2323 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
2324 handler.PerformRemoveFromIndex();
2325}
2326
2327
2329{
2330
2331}
2332
2333
2334void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
2335{
2336 wxLogTrace( traceGit, "onGitSyncTimer" );
2337 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
2338
2339 if( !gitSettings.enableGit || !m_TreeProject )
2340 return;
2341
2343
2344 tp.detach_task( [this]()
2345 {
2346 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2347
2348 if( !gitCommon )
2349 {
2350 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
2351 return;
2352 }
2353
2354 GIT_PULL_HANDLER handler( gitCommon );
2355 handler.PerformFetch();
2356
2357 CallAfter( [this]() { gitStatusTimerHandler(); } );
2358 } );
2359
2360 if( gitSettings.updatInterval > 0 )
2361 {
2362 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
2363 // We store the timer interval in minutes but wxTimer uses milliseconds
2364 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
2365 }
2366}
2367
2368
2370{
2373
2374 tp.detach_task( [this]() { updateGitStatusIconMap(); } );
2375}
2376
2377void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
2378{
2379 wxLogTrace( traceGit, "onGitStatusTimer" );
2380
2381 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2382 return;
2383
2385}
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 GetRepoType() const
wxString GetRepoSSHPath() const
void SetSkipButtonLabel(const wxString &aLabel)
wxString GetBranchName() const
int ShowModal() override
virtual bool IsLibraryAvailable()=0
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.
PROJECT_TREE_PANE * m_leftWin
void OnChangeWatchedPaths(wxCommandEvent &aEvent)
Called by sending a event with id = ID_INIT_WATCHED_PATHS rebuild the list of watched paths.
static git_repository * GetRepositoryForFile(const char *aFilename)
Discover and open the repository that contains the given file.
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.
wxString GetGitRootDirectory() const
bool HasPushAndPullRemote() const
bool HasLocalCommits() const
wxString GetErrorString()
git_repository * GetRepo() const
Get a pointer to the git repository.
KISTATUSBAR is a wxStatusBar suitable for Kicad manager.
Definition kistatusbar.h:46
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:537
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition pgm_base.cpp:206
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
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.
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:167
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition project.h:205
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition confirm.cpp:259
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:230
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:202
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition confirm.cpp:177
This file is part of the common library.
#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:145
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 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:629
@ 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:85
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.
Definition pgm_base.cpp:946
#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[]
#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