FileViewModel.cs 14 KB

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