Editor.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. using Adw;
  2. using Gtk;
  3. namespace Publicate {
  4. public class PpubEditor : Box {
  5. private Adw.HeaderBar header;
  6. private WindowTitle header_title;
  7. private Leaflet leaflet;
  8. private File publication_file;
  9. private ViewerWindow window;
  10. private FileExplorer file_explorer;
  11. private Box tab_box;
  12. private TabView tab_view;
  13. private TabBar tab_bar;
  14. private Button save_tab_button;
  15. private Button remove_file_button;
  16. private SaveProgress progress_window;
  17. private Gee.HashMap<string, Editors.EditorWidget> open_editors = new Gee.HashMap<string, Editors.EditorWidget>();
  18. public PpubEditor(ViewerWindow win) {
  19. window = win;
  20. progress_window = new SaveProgress(window);
  21. orientation = Orientation.VERTICAL;
  22. header = new Adw.HeaderBar();
  23. append(header);
  24. header_title = new WindowTitle("Untitled Publication", "");
  25. header.title_widget = header_title;
  26. var file_box = new Box(Orientation.VERTICAL, 0);
  27. file_box.vexpand = true;
  28. file_explorer = new FileExplorer();
  29. file_explorer.asset_selected.connect(open_asset);
  30. file_explorer.vexpand = true;
  31. file_box.append(file_explorer);
  32. var file_buttons = new Box(Orientation.HORIZONTAL, 0);
  33. file_buttons.add_css_class("linked");
  34. file_buttons.halign = Align.END;
  35. file_buttons.margin_end = 8;
  36. file_buttons.margin_bottom = 8;
  37. file_buttons.margin_start = 8;
  38. file_buttons.margin_top = 8;
  39. file_box.append(file_buttons);
  40. var new_file_button = new Button.from_icon_name("document-new-symbolic");
  41. file_buttons.append(new_file_button);
  42. new_file_button.sensitive = false; // Not yet implemented...
  43. var add_file_button = new Button.from_icon_name("list-add-symbolic");
  44. file_buttons.append(add_file_button);
  45. add_file_button.clicked.connect(import_file);
  46. remove_file_button = new Button.from_icon_name("user-trash-symbolic");
  47. file_buttons.append(remove_file_button);
  48. remove_file_button.clicked.connect(delete_file_clicked);
  49. remove_file_button.sensitive = false;
  50. leaflet = new Leaflet();
  51. leaflet.vexpand = true;
  52. leaflet.append(file_box);
  53. tab_view = new TabView();
  54. tab_bar = new TabBar();
  55. tab_box = new Box(Orientation.VERTICAL, 0);
  56. tab_bar.autohide = false;
  57. tab_bar.set_view(tab_view);
  58. tab_box.append(tab_bar);
  59. tab_box.append(tab_view);
  60. leaflet.append(tab_box);
  61. tab_view.close_page.connect(editor_closing);
  62. save_tab_button = new Button.from_icon_name("document-save-symbolic");
  63. save_tab_button.clicked.connect(save_tab);
  64. header.pack_end(save_tab_button);
  65. tab_view.notify["selected-page"].connect(() => select_file_from_tab());
  66. window.close_request.connect(app_close_request);
  67. append(leaflet);
  68. }
  69. public async void load_ppub(File file) throws Error {
  70. window.publication = new Ppub.Publication(file.get_path());
  71. publication_file = file;
  72. header_title.title = window.publication.metadata.title ?? "Untitled PPUB";
  73. header_title.subtitle = window.publication.metadata.author_name ?? "";
  74. window.title = header_title.title + " - Publicate!";
  75. file_explorer.set_assets(window.publication.assets);
  76. }
  77. public async void open_asset(Ppub.Asset? asset) {
  78. update_deletable();
  79. if(asset == null) {
  80. return;
  81. }
  82. if(open_editors.has_key(asset.name)) {
  83. tab_view.selected_page = open_editors[asset.name].tab_page;
  84. return;
  85. }
  86. if(asset.mimetype == "text/markdown") {
  87. var editor = new Editors.MarkdownEditor(window, tab_view);
  88. add_editor(editor, asset);
  89. }
  90. else if(asset.mimetype.has_prefix("text/")) {
  91. var editor = new Editors.PlainTextEditor(window, tab_view);
  92. add_editor(editor, asset);
  93. }
  94. else if(asset.mimetype == "application/x-ppub-metadata") {
  95. var editor = new Editors.MetadataEditor(window, tab_view);
  96. add_editor(editor, asset);
  97. }
  98. if(asset.mimetype == "application/x-ppvm") {
  99. var editor = new Editors.VideoManifestEditor(window, tab_view);
  100. add_editor(editor, asset);
  101. }
  102. }
  103. private bool editor_closing(TabPage page) {
  104. var editor = (Editors.EditorWidget)page.child;
  105. if(editor.has_unsaved_changes && open_editors.has_key(editor.asset_name)) {
  106. var prompt = new Adw.MessageDialog(window, "Save Changes?", "This editor has unsaved changes, would you like to save them before closing?");
  107. prompt.add_response("cancel", "Cancel");
  108. prompt.add_response("discard", "Discard");
  109. prompt.add_response("save", "Save");
  110. prompt.response.connect(r => {
  111. if(r == "save") {
  112. save_and_close_tab_page.begin(page);
  113. }
  114. else if(r == "discard") {
  115. open_editors.unset(editor.asset_name);
  116. tab_view.close_page(page);
  117. tab_view.close_page_finish(page, true);
  118. }
  119. else {
  120. tab_view.close_page_finish(page, false);
  121. }
  122. });
  123. prompt.present();
  124. return Gdk.EVENT_STOP;
  125. }
  126. open_editors.unset(editor.asset_name);
  127. tab_view.close_page_finish(page, true);
  128. return Gdk.EVENT_STOP;
  129. }
  130. private void add_editor(Editors.EditorWidget editor, Ppub.Asset asset) {
  131. editor.load_asset(window.publication, asset);
  132. open_editors.set(asset.name, editor);
  133. tab_view.selected_page = editor.tab_page;
  134. }
  135. private async void save(Invercargill.Enumerable<Savable> to_save) {
  136. progress_window.display_progress("Saving…", 0.0);
  137. yield do_save(to_save);
  138. progress_window.complete();
  139. }
  140. private async void do_save(Invercargill.Enumerable<Savable> to_save) {
  141. SourceFunc callback = do_save.callback;
  142. ThreadFunc<bool> run = () => {
  143. var items = to_save.count();
  144. var count = 0;
  145. var builder = new Ppub.Builder();
  146. foreach (var item in to_save) {
  147. count++;
  148. item.save_asset(builder);
  149. Idle.add(() => {
  150. progress_window.display_progress("Processing Assets…", (double)count / (double)items);
  151. return false;
  152. });
  153. }
  154. Idle.add(() => {
  155. progress_window.display_progress("Writing PPUB…", 1.0);
  156. return false;
  157. });
  158. var temp_file = File.new_for_path(publication_file.get_path() + ".tmp");
  159. var stream = temp_file.replace(null, false, FileCreateFlags.REPLACE_DESTINATION);
  160. builder.write(stream);
  161. stream.close();
  162. temp_file.move(publication_file, FileCopyFlags.OVERWRITE);
  163. Idle.add(() => {
  164. load_ppub.begin(publication_file, () => {
  165. select_file_from_tab();
  166. callback();
  167. });
  168. return false;
  169. });
  170. return true;
  171. };
  172. new Thread<bool>("saver thread", run);
  173. yield;
  174. }
  175. public async void save_tab() {
  176. var current_editor = (Editors.EditorWidget)tab_view.selected_page.child;
  177. var to_save = window.publication.assets.select<Savable>(a => a.name == current_editor.asset_name ? (Savable)current_editor : new SavableAsset(window.publication, a));
  178. yield save(to_save);
  179. }
  180. private async void save_all() {
  181. var to_save = window.publication.assets.select<Savable>(get_savable_editor_or_asset);
  182. yield save(to_save);
  183. }
  184. private Savable get_savable_editor_or_asset(Ppub.Asset asset) {
  185. if(open_editors.has_key(asset.name)) {
  186. var editor = open_editors[asset.name];
  187. if(editor.has_unsaved_changes) {
  188. return (Savable)editor;
  189. }
  190. }
  191. return new SavableAsset(window.publication, asset);
  192. }
  193. private async void save_and_close_tab_page(TabPage page) {
  194. var editor = (Editors.EditorWidget)page.child;
  195. var to_save = window.publication.assets.select<Savable>(a => a.name == editor.asset_name ? (Savable)editor : new SavableAsset(window.publication, a));
  196. yield save(to_save);
  197. open_editors.unset(editor.asset_name);
  198. tab_view.close_page_finish(page, true);
  199. }
  200. private void select_file_from_tab() {
  201. var editor = (Editors.EditorWidget)tab_view.selected_page.child;
  202. file_explorer.set_selected_item(editor.asset_name);
  203. }
  204. public async void add_asset (string name, string mimetype, GLib.InputStream stream, Ppub.CompressionInfo compression) {
  205. var to_save = window.publication.assets.select<Savable>(a => new SavableAsset(window.publication, a)).to_sequence();
  206. to_save.add(new SavableNewAsset(name, mimetype, stream, compression));
  207. yield save(to_save);
  208. }
  209. public async void import_file() {
  210. var dialog = new FileDialog();
  211. var file = yield dialog.open(window, null);
  212. if(file == null) {
  213. return;
  214. }
  215. var stream = yield file.read_async(Priority.DEFAULT, null);
  216. var compression = new Ppub.CompressionInfo(stream, false);
  217. stream.seek(0, SeekType.SET, null);
  218. var sample = new uint8[2048];
  219. size_t sample_size;
  220. yield stream.read_all_async(sample, Priority.DEFAULT, null, out sample_size);
  221. stream.seek(0, SeekType.SET, null);
  222. var mimetype = Ppub.guess_mimetype(file.get_basename(), sample);
  223. yield add_asset(file.get_basename(), mimetype, stream, compression);
  224. }
  225. private bool app_close_request() {
  226. if(Invercargill.gte(open_editors.values).any(e => e.has_unsaved_changes)) {
  227. var prompt = new Adw.MessageDialog(window, "Save Changes?", "There are editors with unsaved changes, would you like to save them before closing?");
  228. prompt.add_response("cancel", "Cancel");
  229. prompt.add_response("discard", "Discard");
  230. prompt.add_response("save", "Save All");
  231. prompt.response.connect(r => {
  232. if(r == "save") {
  233. save_all_and_close.begin();
  234. }
  235. else if(r == "discard") {
  236. open_editors.clear();
  237. window.close();
  238. }
  239. });
  240. prompt.present();
  241. return Gdk.EVENT_STOP;
  242. }
  243. return Gdk.EVENT_PROPAGATE;
  244. }
  245. private async void save_all_and_close() {
  246. yield save_all();
  247. window.close();
  248. }
  249. private void update_deletable() {
  250. // Inhibit deletion of default document or metadata.
  251. var asset = file_explorer.selected_asset;
  252. remove_file_button.sensitive = (asset != null && asset.name != "metadata" && window.publication.get_default_asset().name != asset.name);
  253. }
  254. private void delete_file_clicked() {
  255. var asset = file_explorer.selected_asset;
  256. if(asset == null) {
  257. return;
  258. }
  259. var prompt = new Adw.MessageDialog(window, "Delete File?", @"Are you sure you want to delete the file\"<i>$(asset.name)</i>\"? This cannot be undone.");
  260. prompt.body_use_markup = true;
  261. prompt.add_response("cancel", "Cancel");
  262. prompt.add_response("delete", "Delete");
  263. prompt.response.connect(r => {
  264. if(r == "delete") {
  265. delete_file.begin(asset);
  266. }
  267. });
  268. prompt.present();
  269. }
  270. private async void delete_file(Ppub.Asset asset) {
  271. var to_save = window.publication.assets
  272. .where(a => a.name != asset.name)
  273. .select<Savable>(a => new SavableAsset(window.publication, a)).to_sequence();
  274. if(open_editors.has_key(asset.name)) {
  275. Editors.EditorWidget editor;
  276. open_editors.unset(asset.name, out editor);
  277. tab_view.close_page(editor.tab_page);
  278. }
  279. yield save(to_save);
  280. }
  281. }
  282. }