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