using Adw; using Gtk; namespace Publicate { public class PpubEditor : Box { private Adw.HeaderBar header; private WindowTitle header_title; private Leaflet leaflet; private File publication_file; private ViewerWindow window; private FileExplorer file_explorer; private Box tab_box; private TabView tab_view; private TabBar tab_bar; private Button save_tab_button; private Button remove_file_button; private SaveProgress progress_window; private Gee.HashMap open_editors = new Gee.HashMap(); public PpubEditor(ViewerWindow win) { window = win; progress_window = new SaveProgress(window); orientation = Orientation.VERTICAL; header = new Adw.HeaderBar(); append(header); header_title = new WindowTitle("Untitled Publication", ""); header.title_widget = header_title; var file_box = new Box(Orientation.VERTICAL, 0); file_box.vexpand = true; file_explorer = new FileExplorer(); file_explorer.asset_selected.connect(open_asset); file_explorer.vexpand = true; file_box.append(file_explorer); var file_buttons = new Box(Orientation.HORIZONTAL, 0); file_buttons.add_css_class("linked"); file_buttons.halign = Align.END; file_buttons.margin_end = 8; file_buttons.margin_bottom = 8; file_buttons.margin_start = 8; file_buttons.margin_top = 8; file_box.append(file_buttons); var new_file_button = new Button.from_icon_name("document-new-symbolic"); file_buttons.append(new_file_button); new_file_button.sensitive = false; // Not yet implemented... var add_file_button = new Button.from_icon_name("list-add-symbolic"); file_buttons.append(add_file_button); add_file_button.clicked.connect(import_file); remove_file_button = new Button.from_icon_name("user-trash-symbolic"); file_buttons.append(remove_file_button); remove_file_button.clicked.connect(delete_file_clicked); remove_file_button.sensitive = false; leaflet = new Leaflet(); leaflet.vexpand = true; leaflet.append(file_box); tab_view = new TabView(); tab_bar = new TabBar(); tab_box = new Box(Orientation.VERTICAL, 0); tab_bar.autohide = false; tab_bar.set_view(tab_view); tab_box.append(tab_bar); tab_box.append(tab_view); leaflet.append(tab_box); tab_view.close_page.connect(editor_closing); save_tab_button = new Button.from_icon_name("document-save-symbolic"); save_tab_button.clicked.connect(save_tab); header.pack_end(save_tab_button); tab_view.notify["selected-page"].connect(() => select_file_from_tab()); window.close_request.connect(app_close_request); append(leaflet); } public async void load_ppub(File file) throws Error { window.publication = new Ppub.Publication(file.get_path()); publication_file = file; header_title.title = window.publication.metadata.title ?? "Untitled PPUB"; header_title.subtitle = window.publication.metadata.author_name ?? ""; window.title = header_title.title + " - Publicate!"; file_explorer.set_assets(window.publication.assets); } public async void open_asset(Ppub.Asset? asset) { update_deletable(); if(asset == null) { return; } if(open_editors.has_key(asset.name)) { tab_view.selected_page = open_editors[asset.name].tab_page; return; } if(asset.mimetype == "text/markdown") { var editor = new Editors.MarkdownEditor(window, tab_view); add_editor(editor, asset); } else if(asset.mimetype.has_prefix("text/")) { var editor = new Editors.PlainTextEditor(window, tab_view); add_editor(editor, asset); } else if(asset.mimetype == "application/x-ppub-metadata") { var editor = new Editors.MetadataEditor(window, tab_view); add_editor(editor, asset); } if(asset.mimetype == "application/x-ppvm") { var editor = new Editors.VideoManifestEditor(window, tab_view); add_editor(editor, asset); } } private bool editor_closing(TabPage page) { var editor = (Editors.EditorWidget)page.child; if(editor.has_unsaved_changes && open_editors.has_key(editor.asset_name)) { var prompt = new Adw.MessageDialog(window, "Save Changes?", "This editor has unsaved changes, would you like to save them before closing?"); prompt.add_response("cancel", "Cancel"); prompt.add_response("discard", "Discard"); prompt.add_response("save", "Save"); prompt.response.connect(r => { if(r == "save") { save_and_close_tab_page.begin(page); } else if(r == "discard") { open_editors.unset(editor.asset_name); tab_view.close_page(page); tab_view.close_page_finish(page, true); } else { tab_view.close_page_finish(page, false); } }); prompt.present(); return Gdk.EVENT_STOP; } open_editors.unset(editor.asset_name); tab_view.close_page_finish(page, true); return Gdk.EVENT_STOP; } private void add_editor(Editors.EditorWidget editor, Ppub.Asset asset) { editor.load_asset(window.publication, asset); open_editors.set(asset.name, editor); tab_view.selected_page = editor.tab_page; } private async void save(Invercargill.Enumerable to_save) { progress_window.display_progress("Saving…", 0.0); yield do_save(to_save); progress_window.complete(); } private async void do_save(Invercargill.Enumerable to_save) { SourceFunc callback = do_save.callback; ThreadFunc run = () => { var items = to_save.count(); var count = 0; var builder = new Ppub.Builder(); foreach (var item in to_save) { count++; item.save_asset(builder); Idle.add(() => { progress_window.display_progress("Processing Assets…", (double)count / (double)items); return false; }); } Idle.add(() => { progress_window.display_progress("Writing PPUB…", 1.0); return false; }); var temp_file = File.new_for_path(publication_file.get_path() + ".tmp"); var stream = temp_file.replace(null, false, FileCreateFlags.REPLACE_DESTINATION); builder.write(stream); stream.close(); temp_file.move(publication_file, FileCopyFlags.OVERWRITE); Idle.add(() => { load_ppub.begin(publication_file, () => { select_file_from_tab(); callback(); }); return false; }); return true; }; new Thread("saver thread", run); yield; } public async void save_tab() { var current_editor = (Editors.EditorWidget)tab_view.selected_page.child; var to_save = window.publication.assets.select(a => a.name == current_editor.asset_name ? (Savable)current_editor : new SavableAsset(window.publication, a)); yield save(to_save); } private async void save_all() { var to_save = window.publication.assets.select(get_savable_editor_or_asset); yield save(to_save); } private Savable get_savable_editor_or_asset(Ppub.Asset asset) { if(open_editors.has_key(asset.name)) { var editor = open_editors[asset.name]; if(editor.has_unsaved_changes) { return (Savable)editor; } } return new SavableAsset(window.publication, asset); } private async void save_and_close_tab_page(TabPage page) { var editor = (Editors.EditorWidget)page.child; var to_save = window.publication.assets.select(a => a.name == editor.asset_name ? (Savable)editor : new SavableAsset(window.publication, a)); yield save(to_save); open_editors.unset(editor.asset_name); tab_view.close_page_finish(page, true); } private void select_file_from_tab() { var editor = (Editors.EditorWidget)tab_view.selected_page.child; file_explorer.set_selected_item(editor.asset_name); } public async void add_asset (string name, string mimetype, GLib.InputStream stream, Ppub.CompressionInfo compression) { var to_save = window.publication.assets.select(a => new SavableAsset(window.publication, a)).to_sequence(); to_save.add(new SavableNewAsset(name, mimetype, stream, compression)); yield save(to_save); } public async void import_file() { var dialog = new FileDialog(); var file = yield dialog.open(window, null); if(file == null) { return; } var stream = yield file.read_async(Priority.DEFAULT, null); var compression = new Ppub.CompressionInfo(stream, false); stream.seek(0, SeekType.SET, null); var sample = new uint8[2048]; size_t sample_size; yield stream.read_all_async(sample, Priority.DEFAULT, null, out sample_size); stream.seek(0, SeekType.SET, null); var mimetype = Ppub.guess_mimetype(file.get_basename(), sample); yield add_asset(file.get_basename(), mimetype, stream, compression); } private bool app_close_request() { if(Invercargill.gte(open_editors.values).any(e => e.has_unsaved_changes)) { var prompt = new Adw.MessageDialog(window, "Save Changes?", "There are editors with unsaved changes, would you like to save them before closing?"); prompt.add_response("cancel", "Cancel"); prompt.add_response("discard", "Discard"); prompt.add_response("save", "Save All"); prompt.response.connect(r => { if(r == "save") { save_all_and_close.begin(); } else if(r == "discard") { open_editors.clear(); window.close(); } }); prompt.present(); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } private async void save_all_and_close() { yield save_all(); window.close(); } private void update_deletable() { // Inhibit deletion of default document or metadata. var asset = file_explorer.selected_asset; remove_file_button.sensitive = (asset != null && asset.name != "metadata" && window.publication.get_default_asset().name != asset.name); } private void delete_file_clicked() { var asset = file_explorer.selected_asset; if(asset == null) { return; } var prompt = new Adw.MessageDialog(window, "Delete File?", @"Are you sure you want to delete the file\"$(asset.name)\"? This cannot be undone."); prompt.body_use_markup = true; prompt.add_response("cancel", "Cancel"); prompt.add_response("delete", "Delete"); prompt.response.connect(r => { if(r == "delete") { delete_file.begin(asset); } }); prompt.present(); } private async void delete_file(Ppub.Asset asset) { var to_save = window.publication.assets .where(a => a.name != asset.name) .select(a => new SavableAsset(window.publication, a)).to_sequence(); if(open_editors.has_key(asset.name)) { Editors.EditorWidget editor; open_editors.unset(asset.name, out editor); tab_view.close_page(editor.tab_page); } yield save(to_save); } } }