ProjectUtils.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include <ProjectUtils.h>
  8. #include <PythonBindingsInterface.h>
  9. #include <QFileDialog>
  10. #include <QDir>
  11. #include <QtMath>
  12. #include <QMessageBox>
  13. #include <QFileInfo>
  14. #include <QProcess>
  15. #include <QProcessEnvironment>
  16. #include <QGuiApplication>
  17. #include <QProgressDialog>
  18. namespace O3DE::ProjectManager
  19. {
  20. namespace ProjectUtils
  21. {
  22. static bool WarnDirectoryOverwrite(const QString& path, QWidget* parent)
  23. {
  24. if (!QDir(path).isEmpty())
  25. {
  26. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  27. parent, QObject::tr("Overwrite Directory"),
  28. QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"), QMessageBox::No | QMessageBox::Yes);
  29. if (warningResult != QMessageBox::Yes)
  30. {
  31. return false;
  32. }
  33. }
  34. return true;
  35. }
  36. static bool IsDirectoryDescedent(const QString& possibleAncestorPath, const QString& possibleDecedentPath)
  37. {
  38. QDir ancestor(possibleAncestorPath);
  39. QDir descendent(possibleDecedentPath);
  40. do
  41. {
  42. if (ancestor == descendent)
  43. {
  44. return true;
  45. }
  46. descendent.cdUp();
  47. } while (!descendent.isRoot());
  48. return false;
  49. }
  50. typedef AZStd::function<void(/*fileCount=*/int, /*totalSizeInBytes=*/int)> StatusFunction;
  51. static void RecursiveGetAllFiles(const QDir& directory, QStringList& outFileList, qint64& outTotalSizeInBytes, StatusFunction statusCallback)
  52. {
  53. const QStringList entries = directory.entryList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
  54. for (const QString& entryPath : entries)
  55. {
  56. const QString filePath = QDir::toNativeSeparators(QString("%1/%2").arg(directory.path()).arg(entryPath));
  57. QFileInfo fileInfo(filePath);
  58. if (fileInfo.isDir())
  59. {
  60. QDir subDirectory(filePath);
  61. RecursiveGetAllFiles(subDirectory, outFileList, outTotalSizeInBytes, statusCallback);
  62. }
  63. else
  64. {
  65. outFileList.push_back(filePath);
  66. outTotalSizeInBytes += fileInfo.size();
  67. const int updateStatusEvery = 64;
  68. if (outFileList.size() % updateStatusEvery == 0)
  69. {
  70. statusCallback(outFileList.size(), outTotalSizeInBytes);
  71. }
  72. }
  73. }
  74. }
  75. static bool CopyDirectory(QProgressDialog* progressDialog,
  76. const QString& origPath,
  77. const QString& newPath,
  78. QStringList& filesToCopy,
  79. int& outNumCopiedFiles,
  80. qint64 totalSizeToCopy,
  81. qint64& outCopiedFileSize,
  82. bool& showIgnoreFileDialog)
  83. {
  84. QDir original(origPath);
  85. if (!original.exists())
  86. {
  87. return false;
  88. }
  89. for (QString directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
  90. {
  91. if (progressDialog->wasCanceled())
  92. {
  93. return false;
  94. }
  95. QString newDirectoryPath = newPath + QDir::separator() + directory;
  96. original.mkpath(newDirectoryPath);
  97. if (!CopyDirectory(progressDialog, origPath + QDir::separator() + directory,
  98. newDirectoryPath, filesToCopy, outNumCopiedFiles, totalSizeToCopy, outCopiedFileSize, showIgnoreFileDialog))
  99. {
  100. return false;
  101. }
  102. }
  103. QLocale locale;
  104. const float progressDialogRangeHalf = qFabs(progressDialog->maximum() - progressDialog->minimum()) * 0.5f;
  105. for (QString file : original.entryList(QDir::Files))
  106. {
  107. if (progressDialog->wasCanceled())
  108. {
  109. return false;
  110. }
  111. // Progress window update
  112. {
  113. // Weight in the number of already copied files as well as the copied bytes to get a better progress indication
  114. // for cases combining many small files and some really large files.
  115. const float normalizedNumFiles = static_cast<float>(outNumCopiedFiles) / filesToCopy.count();
  116. const float normalizedFileSize = static_cast<float>(outCopiedFileSize) / totalSizeToCopy;
  117. const int progress = normalizedNumFiles * progressDialogRangeHalf + normalizedFileSize * progressDialogRangeHalf;
  118. progressDialog->setValue(progress);
  119. const QString copiedFileSizeString = locale.formattedDataSize(outCopiedFileSize);
  120. const QString totalFileSizeString = locale.formattedDataSize(totalSizeToCopy);
  121. progressDialog->setLabelText(QString("Coping file %1 of %2 (%3 of %4) ...").arg(QString::number(outNumCopiedFiles),
  122. QString::number(filesToCopy.count()),
  123. copiedFileSizeString,
  124. totalFileSizeString));
  125. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  126. }
  127. const QString toBeCopiedFilePath = origPath + QDir::separator() + file;
  128. const QString copyToFilePath = newPath + QDir::separator() + file;
  129. if (!QFile::copy(toBeCopiedFilePath, copyToFilePath))
  130. {
  131. // Let the user decide to ignore files that failed to copy or cancel the whole operation.
  132. if (showIgnoreFileDialog)
  133. {
  134. QMessageBox ignoreFileMessageBox;
  135. const QString text = QString("Cannot copy <b>%1</b>.<br><br>"
  136. "Source: %2<br>"
  137. "Destination: %3<br><br>"
  138. "Press <b>Yes</b> to ignore the file, <b>YesToAll</b> to ignore all upcoming non-copyable files or "
  139. "<b>Cancel</b> to abort duplicating the project.").arg(file, toBeCopiedFilePath, copyToFilePath);
  140. ignoreFileMessageBox.setModal(true);
  141. ignoreFileMessageBox.setWindowTitle("Cannot copy file");
  142. ignoreFileMessageBox.setText(text);
  143. ignoreFileMessageBox.setIcon(QMessageBox::Question);
  144. ignoreFileMessageBox.setStandardButtons(QMessageBox::YesToAll | QMessageBox::Yes | QMessageBox::Cancel);
  145. int ignoreFile = ignoreFileMessageBox.exec();
  146. if (ignoreFile == QMessageBox::YesToAll)
  147. {
  148. showIgnoreFileDialog = false;
  149. continue;
  150. }
  151. else if (ignoreFile == QMessageBox::Yes)
  152. {
  153. continue;
  154. }
  155. else
  156. {
  157. return false;
  158. }
  159. }
  160. }
  161. else
  162. {
  163. outNumCopiedFiles++;
  164. QFileInfo fileInfo(toBeCopiedFilePath);
  165. outCopiedFileSize += fileInfo.size();
  166. }
  167. }
  168. return true;
  169. }
  170. bool AddProjectDialog(QWidget* parent)
  171. {
  172. QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory")));
  173. if (!path.isEmpty())
  174. {
  175. return RegisterProject(path);
  176. }
  177. return false;
  178. }
  179. bool RegisterProject(const QString& path)
  180. {
  181. return PythonBindingsInterface::Get()->AddProject(path);
  182. }
  183. bool UnregisterProject(const QString& path)
  184. {
  185. return PythonBindingsInterface::Get()->RemoveProject(path);
  186. }
  187. bool CopyProjectDialog(const QString& origPath, QWidget* parent)
  188. {
  189. bool copyResult = false;
  190. QDir parentOrigDir(origPath);
  191. parentOrigDir.cdUp();
  192. QString newPath = QDir::toNativeSeparators(
  193. QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path()));
  194. if (!newPath.isEmpty())
  195. {
  196. if (!WarnDirectoryOverwrite(newPath, parent))
  197. {
  198. return false;
  199. }
  200. copyResult = CopyProject(origPath, newPath, parent);
  201. }
  202. return copyResult;
  203. }
  204. bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent)
  205. {
  206. // Disallow copying from or into subdirectory
  207. if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
  208. {
  209. return false;
  210. }
  211. QStringList filesToCopy;
  212. qint64 totalSizeInBytes = 0;
  213. QProgressDialog* progressDialog = new QProgressDialog(parent);
  214. progressDialog->setAutoClose(true);
  215. progressDialog->setValue(0);
  216. progressDialog->setRange(0, 1000);
  217. progressDialog->setModal(true);
  218. progressDialog->setWindowTitle(QObject::tr("Copying project ..."));
  219. progressDialog->show();
  220. QLocale locale;
  221. RecursiveGetAllFiles(origPath, filesToCopy, totalSizeInBytes, [=](int fileCount, int sizeInBytes)
  222. {
  223. // Create a human-readable version of the file size.
  224. const QString fileSizeString = locale.formattedDataSize(sizeInBytes);
  225. progressDialog->setLabelText(QString("%1 ... %2 %3, %4 %5.")
  226. .arg(QObject::tr("Indexing files"))
  227. .arg(QString::number(fileCount))
  228. .arg(QObject::tr("files found"))
  229. .arg(fileSizeString)
  230. .arg(QObject::tr("to copy")));
  231. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  232. });
  233. int numFilesCopied = 0;
  234. qint64 copiedFileSize = 0;
  235. // Phase 1: Copy files
  236. bool showIgnoreFileDialog = true;
  237. bool success = CopyDirectory(progressDialog, origPath, newPath, filesToCopy, numFilesCopied, totalSizeInBytes, copiedFileSize, showIgnoreFileDialog);
  238. if (success)
  239. {
  240. // Phase 2: Register project
  241. success = RegisterProject(newPath);
  242. }
  243. if (!success)
  244. {
  245. progressDialog->setLabelText(QObject::tr("Duplicating project failed/cancelled, removing already copied files ..."));
  246. qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
  247. DeleteProjectFiles(newPath, true);
  248. }
  249. progressDialog->deleteLater();
  250. return success;
  251. }
  252. bool DeleteProjectFiles(const QString& path, bool force)
  253. {
  254. QDir projectDirectory(path);
  255. if (projectDirectory.exists())
  256. {
  257. // Check if there is an actual project hereor just force it
  258. if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess())
  259. {
  260. return projectDirectory.removeRecursively();
  261. }
  262. }
  263. return false;
  264. }
  265. bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool ignoreRegister)
  266. {
  267. origPath = QDir::toNativeSeparators(origPath);
  268. newPath = QDir::toNativeSeparators(newPath);
  269. if (!WarnDirectoryOverwrite(newPath, parent) || (!ignoreRegister && !UnregisterProject(origPath)))
  270. {
  271. return false;
  272. }
  273. QDir newDirectory(newPath);
  274. if (!newDirectory.removeRecursively())
  275. {
  276. return false;
  277. }
  278. if (!newDirectory.rename(origPath, newPath))
  279. {
  280. // Likely failed because trying to move to another partition, try copying
  281. if (!CopyProject(origPath, newPath, parent))
  282. {
  283. return false;
  284. }
  285. DeleteProjectFiles(origPath, true);
  286. }
  287. if (!ignoreRegister && !RegisterProject(newPath))
  288. {
  289. return false;
  290. }
  291. return true;
  292. }
  293. bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
  294. {
  295. QFileInfo original(origFile);
  296. if (original.exists())
  297. {
  298. if (interactive)
  299. {
  300. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  301. parent,
  302. QObject::tr("Overwrite File?"),
  303. QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
  304. QMessageBox::No | QMessageBox::Yes);
  305. if (warningResult == QMessageBox::No)
  306. {
  307. return false;
  308. }
  309. }
  310. if (!QFile::remove(origFile))
  311. {
  312. return false;
  313. }
  314. }
  315. if (!QFile::copy(newFile, origFile))
  316. {
  317. return false;
  318. }
  319. return true;
  320. }
  321. static bool IsVS2019Installed_internal()
  322. {
  323. QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
  324. QString programFilesPath = environment.value("ProgramFiles(x86)");
  325. QString vsWherePath = programFilesPath + "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
  326. QFileInfo vsWhereFile(vsWherePath);
  327. if (vsWhereFile.exists() && vsWhereFile.isFile())
  328. {
  329. QProcess vsWhereProcess;
  330. vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels);
  331. vsWhereProcess.start(
  332. vsWherePath,
  333. QStringList{ "-version", "16.0", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
  334. "-property", "isComplete" });
  335. if (!vsWhereProcess.waitForStarted())
  336. {
  337. return false;
  338. }
  339. while (vsWhereProcess.waitForReadyRead())
  340. {
  341. }
  342. QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
  343. if (vsWhereOutput.startsWith("1"))
  344. {
  345. return true;
  346. }
  347. }
  348. return false;
  349. }
  350. bool IsVS2019Installed()
  351. {
  352. static bool vs2019Installed = IsVS2019Installed_internal();
  353. return vs2019Installed;
  354. }
  355. ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
  356. {
  357. auto iter = s_ProjectManagerStringNames.find(screen);
  358. if (iter != s_ProjectManagerStringNames.end())
  359. {
  360. return iter.value();
  361. }
  362. return ProjectManagerScreen::Invalid;
  363. }
  364. } // namespace ProjectUtils
  365. } // namespace O3DE::ProjectManager