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