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);