You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

455 lines
16 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 Mike Williams <mike@mikebwilliams.com>
  5. * Copyright (C) 1992-2024 KiCad Developers, see AUTHORS.txt for contributors.
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU General Public License
  9. * as published by the Free Software Foundation; either version 2
  10. * of the License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, you may find one here:
  19. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  20. * or you may search the http://www.gnu.org website for the version 2 license,
  21. * or you may write to the Free Software Foundation, Inc.,
  22. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  23. */
  24. #include <common.h>
  25. #include <i18n_utility.h>
  26. #include <wx/dir.h>
  27. #include <wx/ffile.h>
  28. #include <wx/filename.h>
  29. #include <wx/log.h>
  30. #include <wx/translation.h>
  31. #include <wx/string.h>
  32. #include <wx/arrstr.h>
  33. #include <wx/datetime.h>
  34. #include <wildcards_and_files_ext.h>
  35. #include <kiway_player.h>
  36. #include <design_block_io.h>
  37. #include <design_block.h>
  38. #include <ki_exception.h>
  39. #include <trace_helpers.h>
  40. #include <fstream>
  41. const wxString DESIGN_BLOCK_IO_MGR::ShowType( DESIGN_BLOCK_FILE_T aFileType )
  42. {
  43. switch( aFileType )
  44. {
  45. case KICAD_SEXP: return _( "KiCad" );
  46. default: return wxString::Format( _( "UNKNOWN (%d)" ), aFileType );
  47. }
  48. }
  49. DESIGN_BLOCK_IO_MGR::DESIGN_BLOCK_FILE_T
  50. DESIGN_BLOCK_IO_MGR::EnumFromStr( const wxString& aFileType )
  51. {
  52. if( aFileType == _( "KiCad" ) )
  53. return DESIGN_BLOCK_FILE_T( KICAD_SEXP );
  54. return DESIGN_BLOCK_FILE_T( DESIGN_BLOCK_FILE_UNKNOWN );
  55. }
  56. DESIGN_BLOCK_IO* DESIGN_BLOCK_IO_MGR::FindPlugin( DESIGN_BLOCK_FILE_T aFileType )
  57. {
  58. switch( aFileType )
  59. {
  60. case KICAD_SEXP: return new DESIGN_BLOCK_IO();
  61. default: return nullptr;
  62. }
  63. }
  64. DESIGN_BLOCK_IO_MGR::DESIGN_BLOCK_FILE_T
  65. DESIGN_BLOCK_IO_MGR::GuessPluginTypeFromLibPath( const wxString& aLibPath, int aCtl )
  66. {
  67. if( IO_RELEASER<DESIGN_BLOCK_IO>( FindPlugin( KICAD_SEXP ) )->CanReadLibrary( aLibPath ) && aCtl != KICTL_NONKICAD_ONLY )
  68. return KICAD_SEXP;
  69. return DESIGN_BLOCK_IO_MGR::FILE_TYPE_NONE;
  70. }
  71. bool DESIGN_BLOCK_IO_MGR::ConvertLibrary( std::map<std::string, UTF8>* aOldFileProps,
  72. const wxString& aOldFilePath,
  73. const wxString& aNewFilePath )
  74. {
  75. DESIGN_BLOCK_IO_MGR::DESIGN_BLOCK_FILE_T oldFileType =
  76. DESIGN_BLOCK_IO_MGR::GuessPluginTypeFromLibPath( aOldFilePath );
  77. if( oldFileType == DESIGN_BLOCK_IO_MGR::FILE_TYPE_NONE )
  78. return false;
  79. IO_RELEASER<DESIGN_BLOCK_IO> oldFilePI( DESIGN_BLOCK_IO_MGR::FindPlugin( oldFileType ) );
  80. IO_RELEASER<DESIGN_BLOCK_IO> kicadPI(
  81. DESIGN_BLOCK_IO_MGR::FindPlugin( DESIGN_BLOCK_IO_MGR::KICAD_SEXP ) );
  82. wxArrayString dbNames;
  83. wxFileName newFileName( aNewFilePath );
  84. if( newFileName.HasExt() )
  85. {
  86. wxString extraDir = newFileName.GetFullName();
  87. newFileName.ClearExt();
  88. newFileName.SetName( "" );
  89. newFileName.AppendDir( extraDir );
  90. }
  91. if( !newFileName.DirExists() && !wxFileName::Mkdir( aNewFilePath, wxS_DIR_DEFAULT ) )
  92. return false;
  93. try
  94. {
  95. bool bestEfforts = false; // throw on first error
  96. oldFilePI->DesignBlockEnumerate( dbNames, aOldFilePath, bestEfforts, aOldFileProps );
  97. for( const wxString& dbName : dbNames )
  98. {
  99. std::unique_ptr<const DESIGN_BLOCK> db(
  100. oldFilePI->GetEnumeratedDesignBlock( aOldFilePath, dbName, aOldFileProps ) );
  101. kicadPI->DesignBlockSave( aNewFilePath, db.get() );
  102. }
  103. }
  104. catch( ... )
  105. {
  106. return false;
  107. }
  108. return true;
  109. }
  110. const DESIGN_BLOCK_IO::IO_FILE_DESC DESIGN_BLOCK_IO::GetLibraryDesc() const
  111. {
  112. return IO_BASE::IO_FILE_DESC( _HKI( "KiCad Design Block folders" ), {},
  113. { FILEEXT::KiCadDesignBlockLibPathExtension }, false );
  114. }
  115. long long DESIGN_BLOCK_IO::GetLibraryTimestamp( const wxString& aLibraryPath ) const
  116. {
  117. wxDir libDir( aLibraryPath );
  118. if( !libDir.IsOpened() )
  119. return 0;
  120. long long ts = 0;
  121. wxString filename;
  122. bool hasMoreFiles = libDir.GetFirst( &filename, wxEmptyString, wxDIR_DIRS );
  123. while( hasMoreFiles )
  124. {
  125. wxFileName blockDir( aLibraryPath, filename );
  126. // Check if the directory ends with ".block", if so hash all the files in it
  127. if( blockDir.GetFullName().EndsWith( FILEEXT::KiCadDesignBlockPathExtension ) )
  128. ts += TimestampDir( blockDir.GetFullPath(), wxT( "*" ) );
  129. hasMoreFiles = libDir.GetNext( &filename );
  130. }
  131. return ts;
  132. }
  133. void DESIGN_BLOCK_IO::CreateLibrary( const wxString& aLibraryPath,
  134. const std::map<std::string, UTF8>* aProperties )
  135. {
  136. if( wxDir::Exists( aLibraryPath ) )
  137. {
  138. THROW_IO_ERROR( wxString::Format( _( "Cannot overwrite library path '%s'." ),
  139. aLibraryPath.GetData() ) );
  140. }
  141. wxFileName dir;
  142. dir.SetPath( aLibraryPath );
  143. if( !dir.Mkdir() )
  144. {
  145. THROW_IO_ERROR(
  146. wxString::Format( _( "Library path '%s' could not be created.\n\n"
  147. "Make sure you have write permissions and try again." ),
  148. dir.GetPath() ) );
  149. }
  150. }
  151. bool DESIGN_BLOCK_IO::DeleteLibrary( const wxString& aLibraryPath,
  152. const std::map<std::string, UTF8>* aProperties )
  153. {
  154. wxFileName fn;
  155. fn.SetPath( aLibraryPath );
  156. // Return if there is no library path to delete.
  157. if( !fn.DirExists() )
  158. return false;
  159. if( !fn.IsDirWritable() )
  160. {
  161. THROW_IO_ERROR( wxString::Format( _( "Insufficient permissions to delete folder '%s'." ),
  162. aLibraryPath.GetData() ) );
  163. }
  164. wxDir dir( aLibraryPath );
  165. // Design block folders should only contain sub-folders for each design block
  166. if( dir.HasFiles() )
  167. {
  168. THROW_IO_ERROR( wxString::Format( _( "Library folder '%s' has unexpected files." ),
  169. aLibraryPath.GetData() ) );
  170. }
  171. // Must delete all sub-directories before deleting the library directory
  172. if( dir.HasSubDirs() )
  173. {
  174. wxArrayString dirs;
  175. // Get all sub-directories in the library path
  176. dir.GetAllFiles( aLibraryPath, &dirs, wxEmptyString, wxDIR_DIRS );
  177. for( size_t i = 0; i < dirs.GetCount(); i++ )
  178. {
  179. wxFileName tmp = dirs[i];
  180. if( tmp.GetExt() != FILEEXT::KiCadDesignBlockLibPathExtension )
  181. {
  182. THROW_IO_ERROR( wxString::Format( _( "Unexpected folder '%s' found in library "
  183. "path '%s'." ),
  184. dirs[i].GetData(), aLibraryPath.GetData() ) );
  185. }
  186. }
  187. for( size_t i = 0; i < dirs.GetCount(); i++ )
  188. wxRemoveFile( dirs[i] );
  189. }
  190. wxLogTrace( traceDesignBlocks, wxT( "Removing design block library '%s'." ),
  191. aLibraryPath.GetData() );
  192. // Some of the more elaborate wxRemoveFile() crap puts up its own wxLog dialog
  193. // we don't want that. we want bare metal portability with no UI here.
  194. if( !wxFileName::Rmdir( aLibraryPath, wxPATH_RMDIR_RECURSIVE ) )
  195. {
  196. THROW_IO_ERROR( wxString::Format( _( "Design block library '%s' cannot be deleted." ),
  197. aLibraryPath.GetData() ) );
  198. }
  199. // For some reason removing a directory in Windows is not immediately updated. This delay
  200. // prevents an error when attempting to immediately recreate the same directory when over
  201. // writing an existing library.
  202. #ifdef __WINDOWS__
  203. wxMilliSleep( 250L );
  204. #endif
  205. return true;
  206. }
  207. void DESIGN_BLOCK_IO::DesignBlockEnumerate( wxArrayString& aDesignBlockNames,
  208. const wxString& aLibraryPath, bool aBestEfforts,
  209. const std::map<std::string, UTF8>* aProperties )
  210. {
  211. // From the starting directory, look for all directories ending in the .block extension
  212. wxDir dir( aLibraryPath );
  213. if( !dir.IsOpened() )
  214. return;
  215. wxString dirname;
  216. wxString fileSpec = wxT( "*." ) + wxString( FILEEXT::KiCadDesignBlockPathExtension );
  217. bool cont = dir.GetFirst( &dirname, fileSpec, wxDIR_DIRS );
  218. while( cont )
  219. {
  220. aDesignBlockNames.Add( dirname.Before( wxT( '.' ) ) );
  221. cont = dir.GetNext( &dirname );
  222. }
  223. }
  224. DESIGN_BLOCK* DESIGN_BLOCK_IO::DesignBlockLoad( const wxString& aLibraryPath,
  225. const wxString& aDesignBlockName, bool aKeepUUID,
  226. const std::map<std::string, UTF8>* aProperties )
  227. {
  228. DESIGN_BLOCK* newDB = new DESIGN_BLOCK();
  229. wxString dbPath = aLibraryPath + wxFileName::GetPathSeparator() +
  230. aDesignBlockName + wxT( "." ) + FILEEXT::KiCadDesignBlockPathExtension + wxFileName::GetPathSeparator();
  231. wxString dbSchPath = dbPath + aDesignBlockName + wxT( "." ) + FILEEXT::KiCadSchematicFileExtension;
  232. wxString dbMetadataPath = dbPath + aDesignBlockName + wxT( "." ) + FILEEXT::JsonFileExtension;
  233. // Library name needs to be empty for when we fill it in with the correct library nickname
  234. // one layer above
  235. newDB->SetLibId( LIB_ID( wxEmptyString, aDesignBlockName ) );
  236. newDB->SetSchematicFile(
  237. // Library path
  238. aLibraryPath + wxFileName::GetPathSeparator() +
  239. // Design block name (project folder)
  240. aDesignBlockName + +wxT( "." ) + FILEEXT::KiCadDesignBlockPathExtension + wxT( "/" ) +
  241. // Schematic file
  242. aDesignBlockName + wxT( "." ) + FILEEXT::KiCadSchematicFileExtension );
  243. // Parse the JSON file if it exists
  244. if( wxFileExists( dbMetadataPath ) )
  245. {
  246. try
  247. {
  248. nlohmann::ordered_json dbMetadata;
  249. std::ifstream dbMetadataFile( dbMetadataPath.fn_str() );
  250. dbMetadataFile >> dbMetadata;
  251. if( dbMetadata.contains( "description" ) )
  252. newDB->SetLibDescription( dbMetadata["description"] );
  253. if( dbMetadata.contains( "keywords" ) )
  254. newDB->SetKeywords( dbMetadata["keywords"] );
  255. nlohmann::ordered_map<wxString, wxString> fields;
  256. // Read the "fields" object from the JSON
  257. if( dbMetadata.contains( "fields" ) )
  258. {
  259. for( auto& item : dbMetadata["fields"].items() )
  260. {
  261. wxString name = wxString::FromUTF8( item.key() );
  262. wxString value = wxString::FromUTF8( item.value().get<std::string>() );
  263. fields[name] = value;
  264. }
  265. newDB->SetFields( fields );
  266. }
  267. }
  268. catch( ... )
  269. {
  270. THROW_IO_ERROR( wxString::Format(
  271. _( "Design block metadata file '%s' could not be read." ), dbMetadataPath ) );
  272. }
  273. }
  274. else
  275. return nullptr;
  276. return newDB;
  277. }
  278. void DESIGN_BLOCK_IO::DesignBlockSave( const wxString& aLibraryPath,
  279. const DESIGN_BLOCK* aDesignBlock,
  280. const std::map<std::string, UTF8>* aProperties )
  281. {
  282. // Make sure we have a valid LIB_ID or we can't save the design block
  283. if( !aDesignBlock->GetLibId().IsValid() )
  284. {
  285. THROW_IO_ERROR( _( "Design block does not have a valid library ID." ) );
  286. }
  287. if( !wxFileExists( aDesignBlock->GetSchematicFile() ) )
  288. {
  289. THROW_IO_ERROR( wxString::Format( _( "Schematic source file '%s' does not exist." ),
  290. aDesignBlock->GetSchematicFile() ) );
  291. }
  292. // Create the design block folder
  293. wxFileName dbFolder( aLibraryPath + wxFileName::GetPathSeparator()
  294. + aDesignBlock->GetLibId().GetLibItemName() + wxT( "." )
  295. + FILEEXT::KiCadDesignBlockPathExtension
  296. + wxFileName::GetPathSeparator() );
  297. if( !dbFolder.DirExists() )
  298. {
  299. if( !dbFolder.Mkdir() )
  300. {
  301. THROW_IO_ERROR( wxString::Format( _( "Design block folder '%s' could not be created." ),
  302. dbFolder.GetFullPath().GetData() ) );
  303. }
  304. }
  305. // The new schematic file name is based on the design block name, not the source sheet name
  306. wxString dbSchematicFile = dbFolder.GetFullPath() + aDesignBlock->GetLibId().GetLibItemName()
  307. + wxT( "." ) + FILEEXT::KiCadSchematicFileExtension;
  308. // If the source and destination files are the same, then we don't need to copy the file
  309. // as we are just updating the metadata
  310. if( aDesignBlock->GetSchematicFile() != dbSchematicFile )
  311. {
  312. // Copy the source sheet file to the design block folder, under the design block name
  313. if( !wxCopyFile( aDesignBlock->GetSchematicFile(), dbSchematicFile ) )
  314. {
  315. THROW_IO_ERROR( wxString::Format(
  316. _( "Schematic file '%s' could not be saved as design block at '%s'." ),
  317. aDesignBlock->GetSchematicFile().GetData(), dbSchematicFile ) );
  318. }
  319. }
  320. wxString dbMetadataFile = dbFolder.GetFullPath() + aDesignBlock->GetLibId().GetLibItemName()
  321. + wxT( "." ) + FILEEXT::JsonFileExtension;
  322. // Write the metadata file
  323. nlohmann::ordered_json dbMetadata;
  324. dbMetadata["description"] = aDesignBlock->GetLibDescription();
  325. dbMetadata["keywords"] = aDesignBlock->GetKeywords();
  326. dbMetadata["fields"] = aDesignBlock->GetFields();
  327. bool success = false;
  328. try
  329. {
  330. wxFFile mdFile( dbMetadataFile, wxT( "wb" ) );
  331. if( mdFile.IsOpened() )
  332. success = mdFile.Write( dbMetadata.dump( 0 ) );
  333. // wxFFile dtor will close the file
  334. }
  335. catch( ... )
  336. {
  337. success = false;
  338. }
  339. if( !success )
  340. {
  341. THROW_IO_ERROR( wxString::Format(
  342. _( "Design block metadata file '%s' could not be saved." ), dbMetadataFile ) );
  343. }
  344. }
  345. void DESIGN_BLOCK_IO::DesignBlockDelete( const wxString& aLibPath, const wxString& aDesignBlockName,
  346. const std::map<std::string, UTF8>* aProperties )
  347. {
  348. wxFileName dbDir = wxFileName( aLibPath + wxFileName::GetPathSeparator() + aDesignBlockName
  349. + wxT( "." ) + FILEEXT::KiCadDesignBlockPathExtension );
  350. if( !dbDir.DirExists() )
  351. {
  352. THROW_IO_ERROR(
  353. wxString::Format( _( "Design block '%s' does not exist." ), dbDir.GetFullName() ) );
  354. }
  355. // Delete the whole design block folder
  356. if( !wxFileName::Rmdir( dbDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE ) )
  357. {
  358. THROW_IO_ERROR( wxString::Format( _( "Design block folder '%s' could not be deleted." ),
  359. dbDir.GetFullPath().GetData() ) );
  360. }
  361. }
  362. bool DESIGN_BLOCK_IO::IsLibraryWritable( const wxString& aLibraryPath )
  363. {
  364. wxFileName path( aLibraryPath );
  365. return path.IsOk() && path.IsDirWritable();
  366. }