FileViewModel.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. using System.Collections.Generic;
  2. using System.IO;
  3. using System.Linq;
  4. using System.Reflection;
  5. using System.Threading.Tasks;
  6. using Avalonia;
  7. using Avalonia.Controls;
  8. using Avalonia.Controls.ApplicationLifetimes;
  9. using Avalonia.Input;
  10. using Avalonia.Platform.Storage;
  11. using ChunkyImageLib;
  12. using Newtonsoft.Json.Linq;
  13. using PixiEditor.AvaloniaUI.Helpers;
  14. using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
  15. using PixiEditor.AvaloniaUI.Models.Controllers;
  16. using PixiEditor.AvaloniaUI.Models.Dialogs;
  17. using PixiEditor.AvaloniaUI.Models.IO;
  18. using PixiEditor.AvaloniaUI.Models.UserData;
  19. using PixiEditor.AvaloniaUI.ViewModels.Document;
  20. using PixiEditor.AvaloniaUI.Views;
  21. using PixiEditor.AvaloniaUI.Views.Dialogs;
  22. using PixiEditor.AvaloniaUI.Views.Windows;
  23. using PixiEditor.ChangeableDocument.Changeables.Graph;
  24. using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
  25. using PixiEditor.DrawingApi.Core;
  26. using PixiEditor.DrawingApi.Core.Numerics;
  27. using PixiEditor.Extensions.Common.Localization;
  28. using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
  29. using PixiEditor.Extensions.Exceptions;
  30. using PixiEditor.Numerics;
  31. using PixiEditor.OperatingSystem;
  32. using PixiEditor.Parser;
  33. using PixiEditor.Parser.Graph;
  34. using PixiEditor.UI.Common.Fonts;
  35. namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
  36. [Command.Group("PixiEditor.File", "FILE")]
  37. internal class FileViewModel : SubViewModel<ViewModelMain>
  38. {
  39. private bool hasRecent;
  40. public bool HasRecent
  41. {
  42. get => hasRecent;
  43. set
  44. {
  45. hasRecent = value;
  46. OnPropertyChanged(nameof(HasRecent));
  47. }
  48. }
  49. public RecentlyOpenedCollection RecentlyOpened { get; init; }
  50. public FileViewModel(ViewModelMain owner)
  51. : base(owner)
  52. {
  53. Owner.OnStartupEvent += Owner_OnStartupEvent;
  54. RecentlyOpened = new RecentlyOpenedCollection(GetRecentlyOpenedDocuments());
  55. if (RecentlyOpened.Count > 0)
  56. {
  57. HasRecent = true;
  58. }
  59. PixiEditorSettings.File.MaxOpenedRecently.ValueChanged += (_, value) => UpdateMaxRecentlyOpened(value);
  60. }
  61. public void AddRecentlyOpened(string path)
  62. {
  63. if (RecentlyOpened.Contains(path))
  64. {
  65. RecentlyOpened.Move(RecentlyOpened.IndexOf(path), 0);
  66. }
  67. else
  68. {
  69. RecentlyOpened.Insert(0, path);
  70. }
  71. int maxCount = PixiEditorSettings.File.MaxOpenedRecently.Value;
  72. while (RecentlyOpened.Count > maxCount)
  73. {
  74. RecentlyOpened.RemoveAt(RecentlyOpened.Count - 1);
  75. }
  76. PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
  77. }
  78. [Command.Internal("PixiEditor.File.RemoveRecent")]
  79. public void RemoveRecentlyOpened(string path)
  80. {
  81. if (!RecentlyOpened.Contains(path))
  82. {
  83. return;
  84. }
  85. RecentlyOpened.Remove(path);
  86. PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
  87. }
  88. private void OpenHelloTherePopup()
  89. {
  90. new HelloTherePopup(this).Show();
  91. }
  92. private void Owner_OnStartupEvent(object sender, System.EventArgs e)
  93. {
  94. List<string> args = StartupArgs.Args;
  95. string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
  96. if (file != null)
  97. {
  98. OpenFromPath(file);
  99. }
  100. else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) &&
  101. !args.Contains("--openedInExisting"))
  102. {
  103. if (PixiEditorSettings.StartupWindow.ShowStartupWindow.Value)
  104. {
  105. OpenHelloTherePopup();
  106. }
  107. }
  108. }
  109. [Command.Internal("PixiEditor.File.OpenRecent")]
  110. public void OpenRecent(string parameter)
  111. {
  112. string path = parameter;
  113. if (!File.Exists(path))
  114. {
  115. NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
  116. RecentlyOpened.Remove(path);
  117. PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
  118. return;
  119. }
  120. OpenFromPath(path);
  121. }
  122. [Command.Basic("PixiEditor.File.Open", "OPEN", "OPEN_FILE", Key = Key.O, Modifiers = KeyModifiers.Control,
  123. MenuItemPath = "FILE/OPEN_FILE", MenuItemOrder = 1, Icon = PixiPerfectIcons.FileText)]
  124. public async Task OpenFromOpenFileDialog()
  125. {
  126. var filter = SupportedFilesHelper.BuildOpenFilter();
  127. if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  128. {
  129. var dialog = await desktop.MainWindow.StorageProvider.OpenFilePickerAsync(
  130. new FilePickerOpenOptions { FileTypeFilter = filter });
  131. if (dialog.Count == 0 || !Importer.IsSupportedFile(dialog[0].Path.LocalPath))
  132. return;
  133. OpenFromPath(dialog[0].Path.LocalPath);
  134. }
  135. }
  136. [Command.Basic("PixiEditor.File.OpenFileFromClipboard", "OPEN_FILE_FROM_CLIPBOARD",
  137. "OPEN_FILE_FROM_CLIPBOARD_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.HasImageInClipboard")]
  138. public async Task OpenFromClipboard()
  139. {
  140. var images = await ClipboardController.GetImagesFromClipboard();
  141. foreach (var dataImage in images)
  142. {
  143. if (File.Exists(dataImage.name))
  144. {
  145. OpenRegularImage(dataImage.image, null);
  146. continue;
  147. }
  148. OpenRegularImage(dataImage.image, null);
  149. }
  150. }
  151. private bool MakeExistingDocumentActiveIfOpened(string path)
  152. {
  153. foreach (DocumentViewModel document in Owner.DocumentManagerSubViewModel.Documents)
  154. {
  155. if (document.FullFilePath is not null &&
  156. System.IO.Path.GetFullPath(document.FullFilePath) == System.IO.Path.GetFullPath(path))
  157. {
  158. Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
  159. return true;
  160. }
  161. }
  162. return false;
  163. }
  164. /// <summary>
  165. /// Tries to open the passed file if it isn't already open
  166. /// </summary>
  167. public void OpenFromPath(string path, bool associatePath = true)
  168. {
  169. if (MakeExistingDocumentActiveIfOpened(path))
  170. return;
  171. try
  172. {
  173. if (path.EndsWith(".pixi"))
  174. {
  175. OpenDotPixi(path, associatePath);
  176. }
  177. else
  178. {
  179. OpenRegularImage(path, associatePath);
  180. }
  181. }
  182. catch (RecoverableException ex)
  183. {
  184. NoticeDialog.Show(ex.DisplayMessage, "ERROR");
  185. }
  186. catch (OldFileFormatException)
  187. {
  188. NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
  189. }
  190. }
  191. /// <summary>
  192. /// Opens a .pixi file from path, creates a document from it, and adds it to the system
  193. /// </summary>
  194. private void OpenDotPixi(string path, bool associatePath = true)
  195. {
  196. DocumentViewModel document = Importer.ImportDocument(path, associatePath);
  197. AddDocumentViewModelToTheSystem(document);
  198. AddRecentlyOpened(document.FullFilePath);
  199. }
  200. /// <summary>
  201. /// Opens a .pixi file from path, creates a document from it, and adds it to the system
  202. /// </summary>
  203. public void OpenRecoveredDotPixi(string? originalPath, byte[] dotPixiBytes)
  204. {
  205. DocumentViewModel document = Importer.ImportDocument(dotPixiBytes, originalPath);
  206. document.MarkAsUnsaved();
  207. AddDocumentViewModelToTheSystem(document);
  208. }
  209. /// <summary>
  210. /// Opens a regular image file from path, creates a document from it, and adds it to the system.
  211. /// </summary>
  212. private void OpenRegularImage(string path, bool associatePath)
  213. {
  214. var image = Importer.ImportImage(path, VecI.NegativeOne);
  215. if (image == null) return;
  216. var doc = NewDocument(b => b
  217. .WithSize(image.Size)
  218. .WithGraph(x => x
  219. .WithImageLayerNode(
  220. new LocalizedString("IMAGE"),
  221. image, out int id)
  222. .WithOutputNode(id, "Output")
  223. ));
  224. if (associatePath)
  225. {
  226. doc.FullFilePath = path;
  227. }
  228. AddRecentlyOpened(path);
  229. }
  230. /// <summary>
  231. /// Opens a regular image file from path, creates a document from it, and adds it to the system.
  232. /// </summary>
  233. private void OpenRegularImage(Surface surface, string path)
  234. {
  235. DocumentViewModel doc = NewDocument(b => b
  236. .WithSize(surface.Size)
  237. .WithGraph(x => x
  238. .WithImageLayerNode(
  239. new LocalizedString("IMAGE"),
  240. surface, out int id)
  241. .WithOutputNode(id, "Output")
  242. ));
  243. if (path == null)
  244. {
  245. return;
  246. }
  247. doc.FullFilePath = path;
  248. AddRecentlyOpened(path);
  249. }
  250. [Command.Basic("PixiEditor.File.New", "NEW_IMAGE", "CREATE_NEW_IMAGE", Key = Key.N,
  251. Modifiers = KeyModifiers.Control,
  252. MenuItemPath = "FILE/NEW_FILE", MenuItemOrder = 0, Icon = PixiPerfectIcons.File)]
  253. public async Task CreateFromNewFileDialog()
  254. {
  255. Window mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
  256. .MainWindow;
  257. NewFileDialog newFile = new NewFileDialog(mainWindow);
  258. if (await newFile.ShowDialog())
  259. {
  260. NewDocument(b => b
  261. .WithSize(newFile.Width, newFile.Height)
  262. .WithGraph(x => x
  263. .WithImageLayerNode(
  264. new LocalizedString("BASE_LAYER_NAME"),
  265. new Surface(new VecI(newFile.Width, newFile.Height)), out int id)
  266. .WithOutputNode(id, "Output")
  267. ));
  268. }
  269. }
  270. public DocumentViewModel NewDocument(Action<DocumentViewModelBuilder> builder)
  271. {
  272. var doc = DocumentViewModel.Build(builder);
  273. AddDocumentViewModelToTheSystem(doc);
  274. return doc;
  275. }
  276. private void AddDocumentViewModelToTheSystem(DocumentViewModel doc)
  277. {
  278. Owner.DocumentManagerSubViewModel.Documents.Add(doc);
  279. Owner.WindowSubViewModel.CreateNewViewport(doc);
  280. Owner.WindowSubViewModel.MakeDocumentViewportActive(doc);
  281. }
  282. [Command.Basic("PixiEditor.File.Save", false, "SAVE", "SAVE_IMAGE", CanExecute = "PixiEditor.HasDocument",
  283. Key = Key.S, Modifiers = KeyModifiers.Control, Icon = PixiPerfectIcons.Save,
  284. MenuItemPath = "FILE/SAVE_PIXI", MenuItemOrder = 3)]
  285. [Command.Basic("PixiEditor.File.SaveAsNew", true, "SAVE_AS", "SAVE_IMAGE_AS", CanExecute = "PixiEditor.HasDocument",
  286. Key = Key.S, Modifiers = KeyModifiers.Control | KeyModifiers.Shift, Icon = PixiPerfectIcons.Save,
  287. MenuItemPath = "FILE/SAVE_AS_PIXI", MenuItemOrder = 4)]
  288. public async Task<bool> SaveActiveDocument(bool asNew)
  289. {
  290. DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
  291. if (doc is null)
  292. return false;
  293. return await SaveDocument(doc, asNew);
  294. }
  295. public async Task<bool> SaveDocument(DocumentViewModel document, bool asNew)
  296. {
  297. string finalPath = null;
  298. if (asNew || string.IsNullOrEmpty(document.FullFilePath))
  299. {
  300. var result = await Exporter.TrySaveWithDialog(document, ExportConfig.Empty);
  301. if (result.Result == DialogSaveResult.Cancelled)
  302. return false;
  303. if (result.Result != DialogSaveResult.Success)
  304. {
  305. ShowSaveError(result.Result);
  306. return false;
  307. }
  308. finalPath = result.Path;
  309. AddRecentlyOpened(result.Path);
  310. }
  311. else
  312. {
  313. var result = await Exporter.TrySaveAsync(document, document.FullFilePath, ExportConfig.Empty);
  314. if (result != SaveResult.Success)
  315. {
  316. ShowSaveError((DialogSaveResult)result);
  317. return false;
  318. }
  319. finalPath = document.FullFilePath;
  320. }
  321. document.FullFilePath = finalPath;
  322. document.MarkAsSaved();
  323. return true;
  324. }
  325. /// <summary>
  326. /// Generates export dialog or saves directly if save data is known.
  327. /// </summary>
  328. /// <param name="parameter">CommandProperty.</param>
  329. [Command.Basic("PixiEditor.File.Export", "EXPORT", "EXPORT_IMAGE", CanExecute = "PixiEditor.HasDocument",
  330. Key = Key.E, Modifiers = KeyModifiers.Control,
  331. MenuItemPath = "FILE/EXPORT_IMG", MenuItemOrder = 5, Icon = PixiPerfectIcons.Image)]
  332. public async Task ExportFile()
  333. {
  334. try
  335. {
  336. DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
  337. if (doc is null)
  338. return;
  339. ExportFileDialog info = new ExportFileDialog(MainWindow.Current, doc.SizeBindable, doc)
  340. {
  341. SuggestedName = Path.GetFileNameWithoutExtension(doc.FileName)
  342. };
  343. if (await info.ShowDialog())
  344. {
  345. var result =
  346. await Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat, info.ExportConfig);
  347. if (result.result == SaveResult.Success)
  348. IOperatingSystem.Current.OpenFolder(result.finalPath);
  349. else
  350. ShowSaveError((DialogSaveResult)result.result);
  351. }
  352. }
  353. catch (RecoverableException e)
  354. {
  355. NoticeDialog.Show(e.DisplayMessage, "ERROR");
  356. }
  357. }
  358. private void ShowSaveError(DialogSaveResult result)
  359. {
  360. switch (result)
  361. {
  362. case DialogSaveResult.InvalidPath:
  363. NoticeDialog.Show("ERROR", "ERROR_SAVE_LOCATION");
  364. break;
  365. case DialogSaveResult.ConcurrencyError:
  366. NoticeDialog.Show("INTERNAL_ERROR", "ERROR_WHILE_SAVING");
  367. break;
  368. case DialogSaveResult.SecurityError:
  369. NoticeDialog.Show(title: "SECURITY_ERROR", message: "SECURITY_ERROR_MSG");
  370. break;
  371. case DialogSaveResult.IoError:
  372. NoticeDialog.Show(title: "IO_ERROR", message: "IO_ERROR_MSG");
  373. break;
  374. case DialogSaveResult.UnknownError:
  375. NoticeDialog.Show("ERROR", "UNKNOWN_ERROR_SAVING");
  376. break;
  377. }
  378. }
  379. private void UpdateMaxRecentlyOpened(int newAmount)
  380. {
  381. if (newAmount >= RecentlyOpened.Count)
  382. {
  383. return;
  384. }
  385. List<RecentlyOpenedDocument> recentlyOpenedDocuments =
  386. new List<RecentlyOpenedDocument>(RecentlyOpened.Take(newAmount));
  387. RecentlyOpened.Clear();
  388. foreach (RecentlyOpenedDocument recent in recentlyOpenedDocuments)
  389. {
  390. RecentlyOpened.Add(recent);
  391. }
  392. }
  393. private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
  394. {
  395. var paths = PixiEditorSettings.File.RecentlyOpened.Value.Take(PixiEditorSettings.File.MaxOpenedRecently.Value);
  396. List<RecentlyOpenedDocument> documents = new List<RecentlyOpenedDocument>();
  397. foreach (string path in paths)
  398. {
  399. if (!File.Exists(path))
  400. continue;
  401. documents.Add(new RecentlyOpenedDocument(path));
  402. }
  403. return documents;
  404. }
  405. }