FileViewModel.cs 14 KB

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