From a0d3dbc550f97a371062e02ba676eba603f04a1d Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Fri, 25 Jan 2013 17:57:46 -0800 Subject: [PATCH] Extract Loading/Saving/Undo stuff from AssFile Add SubsController, which deals with things like what subtitle file is currently open, rather than the contents of the current subtitle file. Move the rest of the relevant logic from FrameMain there in addition to all of the stuff from AssFile. --- aegisub/build/Aegisub/Aegisub.vcxproj | 2 + aegisub/build/Aegisub/Aegisub.vcxproj.filters | 6 + aegisub/src/Makefile | 1 + aegisub/src/ass_exporter.cpp | 7 +- aegisub/src/ass_file.cpp | 241 +------------- aegisub/src/ass_file.h | 88 +---- aegisub/src/audio_controller.cpp | 5 +- aegisub/src/auto4_base.cpp | 7 +- aegisub/src/auto4_lua.cpp | 5 +- aegisub/src/base_grid.cpp | 5 +- aegisub/src/command/edit.cpp | 27 +- aegisub/src/command/recent.cpp | 4 +- aegisub/src/command/subtitle.cpp | 22 +- aegisub/src/dialog_export.cpp | 1 + aegisub/src/dialog_fonts_collector.cpp | 1 + aegisub/src/dialog_shift_times.cpp | 3 +- aegisub/src/dialog_style_manager.cpp | 7 +- aegisub/src/frame_main.cpp | 121 +------ aegisub/src/frame_main.h | 9 +- aegisub/src/include/aegisub/context.h | 2 + aegisub/src/main.cpp | 21 +- aegisub/src/subs_controller.cpp | 311 ++++++++++++++++++ aegisub/src/subs_controller.h | 110 +++++++ aegisub/src/subs_edit_box.h | 2 +- aegisub/src/subtitle_format_encore.cpp | 1 + aegisub/src/subtitle_format_transtation.cpp | 1 + aegisub/src/subtitles_provider_csri.cpp | 5 +- aegisub/src/subtitles_provider_libass.cpp | 13 +- aegisub/src/video_context.cpp | 3 +- aegisub/src/video_display.cpp | 4 +- 30 files changed, 564 insertions(+), 471 deletions(-) create mode 100644 aegisub/src/subs_controller.cpp create mode 100644 aegisub/src/subs_controller.h diff --git a/aegisub/build/Aegisub/Aegisub.vcxproj b/aegisub/build/Aegisub/Aegisub.vcxproj index 6f2a3f842..83bdf3af9 100644 --- a/aegisub/build/Aegisub/Aegisub.vcxproj +++ b/aegisub/build/Aegisub/Aegisub.vcxproj @@ -265,6 +265,7 @@ + @@ -458,6 +459,7 @@ + diff --git a/aegisub/build/Aegisub/Aegisub.vcxproj.filters b/aegisub/build/Aegisub/Aegisub.vcxproj.filters index da5ea5304..dd1a771bb 100644 --- a/aegisub/build/Aegisub/Aegisub.vcxproj.filters +++ b/aegisub/build/Aegisub/Aegisub.vcxproj.filters @@ -687,6 +687,9 @@ Utilities + + ASS + @@ -1235,6 +1238,9 @@ Utilities + + ASS + diff --git a/aegisub/src/Makefile b/aegisub/src/Makefile index ad47d3e48..6097b0db6 100644 --- a/aegisub/src/Makefile +++ b/aegisub/src/Makefile @@ -221,6 +221,7 @@ SRC += \ spline_curve.cpp \ standard_paths.cpp \ string_codec.cpp \ + subs_controller.cpp \ subs_edit_box.cpp \ subs_edit_ctrl.cpp \ subs_grid.cpp \ diff --git a/aegisub/src/ass_exporter.cpp b/aegisub/src/ass_exporter.cpp index 894e4ab42..43f704b49 100644 --- a/aegisub/src/ass_exporter.cpp +++ b/aegisub/src/ass_exporter.cpp @@ -40,6 +40,7 @@ #include "ass_file.h" #include "compat.h" #include "include/aegisub/context.h" +#include "subtitle_format.h" #include #include @@ -106,7 +107,11 @@ AssFile *AssExporter::ExportTransform(wxWindow *export_dialog, bool copy) { void AssExporter::Export(agi::fs::path const& filename, std::string const& charset, wxWindow *export_dialog) { std::unique_ptr subs(ExportTransform(export_dialog, true)); - subs->Save(filename, false, false, charset); + const SubtitleFormat *writer = SubtitleFormat::GetWriter(filename); + if (!writer) + throw "Unknown file type."; + + writer->WriteFile(subs.get(), filename, charset); } wxSizer *AssExporter::GetSettingsSizer(std::string const& name) { diff --git a/aegisub/src/ass_file.cpp b/aegisub/src/ass_file.cpp index a88595770..44bb73544 100644 --- a/aegisub/src/ass_file.cpp +++ b/aegisub/src/ass_file.cpp @@ -40,23 +40,14 @@ #include "ass_info.h" #include "ass_style.h" #include "options.h" -#include "standard_paths.h" -#include "subtitle_format.h" -#include "text_file_reader.h" -#include "text_file_writer.h" #include "utils.h" #include -#include #include -#include #include #include -#include -#include -#include -#include +#include namespace std { template<> @@ -65,136 +56,12 @@ namespace std { } } -AssFile::AssFile () -: commitId(0) -{ -} - AssFile::~AssFile() { auto copy = new EntryList; copy->swap(Line); agi::dispatch::Background().Async([=]{ delete copy; }); } -/// @brief Load generic subs -void AssFile::Load(agi::fs::path const& filename, std::string const& charset) { - const SubtitleFormat *reader = SubtitleFormat::GetReader(filename); - - try { - AssFile temp; - reader->ReadFile(&temp, filename, charset); - - bool found_style = false; - bool found_dialogue = false; - - // Check if the file has at least one style and at least one dialogue line - for (auto const& line : temp.Line) { - AssEntryGroup type = line.Group(); - if (type == ENTRY_STYLE) found_style = true; - if (type == ENTRY_DIALOGUE) found_dialogue = true; - if (found_style && found_dialogue) break; - } - - // And if it doesn't add defaults for each - if (!found_style) - temp.InsertLine(new AssStyle); - if (!found_dialogue) - temp.InsertLine(new AssDialogue); - - swap(temp); - } - catch (agi::UserCancelException const&) { - return; - } - - // Set general data - this->filename = filename; - - // Add comments and set vars - SetScriptInfo("ScriptType", "v4.00+"); - - // Push the initial state of the file onto the undo stack - UndoStack.clear(); - RedoStack.clear(); - undoDescription.clear(); - autosavedCommitId = savedCommitId = commitId + 1; - Commit("", COMMIT_NEW); - - FileOpen(filename); -} - -void AssFile::Save(agi::fs::path const& filename, bool setfilename, bool addToRecent, std::string const& encoding) { - const SubtitleFormat *writer = SubtitleFormat::GetWriter(filename); - if (!writer) - throw "Unknown file type."; - - if (setfilename) { - autosavedCommitId = savedCommitId = commitId; - this->filename = filename; - StandardPaths::SetPathValue("?script", filename.parent_path()); - } - - FileSave(); - - writer->WriteFile(this, filename, encoding); - - if (addToRecent) - AddToRecent(filename); -} - -agi::fs::path AssFile::AutoSave() { - if (commitId == autosavedCommitId) - return ""; - - auto path = StandardPaths::DecodePath(OPT_GET("Path/Auto/Save")->GetString()); - if (path.empty()) - path = filename.parent_path(); - - agi::fs::CreateDirectory(path); - - auto name = filename.filename(); - if (name.empty()) - name = "Untitled"; - - path /= str(boost::format("%s.%s.AUTOSAVE.ass") % name % agi::util::strftime("%Y-%m-%d-%H-%M-%S")); - - Save(path, false, false); - - autosavedCommitId = commitId; - - return path; -} - -void AssFile::SaveMemory(std::vector &dst) { - // Check if subs contain at least one style - // Add a default style if they don't for compatibility with libass/asa - if (GetStyles().empty()) - InsertLine(new AssStyle); - - // Prepare vector - dst.clear(); - dst.reserve(0x4000); - - // Write file - AssEntryGroup group = ENTRY_GROUP_MAX; - for (auto const& line : Line) { - if (group != line.Group()) { - group = line.Group(); - boost::push_back(dst, line.GroupHeader() + "\r\n"); - } - boost::push_back(dst, line.GetEntryData() + "\r\n"); - } -} - -bool AssFile::CanSave() const { - try { - return SubtitleFormat::GetWriter(filename)->CanSave(this); - } - catch (...) { - return false; - } -} - void AssFile::LoadDefault(bool defline) { Line.push_back(*new AssInfo("Title", "Default Aegisub file")); Line.push_back(*new AssInfo("ScriptType", "v4.00+")); @@ -211,26 +78,13 @@ void AssFile::LoadDefault(bool defline) { if (defline) Line.push_back(*new AssDialogue); - - autosavedCommitId = savedCommitId = commitId + 1; - Commit("", COMMIT_NEW); - StandardPaths::SetPathValue("?script", ""); - FileOpen(""); } void AssFile::swap(AssFile &that) throw() { - // Intentionally does not swap undo stack related things - using std::swap; - swap(commitId, that.commitId); - swap(undoDescription, that.undoDescription); - swap(Line, that.Line); + Line.swap(that.Line); } -AssFile::AssFile(const AssFile &from) -: undoDescription(from.undoDescription) -, commitId(from.commitId) -, filename(from.filename) -{ +AssFile::AssFile(const AssFile &from) { Line.clone_from(from.Line, std::mem_fun_ref(&AssEntry::Clone), delete_ptr()); } AssFile& AssFile::operator=(AssFile from) { @@ -332,83 +186,17 @@ AssStyle *AssFile::GetStyle(std::string const& name) { return nullptr; } -void AssFile::AddToRecent(agi::fs::path const& file) const { - config::mru->Add("Subtitle", file); - OPT_SET("Path/Last/Subtitles")->SetString(file.parent_path().string()); -} +int AssFile::Commit(wxString const& desc, int type, int amend_id, AssEntry *single_line) { + AssFileCommit c = { desc, &amend_id, single_line }; + PushState(c); -int AssFile::Commit(wxString const& desc, int type, int amendId, AssEntry *single_line) { std::set changed_lines; if (single_line) changed_lines.insert(single_line); - ++commitId; - // Allow coalescing only if it's the last change and the file has not been - // saved since the last change - if (commitId == amendId+1 && RedoStack.empty() && savedCommitId+1 != commitId && autosavedCommitId+1 != commitId) { - // If only one line changed just modify it instead of copying the file - if (single_line) { - entryIter this_it = Line.begin(), undo_it = UndoStack.back().Line.begin(); - while (&*this_it != single_line) { - ++this_it; - ++undo_it; - } - UndoStack.back().Line.insert(undo_it, *single_line->Clone()); - delete &*undo_it; - } - else { - UndoStack.back() = *this; - } - AnnounceCommit(type, changed_lines); - return commitId; - } - - RedoStack.clear(); - - // Place copy on stack - undoDescription = desc; - UndoStack.push_back(*this); - - // Cap depth - int depth = std::max(OPT_GET("Limits/Undo Levels")->GetInt(), 2); - while ((int)UndoStack.size() > depth) { - UndoStack.pop_front(); - } - - if (UndoStack.size() > 1 && OPT_GET("App/Auto/Save on Every Change")->GetBool() && !filename.empty() && CanSave()) - Save(filename); - AnnounceCommit(type, changed_lines); - return commitId; -} -void AssFile::Undo() { - if (UndoStack.size() <= 1) return; - - RedoStack.emplace_back(); - swap(RedoStack.back()); - UndoStack.pop_back(); - *this = UndoStack.back(); - - AnnounceCommit(COMMIT_NEW, std::set()); -} - -void AssFile::Redo() { - if (RedoStack.empty()) return; - - swap(RedoStack.back()); - UndoStack.push_back(*this); - RedoStack.pop_back(); - - AnnounceCommit(COMMIT_NEW, std::set()); -} - -wxString AssFile::GetUndoDescription() const { - return IsUndoStackEmpty() ? "" : UndoStack.back().undoDescription; -} - -wxString AssFile::GetRedoDescription() const { - return IsRedoStackEmpty() ? "" : RedoStack.back().undoDescription; + return amend_id; } bool AssFile::CompStart(const AssDialogue* lft, const AssDialogue* rgt) { @@ -434,13 +222,6 @@ void AssFile::Sort(CompFunc comp, std::set const& limit) { Sort(Line, comp, limit); } namespace { - struct AssEntryComp : public std::binary_function { - AssFile::CompFunc comp; - bool operator()(AssEntry const&a, AssEntry const&b) const { - return comp(static_cast(&a), static_cast(&b)); - } - }; - inline bool is_dialogue(AssEntry *e, std::set const& limit) { AssDialogue *d = dynamic_cast(e); return d && (limit.empty() || limit.count(d)); @@ -448,8 +229,10 @@ namespace { } void AssFile::Sort(EntryList &lst, CompFunc comp, std::set const& limit) { - AssEntryComp compE; - compE.comp = comp; + auto compE = [&](AssEntry const& a, AssEntry const& b) { + return comp(static_cast(&a), static_cast(&b)); + }; + // Sort each block of AssDialogues separately, leaving everything else untouched for (entryIter begin = lst.begin(); begin != lst.end(); ++begin) { if (!is_dialogue(&*begin, limit)) continue; @@ -465,5 +248,3 @@ void AssFile::Sort(EntryList &lst, CompFunc comp, std::set const& begin = --end; } } - -AssFile *AssFile::top; diff --git a/aegisub/src/ass_file.h b/aegisub/src/ass_file.h index 3e2a87a7a..932daf0d7 100644 --- a/aegisub/src/ass_file.h +++ b/aegisub/src/ass_file.h @@ -32,61 +32,43 @@ /// @ingroup subs_storage /// -#include -#include -#include -#include -#include -#include +#include "ass_entry.h" #include #include -#include "ass_entry.h" +#include +#include +#include class AssDialogue; class AssStyle; class AssAttachment; +class wxString; typedef boost::intrusive::make_list>::type EntryList; typedef EntryList::iterator entryIter; typedef EntryList::const_iterator constEntryIter; +struct AssFileCommit { + wxString const& message; + int *commit_id; + AssEntry *single_line; +}; + class AssFile { - boost::container::list UndoStack; - boost::container::list RedoStack; - wxString undoDescription; - /// Revision counter for undo coalescing and modified state tracking - int commitId; - /// Last saved version of this file - int savedCommitId; - /// Last autosaved version of this file - int autosavedCommitId; - - /// A set of changes has been committed to the file (AssFile::CommitType) + /// A set of changes has been committed to the file (AssFile::COMMITType) agi::signal::Signal const&> AnnounceCommit; - /// A new file has been opened (filename) - agi::signal::Signal FileOpen; - /// The file is about to be saved - /// This signal is intended for adding metadata such as video filename, - /// frame number, etc. Ideally this would all be done immediately rather - /// than waiting for a save, but that causes (more) issues with undo - agi::signal::Signal<> FileSave; - + agi::signal::Signal PushState; public: /// The lines in the file EntryList Line; - /// The filename of this file, if any - agi::fs::path filename; - AssFile(); + AssFile() { } AssFile(const AssFile &from); AssFile& operator=(AssFile from); ~AssFile(); - /// Does the file have unsaved changes? - bool IsModified() const { return commitId != savedCommitId; }; - /// @brief Load default file /// @param defline Add a blank line to the file void LoadDefault(bool defline=true); @@ -103,30 +85,6 @@ public: void swap(AssFile &) throw(); - /// @brief Load from a file - /// @param file File name - /// @param charset Character set of file or empty to autodetect - void Load(agi::fs::path const& file, std::string const& charset=""); - - /// @brief Save to a file - /// @param file Path to save to - /// @param setfilename Should the filename be changed to the passed path? - /// @param addToRecent Should the file be added to the MRU list? - /// @param encoding Encoding to use, or empty to let the writer decide (which usually means "App/Save Charset") - void Save(agi::fs::path const& file, bool setfilename=false, bool addToRecent=true, std::string const& encoding=""); - - /// @brief Autosave the file if there have been any chances since the last autosave - /// @return File name used or empty if no save was performed - agi::fs::path AutoSave(); - - /// @brief Save to a memory buffer. Used for subtitle providers which support it - /// @param[out] dst Destination vector - void SaveMemory(std::vector &dst); - /// Add file name to the MRU list - void AddToRecent(agi::fs::path const& file) const; - /// Can the file be saved in its current format? - bool CanSave() const; - /// @brief Get the script resolution /// @param[out] w Width /// @param[in] h Height @@ -169,8 +127,7 @@ public: }; DEFINE_SIGNAL_ADDERS(AnnounceCommit, AddCommitListener) - DEFINE_SIGNAL_ADDERS(FileOpen, AddFileOpenListener) - DEFINE_SIGNAL_ADDERS(FileSave, AddFileSaveListener) + DEFINE_SIGNAL_ADDERS(PushState, AddUndoManager) /// @brief Flag the file as modified and push a copy onto the undo stack /// @param desc Undo description @@ -179,21 +136,6 @@ public: /// @param single_line Line which was changed, if only one line was /// @return Unique identifier for the new undo group int Commit(wxString const& desc, int type, int commitId = -1, AssEntry *single_line = 0); - /// @brief Undo the last set of changes to the file - void Undo(); - /// @brief Redo the last undone changes - void Redo(); - /// Check if undo stack is empty - bool IsUndoStackEmpty() const { return UndoStack.size() <= 1; }; - /// Check if redo stack is empty - bool IsRedoStackEmpty() const { return RedoStack.empty(); }; - /// Get the description of the first undoable change - wxString GetUndoDescription() const; - /// Get the description of the first redoable change - wxString GetRedoDescription() const; - - /// Current script file. It is "above" the stack. - static AssFile *top; /// Comparison function for use when sorting typedef bool (*CompFunc)(const AssDialogue* lft, const AssDialogue* rgt); diff --git a/aegisub/src/audio_controller.cpp b/aegisub/src/audio_controller.cpp index dc3427362..5abd7fa3c 100644 --- a/aegisub/src/audio_controller.cpp +++ b/aegisub/src/audio_controller.cpp @@ -46,6 +46,7 @@ #include "pen.h" #include "options.h" #include "selection_controller.h" +#include "subs_controller.h" #include "utils.h" #include "video_context.h" @@ -56,7 +57,7 @@ AudioController::AudioController(agi::Context *context) : context(context) -, subtitle_save_slot(context->ass->AddFileSaveListener(&AudioController::OnSubtitlesSave, this)) +, subtitle_save_slot(context->subsController->AddFileSaveListener(&AudioController::OnSubtitlesSave, this)) , player(0) , provider(0) , playback_mode(PM_NotPlaying) @@ -238,9 +239,7 @@ void AudioController::SetTimingController(AudioTimingController *new_controller) void AudioController::OnTimingControllerUpdatedPrimaryRange() { if (playback_mode == PM_PrimaryRange) - { player->SetEndPosition(SamplesFromMilliseconds(timing_controller->GetPrimaryPlaybackRange().end())); - } } void AudioController::OnSubtitlesSave() diff --git a/aegisub/src/auto4_base.cpp b/aegisub/src/auto4_base.cpp index 455f2576a..b479bba81 100644 --- a/aegisub/src/auto4_base.cpp +++ b/aegisub/src/auto4_base.cpp @@ -44,6 +44,7 @@ #include "options.h" #include "standard_paths.h" #include "string_codec.h" +#include "subs_controller.h" #include "subtitle_format.h" #include "utils.h" @@ -379,8 +380,8 @@ namespace Automation4 { LocalScriptManager::LocalScriptManager(agi::Context *c) : context(c) { - slots.push_back(c->ass->AddFileSaveListener(&LocalScriptManager::OnSubtitlesSave, this)); - slots.push_back(c->ass->AddFileOpenListener(&LocalScriptManager::Reload, this)); + slots.push_back(c->subsController->AddFileSaveListener(&LocalScriptManager::OnSubtitlesSave, this)); + slots.push_back(c->subsController->AddFileOpenListener(&LocalScriptManager::Reload, this)); } void LocalScriptManager::Reload() @@ -403,7 +404,7 @@ namespace Automation4 { agi::fs::path basepath; if (first_char == '~') { - basepath = context->ass->filename.parent_path(); + basepath = context->subsController->Filename().parent_path(); } else if (first_char == '$') { basepath = autobasefn; } else if (first_char == '/') { diff --git a/aegisub/src/auto4_lua.cpp b/aegisub/src/auto4_lua.cpp index 566957524..d19e2dbc5 100644 --- a/aegisub/src/auto4_lua.cpp +++ b/aegisub/src/auto4_lua.cpp @@ -47,6 +47,7 @@ #include "include/aegisub/context.h" #include "main.h" #include "selection_controller.h" +#include "subs_controller.h" #include "standard_paths.h" #include "video_context.h" #include "utils.h" @@ -159,8 +160,8 @@ namespace { int get_file_name(lua_State *L) { const agi::Context *c = get_context(L); - if (c && !c->ass->filename.empty()) - push_value(L, c->ass->filename.filename()); + if (c && !c->subsController->Filename().empty()) + push_value(L, c->subsController->Filename().filename()); else lua_pushnil(L); return 1; diff --git a/aegisub/src/base_grid.cpp b/aegisub/src/base_grid.cpp index ad159ea99..d62e4e3da 100644 --- a/aegisub/src/base_grid.cpp +++ b/aegisub/src/base_grid.cpp @@ -58,6 +58,7 @@ #include "frame_main.h" #include "options.h" #include "utils.h" +#include "subs_controller.h" #include "video_context.h" #include "video_slider.h" @@ -117,8 +118,8 @@ BaseGrid::BaseGrid(wxWindow* parent, agi::Context *context, const wxSize& size, OPT_SUB("Subtitle/Grid/Font Size", &BaseGrid::UpdateStyle, this); OPT_SUB("Subtitle/Grid/Highlight Subtitles in Frame", &BaseGrid::OnHighlightVisibleChange, this); context->ass->AddCommitListener(&BaseGrid::OnSubtitlesCommit, this); - context->ass->AddFileOpenListener(&BaseGrid::OnSubtitlesOpen, this); - context->ass->AddFileSaveListener(&BaseGrid::OnSubtitlesSave, this); + context->subsController->AddFileOpenListener(&BaseGrid::OnSubtitlesOpen, this); + context->subsController->AddFileSaveListener(&BaseGrid::OnSubtitlesSave, this); OPT_SUB("Colour/Subtitle Grid/Active Border", &BaseGrid::UpdateStyle, this); OPT_SUB("Colour/Subtitle Grid/Background/Background", &BaseGrid::UpdateStyle, this); diff --git a/aegisub/src/command/edit.cpp b/aegisub/src/command/edit.cpp index c5872dec1..7fa6c114e 100644 --- a/aegisub/src/command/edit.cpp +++ b/aegisub/src/command/edit.cpp @@ -50,6 +50,7 @@ #include "../initial_line_state.h" #include "../options.h" #include "../search_replace_engine.h" +#include "../subs_controller.h" #include "../subs_edit_ctrl.h" #include "../subs_grid.h" #include "../text_selection_controller.h" @@ -307,7 +308,7 @@ void show_color_picker(const agi::Context *c, agi::Color (AssStyle::*field), con commit_text(c, _("set color"), -1, -1, &commit_id); if (!ok) { - c->ass->Undo(); + c->subsController->Undo(); c->textSelectionController->SetSelection(initial_sel_start, initial_sel_end); } } @@ -876,22 +877,22 @@ struct edit_redo : public Command { CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME) wxString StrMenu(const agi::Context *c) const { - return c->ass->IsRedoStackEmpty() ? + return c->subsController->IsRedoStackEmpty() ? _("Nothing to &redo") : - wxString::Format(_("&Redo %s"), c->ass->GetRedoDescription()); + wxString::Format(_("&Redo %s"), c->subsController->GetRedoDescription()); } wxString StrDisplay(const agi::Context *c) const { - return c->ass->IsRedoStackEmpty() ? + return c->subsController->IsRedoStackEmpty() ? _("Nothing to redo") : - wxString::Format(_("Redo %s"), c->ass->GetRedoDescription()); + wxString::Format(_("Redo %s"), c->subsController->GetRedoDescription()); } bool Validate(const agi::Context *c) { - return !c->ass->IsRedoStackEmpty(); + return !c->subsController->IsRedoStackEmpty(); } void operator()(agi::Context *c) { - c->ass->Redo(); + c->subsController->Redo(); } }; @@ -902,22 +903,22 @@ struct edit_undo : public Command { CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME) wxString StrMenu(const agi::Context *c) const { - return c->ass->IsUndoStackEmpty() ? + return c->subsController->IsUndoStackEmpty() ? _("Nothing to &undo") : - wxString::Format(_("&Undo %s"), c->ass->GetUndoDescription()); + wxString::Format(_("&Undo %s"), c->subsController->GetUndoDescription()); } wxString StrDisplay(const agi::Context *c) const { - return c->ass->IsUndoStackEmpty() ? + return c->subsController->IsUndoStackEmpty() ? _("Nothing to undo") : - wxString::Format(_("Undo %s"), c->ass->GetUndoDescription()); + wxString::Format(_("Undo %s"), c->subsController->GetUndoDescription()); } bool Validate(const agi::Context *c) { - return !c->ass->IsUndoStackEmpty(); + return !c->subsController->IsUndoStackEmpty(); } void operator()(agi::Context *c) { - c->ass->Undo(); + c->subsController->Undo(); } }; diff --git a/aegisub/src/command/recent.cpp b/aegisub/src/command/recent.cpp index b448e4ea3..c641e4c28 100644 --- a/aegisub/src/command/recent.cpp +++ b/aegisub/src/command/recent.cpp @@ -38,10 +38,10 @@ #include "../audio_controller.h" #include "../compat.h" -#include "../frame_main.h" #include "../include/aegisub/context.h" #include "../main.h" #include "../options.h" +#include "../subs_controller.h" #include "../video_context.h" #include @@ -93,7 +93,7 @@ struct recent_subtitle_entry : public Command { STR_HELP("Open recent subtitles") void operator()(agi::Context *c, int id) { - wxGetApp().frame->LoadSubtitles(config::mru->GetEntry("Subtitle", id)); + c->subsController->Load(config::mru->GetEntry("Subtitle", id)); } }; diff --git a/aegisub/src/command/subtitle.cpp b/aegisub/src/command/subtitle.cpp index 6381dfbcf..e8627f47d 100644 --- a/aegisub/src/command/subtitle.cpp +++ b/aegisub/src/command/subtitle.cpp @@ -47,12 +47,12 @@ #include "../dialog_properties.h" #include "../dialog_search_replace.h" #include "../dialog_spellchecker.h" -#include "../frame_main.h" #include "../include/aegisub/context.h" #include "../main.h" #include "../options.h" #include "../search_replace_engine.h" #include "../selection_controller.h" +#include "../subs_controller.h" #include "../subtitle_format.h" #include "../utils.h" #include "../video_context.h" @@ -246,8 +246,8 @@ struct subtitle_new : public Command { STR_HELP("New subtitles") void operator()(agi::Context *c) { - if (wxGetApp().frame->TryToCloseSubs() != wxCANCEL) - c->ass->LoadDefault(); + if (c->subsController->TryToClose() != wxCANCEL) + c->subsController->Close(); } }; @@ -262,7 +262,7 @@ struct subtitle_open : public Command { void operator()(agi::Context *c) { auto filename = OpenFileSelector(_("Open subtitles file"), "Path/Last/Subtitles", "","", SubtitleFormat::GetWildcards(0), c->parent); if (!filename.empty()) - wxGetApp().frame->LoadSubtitles(filename); + c->subsController->Load(filename); } }; @@ -275,7 +275,7 @@ struct subtitle_open_autosave : public Command { void operator()(agi::Context *c) { DialogAutosave dialog(c->parent); if (dialog.ShowModal() == wxID_OK) - wxGetApp().frame->LoadSubtitles(dialog.ChosenFile()); + c->subsController->Load(dialog.ChosenFile()); } }; @@ -293,7 +293,7 @@ struct subtitle_open_charset : public Command { wxString charset = wxGetSingleChoice(_("Choose charset code:"), _("Charset"), agi::charset::GetEncodingsList(), c->parent, -1, -1, true, 250, 200); if (charset.empty()) return; - wxGetApp().frame->LoadSubtitles(filename, from_wx(charset)); + c->subsController->Load(filename, from_wx(charset)); } }; @@ -306,7 +306,7 @@ struct subtitle_open_video : public Command { CMD_TYPE(COMMAND_VALIDATE) void operator()(agi::Context *c) { - wxGetApp().frame->LoadSubtitles(c->videoController->GetVideoName(), "binary"); + c->subsController->Load(c->videoController->GetVideoName(), "binary"); } bool Validate(const agi::Context *c) { @@ -332,13 +332,13 @@ static void save_subtitles(agi::Context *c, agi::fs::path filename) { if (filename.empty()) { c->videoController->Stop(); filename = SaveFileSelector(_("Save subtitles file"), "Path/Last/Subtitles", - c->ass->filename.stem().string() + ".ass", "ass", + c->subsController->Filename().stem().string() + ".ass", "ass", "Advanced Substation Alpha (*.ass)|*.ass", c->parent); if (filename.empty()) return; } try { - c->ass->Save(filename, true, true); + c->subsController->Save(filename); } catch (const agi::Exception& err) { wxMessageBox(to_wx(err.GetMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, c->parent); @@ -360,11 +360,11 @@ struct subtitle_save : public Command { CMD_TYPE(COMMAND_VALIDATE) void operator()(agi::Context *c) { - save_subtitles(c, c->ass->CanSave() ? c->ass->filename : ""); + save_subtitles(c, c->subsController->CanSave() ? c->subsController->Filename() : ""); } bool Validate(const agi::Context *c) { - return c->ass->IsModified(); + return c->subsController->IsModified(); } }; diff --git a/aegisub/src/dialog_export.cpp b/aegisub/src/dialog_export.cpp index 14c410330..71cfda823 100644 --- a/aegisub/src/dialog_export.cpp +++ b/aegisub/src/dialog_export.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include diff --git a/aegisub/src/dialog_fonts_collector.cpp b/aegisub/src/dialog_fonts_collector.cpp index 13db7c8eb..b21fd15f3 100644 --- a/aegisub/src/dialog_fonts_collector.cpp +++ b/aegisub/src/dialog_fonts_collector.cpp @@ -35,6 +35,7 @@ #include "scintilla_text_ctrl.h" #include "selection_controller.h" #include "standard_paths.h" +#include "subs_controller.h" #include "utils.h" #include diff --git a/aegisub/src/dialog_shift_times.cpp b/aegisub/src/dialog_shift_times.cpp index 6b8737789..848ad958e 100644 --- a/aegisub/src/dialog_shift_times.cpp +++ b/aegisub/src/dialog_shift_times.cpp @@ -31,6 +31,7 @@ #include "help_button.h" #include "libresrc/libresrc.h" #include "options.h" +#include "subs_controller.h" #include "standard_paths.h" #include "timeedit_ctrl.h" #include "video_context.h" @@ -278,7 +279,7 @@ void DialogShiftTimes::OnHistoryClick(wxCommandEvent &evt) { void DialogShiftTimes::SaveHistory(json::Array const& shifted_blocks) { json::Object new_entry; - new_entry["filename"] = context->ass->filename.filename().string(); + new_entry["filename"] = context->subsController->Filename().filename().string(); new_entry["is by time"] = shift_by_time->GetValue(); new_entry["is backward"] = shift_backward->GetValue(); new_entry["amount"] = from_wx(shift_by_time->GetValue() ? shift_time->GetValue() : shift_frames->GetValue()); diff --git a/aegisub/src/dialog_style_manager.cpp b/aegisub/src/dialog_style_manager.cpp index dca7f8587..27940848e 100644 --- a/aegisub/src/dialog_style_manager.cpp +++ b/aegisub/src/dialog_style_manager.cpp @@ -47,6 +47,7 @@ #include "options.h" #include "persist_location.h" #include "selection_controller.h" +#include "subs_controller.h" #include "standard_paths.h" #include "subtitle_format.h" #include "utils.h" @@ -565,7 +566,11 @@ void DialogStyleManager::OnCurrentImport() { AssFile temp; try { - temp.Load(filename); + auto reader = SubtitleFormat::GetReader(filename); + if (!reader) + wxMessageBox("Unsupported subtitle format", "Error", wxOK | wxICON_ERROR | wxCENTER, this); + else + reader->ReadFile(&temp, filename); } catch (agi::Exception const& err) { wxMessageBox(to_wx(err.GetChainedMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, this); diff --git a/aegisub/src/frame_main.cpp b/aegisub/src/frame_main.cpp index bc8fbb939..849ce44b5 100644 --- a/aegisub/src/frame_main.cpp +++ b/aegisub/src/frame_main.cpp @@ -60,10 +60,10 @@ #include "main.h" #include "options.h" #include "search_replace_engine.h" +#include "subs_controller.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" #include "subs_grid.h" -#include "text_file_reader.h" #include "utils.h" #include "version.h" #include "video_box.h" @@ -213,18 +213,20 @@ FrameMain::FrameMain (wxArrayString args) StartupLog("Initializing context models"); memset(context.get(), 0, sizeof(*context)); - AssFile::top = context->ass = new AssFile; - context->ass->AddCommitListener(&FrameMain::UpdateTitle, this); - context->ass->AddFileOpenListener(&FrameMain::OnSubtitlesOpen, this); - context->ass->AddFileSaveListener(&FrameMain::UpdateTitle, this); - - context->local_scripts = new Automation4::LocalScriptManager(context.get()); + context->ass = new AssFile; StartupLog("Initializing context controls"); + context->subsController = new SubsController(context.get()); + context->ass->AddCommitListener(&FrameMain::UpdateTitle, this); + context->subsController->AddFileOpenListener(&FrameMain::OnSubtitlesOpen, this); + context->subsController->AddFileSaveListener(&FrameMain::UpdateTitle, this); + context->audioController = new AudioController(context.get()); context->audioController->AddAudioOpenListener(&FrameMain::OnAudioOpen, this); context->audioController->AddAudioCloseListener(&FrameMain::OnAudioClose, this); + context->local_scripts = new Automation4::LocalScriptManager(context.get()); + // Initialized later due to that the selection controller is currently the subtitles grid context->selectionController = 0; @@ -280,7 +282,7 @@ FrameMain::FrameMain (wxArrayString args) SetDropTarget(new AegisubFileDropTarget(this)); StartupLog("Load default file"); - context->ass->LoadDefault(); + context->subsController->Close(); StartupLog("Display main window"); AddFullScreenButton(this); @@ -408,75 +410,6 @@ void FrameMain::InitContents() { StartupLog("Leaving InitContents"); } -void FrameMain::LoadSubtitles(agi::fs::path const& filename, std::string const& charset) { - if (TryToCloseSubs() == wxCANCEL) return; - - try { - // Make sure that file isn't actually a timecode file - try { - TextFileReader testSubs(filename, charset); - std::string cur = testSubs.ReadLineFromFile(); - if (boost::starts_with(cur, "# timecode")) { - context->videoController->LoadTimecodes(filename); - return; - } - } - catch (...) { - // if trying to load the file as timecodes fails it's fairly - // safe to assume that it is in fact not a timecode file - } - - context->ass->Load(filename, charset); - - StandardPaths::SetPathValue("?script", filename); - config::mru->Add("Subtitle", filename); - OPT_SET("Path/Last/Subtitles")->SetString(filename.parent_path().string()); - - // Save backup of file - if (context->ass->CanSave() && OPT_GET("App/Auto/Backup")->GetBool()) { - if (agi::fs::FileExists(filename)) { - auto path_str = OPT_GET("Path/Auto/Backup")->GetString(); - agi::fs::path path; - if (path_str.empty()) - path = filename.parent_path(); - else - path = StandardPaths::DecodePath(path_str); - agi::fs::CreateDirectory(path); - agi::fs::Copy(filename, path/(filename.stem().string() + ".ORIGINAL" + filename.extension().string())); - } - } - } - catch (agi::fs::FileNotFound const&) { - wxMessageBox(filename.wstring() + " not found.", "Error", wxOK | wxICON_ERROR | wxCENTER, this); - config::mru->Remove("Subtitle", filename); - return; - } - catch (agi::Exception const& err) { - wxMessageBox(to_wx(err.GetChainedMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, this); - } - catch (...) { - wxMessageBox("Unknown error", "Error", wxOK | wxICON_ERROR | wxCENTER, this); - return; - } -} - -int FrameMain::TryToCloseSubs(bool enableCancel) { - if (context->ass->IsModified()) { - int flags = wxYES_NO; - if (enableCancel) flags |= wxCANCEL; - int result = wxMessageBox(wxString::Format(_("Do you want to save changes to %s?"), GetScriptFileName()), _("Unsaved changes"), flags, this); - if (result == wxYES) { - cmd::call("subtitle/save", context.get()); - // If it fails saving, return cancel anyway - return context->ass->IsModified() ? wxCANCEL : wxYES; - } - return result; - } - else { - return wxYES; - } -} - void FrameMain::SetDisplayMode(int video, int audio) { if (!IsShownOnScreen()) return; @@ -512,8 +445,8 @@ void FrameMain::SetDisplayMode(int video, int audio) { void FrameMain::UpdateTitle() { wxString newTitle; - if (context->ass->IsModified()) newTitle << "* "; - newTitle << GetScriptFileName(); + if (context->subsController->IsModified()) newTitle << "* "; + newTitle << context->subsController->Filename().filename().wstring(); #ifndef __WXMAC__ newTitle << " - Aegisub " << GetAegisubLongVersionString(); @@ -521,7 +454,7 @@ void FrameMain::UpdateTitle() { #if defined(__WXMAC__) && !defined(__LP64__) // On Mac, set the mark in the close button - OSXSetModified(context->ass->IsModified()); + OSXSetModified(context->subsController->IsModified()); #endif if (GetTitle() != newTitle) SetTitle(newTitle); @@ -596,7 +529,7 @@ bool FrameMain::LoadList(wxArrayString list) { // Load files if (subs.size()) - LoadSubtitles(subs); + context->subsController->Load(subs); if (blockVideoLoad) { blockVideoLoad = false; @@ -634,21 +567,18 @@ BEGIN_EVENT_TABLE(FrameMain, wxFrame) EVT_MOUSEWHEEL(FrameMain::OnMouseWheel) END_EVENT_TABLE() -void FrameMain::OnCloseWindow (wxCloseEvent &event) { - // Stop audio and video +void FrameMain::OnCloseWindow(wxCloseEvent &event) { context->videoController->Stop(); context->audioController->Stop(); // Ask user if he wants to save first - bool canVeto = event.CanVeto(); - int result = TryToCloseSubs(canVeto); - if (canVeto && result == wxCANCEL) { + if (context->subsController->TryToClose(event.CanVeto()) == wxCANCEL) { event.Veto(); return; } delete context->dialog; - context->dialog = 0; + context->dialog = nullptr; // Store maximization state OPT_SET("App/Maximized")->SetBool(IsMaximized()); @@ -657,7 +587,7 @@ void FrameMain::OnCloseWindow (wxCloseEvent &event) { } void FrameMain::OnAutoSave(wxTimerEvent &) try { - auto fn = context->ass->AutoSave(); + auto fn = context->subsController->AutoSave(); if (!fn.empty()) StatusTimeout(wxString::Format(_("File backup saved as \"%s\"."), fn.wstring())); } @@ -766,18 +696,3 @@ void FrameMain::OnKeyDown(wxKeyEvent &event) { void FrameMain::OnMouseWheel(wxMouseEvent &evt) { ForwardMouseWheelEvent(this, evt); } - -wxString FrameMain::GetScriptFileName() const { - if (context->ass->filename.empty()) { - // Apple HIG says "untitled" should not be capitalised - // and the window is a document window, it shouldn't contain the app name - // (The app name is already present in the menu bar) -#ifndef __WXMAC__ - return _("Untitled"); -#else - return _("untitled"); -#endif - } - else - return context->ass->filename.filename().wstring(); -} diff --git a/aegisub/src/frame_main.h b/aegisub/src/frame_main.h index 5e2a7daa7..58f38bf50 100644 --- a/aegisub/src/frame_main.h +++ b/aegisub/src/frame_main.h @@ -45,6 +45,7 @@ #include #include +class AegisubApp; class AegisubFileDropTarget; class AssFile; class AudioBox; @@ -62,6 +63,7 @@ class VideoZoomSlider; namespace agi { struct Context; class OptionValue; } class FrameMain: public wxFrame { + friend class AegisubApp; friend class AegisubFileDropTarget; std::unique_ptr context; @@ -88,7 +90,6 @@ class FrameMain: public wxFrame { void OnFilesDropped(wxThreadEvent &evt); bool LoadList(wxArrayString list); void UpdateTitle(); - wxString GetScriptFileName() const; void OnKeyDown(wxKeyEvent &event); void OnMouseWheel(wxMouseEvent &evt); @@ -134,11 +135,5 @@ public: bool IsVideoShown() const { return showVideo; } bool IsAudioShown() const { return showAudio; } - /// Close the currently open subs, asking the user if they want to save if there are unsaved changes - /// @param enableCancel Should the user be able to cancel the close? - int TryToCloseSubs(bool enableCancel=true); - - void LoadSubtitles(agi::fs::path const& filename, std::string const& charset=""); - DECLARE_EVENT_TABLE() }; diff --git a/aegisub/src/include/aegisub/context.h b/aegisub/src/include/aegisub/context.h index 9ed70bf48..c29035ee8 100644 --- a/aegisub/src/include/aegisub/context.h +++ b/aegisub/src/include/aegisub/context.h @@ -7,6 +7,7 @@ class DialogManager; class SearchReplaceEngine; class InitialLineState; template class SelectionController; +class SubsController; class SubsTextEditCtrl; class SubtitlesGrid; class TextSelectionController; @@ -26,6 +27,7 @@ struct Context { // Controllers AudioController *audioController; SelectionController *selectionController; + SubsController *subsController; TextSelectionController *textSelectionController; VideoContext *videoController; diff --git a/aegisub/src/main.cpp b/aegisub/src/main.cpp index 0da644497..537f6c47e 100644 --- a/aegisub/src/main.cpp +++ b/aegisub/src/main.cpp @@ -45,11 +45,13 @@ #include "export_fixstyle.h" #include "export_framerate.h" #include "frame_main.h" +#include "include/aegisub/context.h" #include "main.h" #include "libresrc/libresrc.h" #include "options.h" #include "plugin_manager.h" #include "standard_paths.h" +#include "subs_controller.h" #include "subtitle_format.h" #include "version.h" #include "video_context.h" @@ -364,15 +366,15 @@ StackWalker::~StackWalker() { /// Message displayed when an exception has occurred. const static wxString exception_message = _("Oops, Aegisub has crashed!\n\nAn attempt has been made to save a copy of your file to:\n\n%s\n\nAegisub will now close."); -static void UnhandledExeception(bool stackWalk) { +static void UnhandledExeception(bool stackWalk, agi::Context *c) { #if (!defined(_DEBUG) || defined(WITH_EXCEPTIONS)) && (wxUSE_ON_FATAL_EXCEPTION+0) - if (AssFile::top) { + if (c->ass && c->subsController) { auto path = StandardPaths::DecodePath("?user/recovered"); agi::fs::CreateDirectory(path); - auto filename = AssFile::top->filename.empty() ? "untitled" : AssFile::top->filename.stem().string(); + auto filename = c->subsController->Filename().stem(); path /= str(boost::format("%s.%s.ass") % filename % agi::util::strftime("%Y-%m-%d-%H-%M-%S")); - AssFile::top->Save(path, false, false); + c->subsController->Save(path); #if wxUSE_STACKWALKER == 1 if (stackWalk) { @@ -397,11 +399,11 @@ static void UnhandledExeception(bool stackWalk) { } void AegisubApp::OnUnhandledException() { - UnhandledExeception(false); + UnhandledExeception(false, frame ? frame->context.get() : nullptr); } void AegisubApp::OnFatalException() { - UnhandledExeception(true); + UnhandledExeception(true, frame ? frame->context.get() : nullptr); } void AegisubApp::HandleEvent(wxEvtHandler *handler, wxEventFunction func, wxEvent& event) const { @@ -456,9 +458,6 @@ int AegisubApp::OnRun() { } void AegisubApp::MacOpenFile(const wxString &filename) { - if (frame != nullptr && !filename.empty()) { - frame->LoadSubtitles(from_wx(filename)); - wxFileName filepath(filename); - OPT_SET("Path/Last/Subtitles")->SetString(from_wx(filepath.GetPath())); - } + if (frame && !filename.empty()) + frame->context->subsController->Load(agi::fs::path(filename)); } diff --git a/aegisub/src/subs_controller.cpp b/aegisub/src/subs_controller.cpp new file mode 100644 index 000000000..edde055fd --- /dev/null +++ b/aegisub/src/subs_controller.cpp @@ -0,0 +1,311 @@ +// Copyright (c) 2013, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#include "config.h" + +#include "subs_controller.h" + +#include "ass_dialogue.h" +#include "ass_file.h" +#include "ass_style.h" +#include "compat.h" +#include "command/command.h" +#include "include/aegisub/context.h" +#include "options.h" +#include "standard_paths.h" +#include "subtitle_format.h" +#include "text_file_reader.h" +#include "video_context.h" + +#include +#include + +#include +#include +#include + +struct SubsController::UndoInfo { + AssFile file; + wxString undo_description; + int commit_id; + UndoInfo(AssFile const& f, wxString const& d, int c) : file(f), undo_description(d), commit_id(c) { } +}; + +SubsController::SubsController(agi::Context *context) +: context(context) +, undo_connection(context->ass->AddUndoManager(&SubsController::OnCommit, this)) +, commit_id(0) +, saved_commit_id(0) +, autosaved_commit_id(0) +{ +} + +void SubsController::Load(agi::fs::path const& filename, std::string const& charset) { + if (TryToClose() == wxCANCEL) return; + + // Make sure that file isn't actually a timecode file + try { + TextFileReader testSubs(filename, charset); + std::string cur = testSubs.ReadLineFromFile(); + if (boost::starts_with(cur, "# timecode")) { + context->videoController->LoadTimecodes(filename); + return; + } + } + catch (...) { + // if trying to load the file as timecodes fails it's fairly + // safe to assume that it is in fact not a timecode file + } + + const SubtitleFormat *reader = SubtitleFormat::GetReader(filename); + + try { + AssFile temp; + reader->ReadFile(&temp, filename, charset); + + bool found_style = false; + bool found_dialogue = false; + + // Check if the file has at least one style and at least one dialogue line + for (auto const& line : temp.Line) { + AssEntryGroup type = line.Group(); + if (type == ENTRY_STYLE) found_style = true; + if (type == ENTRY_DIALOGUE) found_dialogue = true; + if (found_style && found_dialogue) break; + } + + // And if it doesn't add defaults for each + if (!found_style) + temp.InsertLine(new AssStyle); + if (!found_dialogue) + temp.InsertLine(new AssDialogue); + + context->ass->swap(temp); + } + catch (agi::UserCancelException const&) { + return; + } + catch (agi::fs::FileNotFound const&) { + wxMessageBox(filename.wstring() + " not found.", "Error", wxOK | wxICON_ERROR | wxCENTER, context->parent); + config::mru->Remove("Subtitle", filename); + return; + } + catch (agi::Exception const& err) { + wxMessageBox(to_wx(err.GetChainedMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, context->parent); + return; + } + catch (std::exception const& err) { + wxMessageBox(to_wx(err.what()), "Error", wxOK | wxICON_ERROR | wxCENTER, context->parent); + return; + } + catch (...) { + wxMessageBox("Unknown error", "Error", wxOK | wxICON_ERROR | wxCENTER, context->parent); + return; + } + + SetFileName(filename); + + // Push the initial state of the file onto the undo stack + undo_stack.clear(); + redo_stack.clear(); + autosaved_commit_id = saved_commit_id = commit_id + 1; + context->ass->Commit("", AssFile::COMMIT_NEW); + + // Save backup of file + if (CanSave() && OPT_GET("App/Auto/Backup")->GetBool()) { + auto path_str = OPT_GET("Path/Auto/Backup")->GetString(); + agi::fs::path path; + if (path_str.empty()) + path = filename.parent_path(); + else + path = StandardPaths::DecodePath(path_str); + agi::fs::CreateDirectory(path); + agi::fs::Copy(filename, path/(filename.stem().string() + ".ORIGINAL" + filename.extension().string())); + } + + FileOpen(filename); +} + +void SubsController::Save(agi::fs::path const& filename, std::string const& encoding) { + const SubtitleFormat *writer = SubtitleFormat::GetWriter(filename); + if (!writer) + throw "Unknown file type."; + + int old_autosaved_commit_id = autosaved_commit_id, old_saved_commit_id = saved_commit_id; + try { + autosaved_commit_id = saved_commit_id = commit_id; + + // Have to set these now for the sake of things that want to save paths + // relative to the script in the header + this->filename = filename; + StandardPaths::SetPathValue("?script", filename.parent_path()); + + FileSave(); + + writer->WriteFile(context->ass, filename, encoding); + } + catch (...) { + autosaved_commit_id = old_autosaved_commit_id; + saved_commit_id = old_saved_commit_id; + throw; + } + + SetFileName(filename); +} + +void SubsController::Close() { + undo_stack.clear(); + redo_stack.clear(); + autosaved_commit_id = saved_commit_id = commit_id + 1; + filename.clear(); + context->ass->Line.clear(); + context->ass->LoadDefault(); + context->ass->Commit("", AssFile::COMMIT_NEW); +} + +int SubsController::TryToClose(bool allow_cancel) const { + if (!IsModified()) + return wxYES; + + int flags = wxYES_NO; + if (allow_cancel) + flags |= wxCANCEL; + int result = wxMessageBox(wxString::Format(_("Do you want to save changes to %s?"), Filename().wstring()), _("Unsaved changes"), flags, context->parent); + if (result == wxYES) { + cmd::call("subtitle/save", context); + // If it fails saving, return cancel anyway + return IsModified() ? wxCANCEL : wxYES; + } + return result; +} + +agi::fs::path SubsController::AutoSave() { + if (commit_id == autosaved_commit_id) + return ""; + + auto path = StandardPaths::DecodePath(OPT_GET("Path/Auto/Save")->GetString()); + if (path.empty()) + path = filename.parent_path(); + + agi::fs::CreateDirectory(path); + + auto name = filename.filename(); + if (name.empty()) + name = "Untitled"; + + path /= str(boost::format("%s.%s.AUTOSAVE.ass") % name.string() % agi::util::strftime("%Y-%m-%d-%H-%M-%S")); + + SubtitleFormat::GetWriter(path)->WriteFile(context->ass, path); + autosaved_commit_id = commit_id; + + return path; +} + +bool SubsController::CanSave() const { + try { + return SubtitleFormat::GetWriter(filename)->CanSave(context->ass); + } + catch (...) { + return false; + } +} + +void SubsController::SetFileName(agi::fs::path const& path) { + filename = path; + StandardPaths::SetPathValue("?script", path.parent_path()); + config::mru->Add("Subtitle", path); + OPT_SET("Path/Last/Subtitles")->SetString(filename.parent_path().string()); +} + +void SubsController::OnCommit(AssFileCommit c) { + if (c.message.empty() && !undo_stack.empty()) return; + + ++commit_id; + // Allow coalescing only if it's the last change and the file has not been + // saved since the last change + if (commit_id == *c.commit_id+1 && redo_stack.empty() && saved_commit_id+1 != commit_id && autosaved_commit_id+1 != commit_id) { + // If only one line changed just modify it instead of copying the file + if (c.single_line) { + entryIter this_it = context->ass->Line.begin(), undo_it = undo_stack.back().file.Line.begin(); + while (&*this_it != c.single_line) { + ++this_it; + ++undo_it; + } + undo_stack.back().file.Line.insert(undo_it, *c.single_line->Clone()); + delete &*undo_it; + } + else + undo_stack.back().file = *context->ass; + + *c.commit_id = commit_id; + return; + } + + redo_stack.clear(); + + undo_stack.emplace_back(*context->ass, c.message, commit_id); + + int depth = std::max(OPT_GET("Limits/Undo Levels")->GetInt(), 2); + while ((int)undo_stack.size() > depth) + undo_stack.pop_front(); + + if (undo_stack.size() > 1 && OPT_GET("App/Auto/Save on Every Change")->GetBool() && !filename.empty() && CanSave()) + Save(filename); + + *c.commit_id = commit_id; +} + +void SubsController::Undo() { + if (undo_stack.size() <= 1) return; + + redo_stack.emplace_back(AssFile(), undo_stack.back().undo_description, commit_id); + context->ass->swap(redo_stack.back().file); + undo_stack.pop_back(); + *context->ass = undo_stack.back().file; + commit_id = undo_stack.back().commit_id; + + context->ass->Commit("", AssFile::COMMIT_NEW); +} + +void SubsController::Redo() { + if (redo_stack.empty()) return; + + context->ass->swap(redo_stack.back().file); + commit_id = redo_stack.back().commit_id; + undo_stack.emplace_back(*context->ass, redo_stack.back().undo_description, commit_id); + redo_stack.pop_back(); + + context->ass->Commit("", AssFile::COMMIT_NEW); +} + +wxString SubsController::GetUndoDescription() const { + return IsUndoStackEmpty() ? "" : undo_stack.back().undo_description; +} + +wxString SubsController::GetRedoDescription() const { + return IsRedoStackEmpty() ? "" : redo_stack.back().undo_description; +} + +agi::fs::path SubsController::Filename() const { + if (!filename.empty()) return filename; + + // Apple HIG says "untitled" should not be capitalised +#ifndef __WXMAC__ + return _("Untitled").wx_str(); +#else + return _("untitled").wx_str(); +#endif +} diff --git a/aegisub/src/subs_controller.h b/aegisub/src/subs_controller.h new file mode 100644 index 000000000..7a6417426 --- /dev/null +++ b/aegisub/src/subs_controller.h @@ -0,0 +1,110 @@ +// Copyright (c) 2013, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#include +#include + +#include +#include +#include + +class AssEntry; +class AssFile; +struct AssFileCommit; + +namespace agi { struct Context; } + +class SubsController { + agi::Context *context; + agi::signal::Connection undo_connection; + + struct UndoInfo; + boost::container::list undo_stack; + boost::container::list redo_stack; + + /// Revision counter for undo coalescing and modified state tracking + int commit_id; + /// Last saved version of this file + int saved_commit_id; + /// Last autosaved version of this file + int autosaved_commit_id; + + /// A new file has been opened (filename) + agi::signal::Signal FileOpen; + /// The file is about to be saved + /// This signal is intended for adding metadata such as video filename, + /// frame number, etc. Ideally this would all be done immediately rather + /// than waiting for a save, but that causes (more) issues with undo + agi::signal::Signal<> FileSave; + + /// The filename of the currently open file, if any + agi::fs::path filename; + + void OnCommit(AssFileCommit c); + + /// Set the filename, updating things like the MRU and last used path + void SetFileName(agi::fs::path const& file); + +public: + SubsController(agi::Context *context); + + /// The file's path and filename if any, or platform-appropriate "untitled" + agi::fs::path Filename() const; + + /// Does the file have unsaved changes? + bool IsModified() const { return commit_id != saved_commit_id; }; + + /// @brief Load from a file + /// @param file File name + /// @param charset Character set of file or empty to autodetect + void Load(agi::fs::path const& file, std::string const& charset=""); + + /// @brief Save to a file + /// @param file Path to save to + /// @param encoding Encoding to use, or empty to let the writer decide (which usually means "App/Save Charset") + void Save(agi::fs::path const& file, std::string const& encoding=""); + + /// Close the currently open file (i.e. open a new blank file) + void Close(); + + /// If there are unsaved changes, asl the user if they want to save them + /// @param allow_cancel Let the user cancel the closing + /// @return wxYES, wxNO or wxCANCEL (note: all three are true in a boolean context) + int TryToClose(bool allow_cancel = true) const; + + /// @brief Autosave the file if there have been any chances since the last autosave + /// @return File name used or empty if no save was performed + agi::fs::path AutoSave(); + + /// Can the file be saved in its current format? + bool CanSave() const; + + DEFINE_SIGNAL_ADDERS(FileOpen, AddFileOpenListener) + DEFINE_SIGNAL_ADDERS(FileSave, AddFileSaveListener) + + /// @brief Undo the last set of changes to the file + void Undo(); + /// @brief Redo the last undone changes + void Redo(); + /// Check if undo stack is empty + bool IsUndoStackEmpty() const { return undo_stack.size() <= 1; }; + /// Check if redo stack is empty + bool IsRedoStackEmpty() const { return redo_stack.empty(); }; + /// Get the description of the first undoable change + wxString GetUndoDescription() const; + /// Get the description of the first redoable change + wxString GetRedoDescription() const; +}; diff --git a/aegisub/src/subs_edit_box.h b/aegisub/src/subs_edit_box.h index 998cc6331..f92202d1a 100644 --- a/aegisub/src/subs_edit_box.h +++ b/aegisub/src/subs_edit_box.h @@ -183,7 +183,7 @@ class SubsEditBox : public wxPanel { void SetSelectedRows(T AssDialogue::*field, wxString const& value, wxString const& desc, int type, bool amend = false); /// @brief Reload the current line from the file - /// @param type AssFile::CommitType + /// @param type AssFile::COMMITType void OnCommit(int type); /// Regenerate a dropdown list with the unique values of a dialogue field diff --git a/aegisub/src/subtitle_format_encore.cpp b/aegisub/src/subtitle_format_encore.cpp index 13796a355..baf5920bb 100644 --- a/aegisub/src/subtitle_format_encore.cpp +++ b/aegisub/src/subtitle_format_encore.cpp @@ -43,6 +43,7 @@ #include #include +#include #include EncoreSubtitleFormat::EncoreSubtitleFormat() diff --git a/aegisub/src/subtitle_format_transtation.cpp b/aegisub/src/subtitle_format_transtation.cpp index 6f38f7e09..4daf326eb 100644 --- a/aegisub/src/subtitle_format_transtation.cpp +++ b/aegisub/src/subtitle_format_transtation.cpp @@ -47,6 +47,7 @@ #include #include +#include #include TranStationSubtitleFormat::TranStationSubtitleFormat() diff --git a/aegisub/src/subtitles_provider_csri.cpp b/aegisub/src/subtitles_provider_csri.cpp index 15e8076c7..c31db5d55 100644 --- a/aegisub/src/subtitles_provider_csri.cpp +++ b/aegisub/src/subtitles_provider_csri.cpp @@ -37,9 +37,8 @@ #ifdef WITH_CSRI #include "subtitles_provider_csri.h" -#include "ass_file.h" +#include "subtitle_format.h" #include "standard_paths.h" -#include "video_context.h" #include "video_frame.h" #include @@ -81,7 +80,7 @@ CSRISubtitlesProvider::~CSRISubtitlesProvider() { void CSRISubtitlesProvider::LoadSubtitles(AssFile *subs) { if (tempfile.empty()) tempfile = unique_path(StandardPaths::DecodePath("?temp/csri-%%%%-%%%%-%%%%-%%%%.ass")); - subs->Save(tempfile, false, false, "utf-8"); + SubtitleFormat::GetWriter(tempfile)->WriteFile(subs, tempfile, "utf-8"); std::lock_guard lock(csri_mutex); instance = csri_open_file(renderer, tempfile.string().c_str(), nullptr); diff --git a/aegisub/src/subtitles_provider_libass.cpp b/aegisub/src/subtitles_provider_libass.cpp index 0db954c51..38d80b0c6 100644 --- a/aegisub/src/subtitles_provider_libass.cpp +++ b/aegisub/src/subtitles_provider_libass.cpp @@ -52,6 +52,7 @@ #include #include +#include #include #include #include @@ -117,7 +118,17 @@ LibassSubtitlesProvider::~LibassSubtitlesProvider() { void LibassSubtitlesProvider::LoadSubtitles(AssFile *subs) { std::vector data; - subs->SaveMemory(data); + data.clear(); + data.reserve(0x4000); + + AssEntryGroup group = ENTRY_GROUP_MAX; + for (auto const& line : subs->Line) { + if (group != line.Group()) { + group = line.Group(); + boost::push_back(data, line.GroupHeader() + "\r\n"); + } + boost::push_back(data, line.GetEntryData() + "\r\n"); + } if (ass_track) ass_free_track(ass_track); ass_track = ass_read_memory(library, &data[0], data.size(),(char *)"UTF-8"); diff --git a/aegisub/src/video_context.cpp b/aegisub/src/video_context.cpp index 3b81d6d4d..6593ac297 100644 --- a/aegisub/src/video_context.cpp +++ b/aegisub/src/video_context.cpp @@ -46,6 +46,7 @@ #include "mkv_wrap.h" #include "options.h" #include "selection_controller.h" +#include "subs_controller.h" #include "time_range.h" #include "threaded_frame_source.h" #include "utils.h" @@ -116,7 +117,7 @@ void VideoContext::Reset() { void VideoContext::SetContext(agi::Context *context) { this->context = context; context->ass->AddCommitListener(&VideoContext::OnSubtitlesCommit, this); - context->ass->AddFileSaveListener(&VideoContext::OnSubtitlesSave, this); + context->subsController->AddFileSaveListener(&VideoContext::OnSubtitlesSave, this); } void VideoContext::SetVideo(const agi::fs::path &filename) { diff --git a/aegisub/src/video_display.cpp b/aegisub/src/video_display.cpp index c1687b713..d86009bf9 100644 --- a/aegisub/src/video_display.cpp +++ b/aegisub/src/video_display.cpp @@ -32,7 +32,6 @@ /// @ingroup video main_ui /// -// Includes #include "config.h" #include @@ -60,6 +59,7 @@ #include "include/aegisub/menu.h" #include "options.h" #include "spline_curve.h" +#include "subs_controller.h" #include "threaded_frame_source.h" #include "utils.h" #include "video_out_gl.h" @@ -111,7 +111,7 @@ VideoDisplay::VideoDisplay( slots.push_back(con->videoController->AddVideoOpenListener(&VideoDisplay::UpdateSize, this)); slots.push_back(con->videoController->AddARChangeListener(&VideoDisplay::UpdateSize, this)); - slots.push_back(con->ass->AddFileSaveListener(&VideoDisplay::OnSubtitlesSave, this)); + slots.push_back(con->subsController->AddFileSaveListener(&VideoDisplay::OnSubtitlesSave, this)); Bind(wxEVT_PAINT, std::bind(&VideoDisplay::Render, this)); Bind(wxEVT_SIZE, &VideoDisplay::OnSizeEvent, this);