diff --git a/aegisub/build/Aegisub/Aegisub.vcxproj b/aegisub/build/Aegisub/Aegisub.vcxproj index 524ef41e6..be0a770e1 100644 --- a/aegisub/build/Aegisub/Aegisub.vcxproj +++ b/aegisub/build/Aegisub/Aegisub.vcxproj @@ -256,6 +256,7 @@ + @@ -448,6 +449,7 @@ + diff --git a/aegisub/build/Aegisub/Aegisub.vcxproj.filters b/aegisub/build/Aegisub/Aegisub.vcxproj.filters index 9dc72df3f..7fd87b4f9 100644 --- a/aegisub/build/Aegisub/Aegisub.vcxproj.filters +++ b/aegisub/build/Aegisub/Aegisub.vcxproj.filters @@ -686,6 +686,10 @@ Preferences + + + Features\Search-replace + @@ -1231,6 +1235,9 @@ Features\Autosave + + Features\Search-replace + diff --git a/aegisub/src/Makefile b/aegisub/src/Makefile index a3290337e..2567021ad 100644 --- a/aegisub/src/Makefile +++ b/aegisub/src/Makefile @@ -213,6 +213,7 @@ SRC += \ plugin_manager.cpp \ preferences.cpp \ preferences_base.cpp \ + search_replace_engine.cpp \ scintilla_text_ctrl.cpp \ scintilla_text_selection_controller.cpp \ spellchecker.cpp \ diff --git a/aegisub/src/command/edit.cpp b/aegisub/src/command/edit.cpp index bcb173d63..bd06ad180 100644 --- a/aegisub/src/command/edit.cpp +++ b/aegisub/src/command/edit.cpp @@ -54,6 +54,7 @@ #include "../dialog_search_replace.h" #include "../include/aegisub/context.h" #include "../options.h" +#include "../search_replace_engine.h" #include "../subs_edit_ctrl.h" #include "../subs_grid.h" #include "../text_selection_controller.h" @@ -462,7 +463,7 @@ struct edit_find_replace : public Command { void operator()(agi::Context *c) { c->videoController->Stop(); - c->search->OpenDialog(true); + DialogSearchReplace::Show(c, true); } }; diff --git a/aegisub/src/command/subtitle.cpp b/aegisub/src/command/subtitle.cpp index a82146339..fe0bb734b 100644 --- a/aegisub/src/command/subtitle.cpp +++ b/aegisub/src/command/subtitle.cpp @@ -58,6 +58,7 @@ #include "../include/aegisub/context.h" #include "../main.h" #include "../options.h" +#include "../search_replace_engine.h" #include "../subs_grid.h" #include "../subtitle_format.h" #include "../utils.h" @@ -105,7 +106,7 @@ struct subtitle_find : public Command { void operator()(agi::Context *c) { c->videoController->Stop(); - c->search->OpenDialog(false); + DialogSearchReplace::Show(c, false); } }; @@ -119,7 +120,8 @@ struct subtitle_find_next : public Command { void operator()(agi::Context *c) { c->videoController->Stop(); - c->search->FindNext(); + if (!c->search->FindNext()) + DialogSearchReplace::Show(c, false); } }; diff --git a/aegisub/src/dialog_search_replace.cpp b/aegisub/src/dialog_search_replace.cpp index 901cafbe1..d427fdc0f 100644 --- a/aegisub/src/dialog_search_replace.cpp +++ b/aegisub/src/dialog_search_replace.cpp @@ -1,29 +1,16 @@ -// Copyright (c) 2005, Rodrigo Braz Monteiro -// All rights reserved. +// Copyright (c) 2013, Thomas Goyne // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// 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. // -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the Aegisub Group nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// 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/ @@ -36,156 +23,117 @@ #include "dialog_search_replace.h" -#include "ass_dialogue.h" -#include "ass_file.h" #include "compat.h" #include "include/aegisub/context.h" #include "options.h" -#include "selection_controller.h" -#include "text_selection_controller.h" -#include "subs_grid.h" +#include "search_replace_engine.h" #include "utils.h" - -#include +#include "validators.h" #include #include #include #include -#include #include -#include #include #include #include +#include -enum { - BUTTON_FIND_NEXT, - BUTTON_REPLACE_NEXT, - BUTTON_REPLACE_ALL -}; - -enum { - FIELD_TEXT = 0, - FIELD_STYLE, - FIELD_ACTOR, - FIELD_EFFECT -}; - -enum { - LIMIT_ALL = 0, - LIMIT_SELECTED -}; - -DialogSearchReplace::DialogSearchReplace(agi::Context* c, bool withReplace) -: wxDialog(c->parent, -1, withReplace ? _("Replace") : _("Find")) +DialogSearchReplace::DialogSearchReplace(agi::Context* c, bool replace) +: wxDialog(c->parent, -1, replace ? _("Replace") : _("Find")) , c(c) -, hasReplace(withReplace) +, settings(new SearchReplaceSettings) +, has_replace(replace) { - wxSizer *FindSizer = new wxFlexGridSizer(2, 2, 5, 15); - FindEdit = new wxComboBox(this, -1, "", wxDefaultPosition, wxSize(300, -1), lagi_MRU_wxAS("Find"), wxCB_DROPDOWN); - if (!FindEdit->IsListEmpty()) - FindEdit->SetSelection(0); - FindSizer->Add(new wxStaticText(this, -1, _("Find what:")), 0, wxALIGN_CENTER_VERTICAL); - FindSizer->Add(FindEdit); - if (hasReplace) { - ReplaceEdit = new wxComboBox(this, -1, "", wxDefaultPosition, wxSize(300, -1), lagi_MRU_wxAS("Replace"), wxCB_DROPDOWN); - FindSizer->Add(new wxStaticText(this, -1, _("Replace with:")), 0, wxALIGN_CENTER_VERTICAL); - FindSizer->Add(ReplaceEdit); - if (!ReplaceEdit->IsListEmpty()) - ReplaceEdit->SetSelection(0); + auto recent_find(lagi_MRU_wxAS("Find")); + auto recent_replace(lagi_MRU_wxAS("Replace")); + + settings->field = static_cast(OPT_GET("Tool/Search Replace/Field")->GetInt()); + settings->limit_to = static_cast(OPT_GET("Tool/Search Replace/Affect")->GetInt()); + settings->find = recent_find.empty() ? wxString() : recent_find.front(); + settings->replace_with = recent_replace.empty() ? wxString() : recent_replace.front(); + settings->match_case = OPT_GET("Tool/Search Replace/Match Case")->GetBool(); + settings->use_regex = OPT_GET("Tool/Search Replace/RegExp")->GetBool(); + + auto find_sizer = new wxFlexGridSizer(2, 2, 5, 15); + find_edit = new wxComboBox(this, -1, "", wxDefaultPosition, wxSize(300, -1), recent_find, wxCB_DROPDOWN, wxGenericValidator(&settings->find)); + find_sizer->Add(new wxStaticText(this, -1, _("Find what:")), wxSizerFlags().Center().Left()); + find_sizer->Add(find_edit); + + if (has_replace) { + replace_edit = new wxComboBox(this, -1, "", wxDefaultPosition, wxSize(300, -1), lagi_MRU_wxAS("Replace"), wxCB_DROPDOWN, wxGenericValidator(&settings->replace_with)); + find_sizer->Add(new wxStaticText(this, -1, _("Replace with:")), wxSizerFlags().Center().Left()); + find_sizer->Add(replace_edit); } - wxSizer *OptionsSizer = new wxBoxSizer(wxVERTICAL); - CheckMatchCase = new wxCheckBox(this, -1, _("&Match case")); - CheckRegExp = new wxCheckBox(this, -1, _("&Use regular expressions")); - CheckMatchCase->SetValue(OPT_GET("Tool/Search Replace/Match Case")->GetBool()); - CheckRegExp->SetValue(OPT_GET("Tool/Search Replace/RegExp")->GetBool()); - OptionsSizer->Add(CheckMatchCase, wxSizerFlags().Border(wxBOTTOM)); - OptionsSizer->Add(CheckRegExp); + auto options_sizer = new wxBoxSizer(wxVERTICAL); + options_sizer->Add(new wxCheckBox(this, -1, _("&Match case"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->match_case)), wxSizerFlags().Border(wxBOTTOM)); + options_sizer->Add(new wxCheckBox(this, -1, _("&Use regular expressions"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->use_regex))); - // Left sizer - wxSizer *LeftSizer = new wxBoxSizer(wxVERTICAL); - LeftSizer->Add(FindSizer, wxSizerFlags().DoubleBorder(wxBOTTOM)); - LeftSizer->Add(OptionsSizer); + auto left_sizer = new wxBoxSizer(wxVERTICAL); + left_sizer->Add(find_sizer, wxSizerFlags().DoubleBorder(wxBOTTOM)); + left_sizer->Add(options_sizer); - // Limits sizer wxString field[] = { _("Text"), _("Style"), _("Actor"), _("Effect") }; wxString affect[] = { _("All rows"), _("Selected rows") }; - Field = new wxRadioBox(this, -1, _("In Field"), wxDefaultPosition, wxDefaultSize, countof(field), field); - Affect = new wxRadioBox(this, -1, _("Limit to"), wxDefaultPosition, wxDefaultSize, countof(affect), affect); - wxSizer *LimitSizer = new wxBoxSizer(wxHORIZONTAL); - LimitSizer->Add(Field, wxSizerFlags().Border(wxRIGHT)); - LimitSizer->Add(Affect); - Field->SetSelection(OPT_GET("Tool/Search Replace/Field")->GetInt()); - Affect->SetSelection(OPT_GET("Tool/Search Replace/Affect")->GetInt()); + auto limit_sizer = new wxBoxSizer(wxHORIZONTAL); + limit_sizer->Add(new wxRadioBox(this, -1, _("In Field"), wxDefaultPosition, wxDefaultSize, countof(field), field, 0, wxRA_SPECIFY_COLS, MakeEnumBinder(&settings->field)), wxSizerFlags().Border(wxRIGHT)); + limit_sizer->Add(new wxRadioBox(this, -1, _("Limit to"), wxDefaultPosition, wxDefaultSize, countof(affect), affect, 0, wxRA_SPECIFY_COLS, MakeEnumBinder(&settings->limit_to))); - // Buttons - wxSizer *ButtonSizer = new wxBoxSizer(wxVERTICAL); - wxButton *FindNext = new wxButton(this, BUTTON_FIND_NEXT, _("&Find next")); - FindNext->SetDefault(); - ButtonSizer->Add(FindNext, wxSizerFlags().Border(wxBOTTOM)); - if (hasReplace) { - ButtonSizer->Add(new wxButton(this, BUTTON_REPLACE_NEXT, _("Replace &next")), wxSizerFlags().Border(wxBOTTOM)); - ButtonSizer->Add(new wxButton(this, BUTTON_REPLACE_ALL, _("Replace &all")), wxSizerFlags().Border(wxBOTTOM)); + auto find_next = new wxButton(this, -1, _("&Find next")); + auto replace_next = new wxButton(this, -1, _("Replace &next")); + auto replace_all = new wxButton(this, -1, _("Replace &all")); + find_next->SetDefault(); + + auto button_sizer = new wxBoxSizer(wxVERTICAL); + button_sizer->Add(find_next, wxSizerFlags().Border(wxBOTTOM)); + button_sizer->Add(replace_next, wxSizerFlags().Border(wxBOTTOM)); + button_sizer->Add(replace_all, wxSizerFlags().Border(wxBOTTOM)); + button_sizer->Add(new wxButton(this, wxID_CANCEL)); + + if (!has_replace) { + button_sizer->Hide(replace_next); + button_sizer->Hide(replace_all); } - ButtonSizer->Add(new wxButton(this, wxID_CANCEL)); - wxSizer *TopSizer = new wxBoxSizer(wxHORIZONTAL); - TopSizer->Add(LeftSizer, wxSizerFlags().Border()); - TopSizer->Add(ButtonSizer, wxSizerFlags().Border()); + auto top_sizer = new wxBoxSizer(wxHORIZONTAL); + top_sizer->Add(left_sizer, wxSizerFlags().Border()); + top_sizer->Add(button_sizer, wxSizerFlags().Border()); - // Main sizer - wxSizer *MainSizer = new wxBoxSizer(wxVERTICAL); - MainSizer->Add(TopSizer); - MainSizer->Add(LimitSizer, wxSizerFlags().Border()); - SetSizerAndFit(MainSizer); + auto main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(top_sizer); + main_sizer->Add(limit_sizer, wxSizerFlags().Border()); + SetSizerAndFit(main_sizer); CenterOnParent(); - c->search->OnDialogOpen(); - - Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, 0), BUTTON_FIND_NEXT); - Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, 1), BUTTON_REPLACE_NEXT); - Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, 2), BUTTON_REPLACE_ALL); + find_next->Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, &SearchReplaceEngine::FindNext)); + replace_next->Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, &SearchReplaceEngine::ReplaceNext)); + replace_all->Bind(wxEVT_COMMAND_BUTTON_CLICKED, std::bind(&DialogSearchReplace::FindReplace, this, &SearchReplaceEngine::ReplaceAll)); } DialogSearchReplace::~DialogSearchReplace() { - c->search->isReg = CheckRegExp->IsChecked() && CheckRegExp->IsEnabled(); - c->search->matchCase = CheckMatchCase->IsChecked(); - OPT_SET("Tool/Search Replace/Match Case")->SetBool(CheckMatchCase->IsChecked()); - OPT_SET("Tool/Search Replace/RegExp")->SetBool(CheckRegExp->IsChecked()); - OPT_SET("Tool/Search Replace/Field")->SetInt(Field->GetSelection()); - OPT_SET("Tool/Search Replace/Affect")->SetInt(Affect->GetSelection()); } -void DialogSearchReplace::FindReplace(int mode) { - if (mode < 0 || mode > 2) return; +void DialogSearchReplace::FindReplace(bool (SearchReplaceEngine::*func)()) { + TransferDataFromWindow(); - wxString LookFor = FindEdit->GetValue(); - if (!LookFor) return; + if (settings->find.empty()) + return; - c->search->isReg = CheckRegExp->IsChecked() && CheckRegExp->IsEnabled(); - c->search->matchCase = CheckMatchCase->IsChecked(); - c->search->LookFor = LookFor; - c->search->initialized = true; - c->search->affect = Affect->GetSelection(); - c->search->field = Field->GetSelection(); + c->search->Configure(*settings); + (c->search->*func)(); - if (hasReplace) { - wxString ReplaceWith = ReplaceEdit->GetValue(); - c->search->ReplaceWith = ReplaceWith; - config::mru->Add("Replace", from_wx(ReplaceWith)); - } + config::mru->Add("Find", from_wx(settings->find)); + if (has_replace) + config::mru->Add("Replace", from_wx(settings->replace_with)); - if (mode == 0) - c->search->FindNext(); - else if (mode == 1) - c->search->ReplaceNext(); - else - c->search->ReplaceAll(); + OPT_SET("Tool/Search Replace/Match Case")->SetBool(settings->match_case); + OPT_SET("Tool/Search Replace/RegExp")->SetBool(settings->use_regex); + OPT_SET("Tool/Search Replace/Field")->SetInt(static_cast(settings->field)); + OPT_SET("Tool/Search Replace/Affect")->SetInt(static_cast(settings->limit_to)); - config::mru->Add("Find", from_wx(LookFor)); UpdateDropDowns(); } @@ -199,235 +147,16 @@ static void update_mru(wxComboBox *cb, const char *mru_name) { } void DialogSearchReplace::UpdateDropDowns() { - update_mru(FindEdit, "Find"); + update_mru(find_edit, "Find"); - if (hasReplace) - update_mru(ReplaceEdit, "Replace"); + if (has_replace) + update_mru(replace_edit, "Replace"); } -SearchReplaceEngine::SearchReplaceEngine(agi::Context *c) -: context(c) -, curLine(0) -, pos(0) -, matchLen(0) -, replaceLen(0) -, LastWasFind(true) -, hasReplace(false) -, isReg(false) -, matchCase(false) -, initialized(false) -, field(FIELD_TEXT) -, affect(LIMIT_ALL) -{ -} - -static boost::flyweight *get_text(AssDialogue *cur, int field) { - switch (field) { - case FIELD_TEXT: return &cur->Text; - case FIELD_STYLE: return &cur->Style; - case FIELD_ACTOR: return &cur->Actor; - case FIELD_EFFECT: return &cur->Effect; - default: throw agi::InternalError("Bad find/replace field", 0); - } -} - -void SearchReplaceEngine::ReplaceNext(bool DoReplace) { - if (!initialized) { - OpenDialog(DoReplace); - return; - } - - wxArrayInt sels = context->subsGrid->GetSelection(); - int firstLine = 0; - if (sels.Count() > 0) firstLine = sels[0]; - // if selection has changed reset values - if (firstLine != curLine) { - curLine = firstLine; - LastWasFind = true; - pos = 0; - matchLen = 0; - replaceLen = 0; - } - - // Setup - int start = curLine; - int nrows = context->subsGrid->GetRows(); - bool found = false; - int regFlags = wxRE_ADVANCED; - if (!matchCase) { - if (isReg) - regFlags |= wxRE_ICASE; - else - LookFor.MakeLower(); - } - wxRegEx regex; - if (isReg) { - regex.Compile(LookFor, regFlags); - - if (!regex.IsValid()) { - LastWasFind = !DoReplace; - return; - } - } - - // Search for it - boost::flyweight *Text = nullptr; - while (!found) { - Text = get_text(context->subsGrid->GetDialogue(curLine), field); - size_t tempPos; - if (DoReplace && LastWasFind) - tempPos = pos; - else - tempPos = pos + replaceLen; - - if (isReg) { - if (regex.Matches(Text->get().substr(tempPos))) { - size_t match_start; - regex.GetMatch(&match_start, &matchLen, 0); - pos = match_start + tempPos; - found = true; - } - } - else { - wxString src = Text->get().substr(tempPos); - if (!matchCase) src.MakeLower(); - size_t textPos = src.find(LookFor); - if (textPos != src.npos) { - pos = tempPos+textPos; - found = true; - matchLen = LookFor.size(); - } - } - - // Didn't find, go to next line - if (!found) { - curLine = (curLine + 1) % nrows; - pos = 0; - matchLen = 0; - replaceLen = 0; - if (curLine == start) break; - } - } - - if (found) { - if (!DoReplace) - replaceLen = matchLen; - else { - if (isReg) { - wxString toReplace = Text->get().substr(pos,matchLen); - regex.ReplaceFirst(&toReplace,ReplaceWith); - *Text = Text->get().Left(pos) + toReplace + Text->get().substr(pos+matchLen); - replaceLen = toReplace.size(); - } - else { - *Text = Text->get().Left(pos) + ReplaceWith + Text->get().substr(pos+matchLen); - replaceLen = ReplaceWith.size(); - } - - context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); - } - - context->subsGrid->SelectRow(curLine,false); - context->subsGrid->MakeCellVisible(curLine,0); - if (field == FIELD_TEXT) { - context->selectionController->SetActiveLine(context->subsGrid->GetDialogue(curLine)); - context->textSelectionController->SetSelection(pos, pos + replaceLen); - } - // hAx to prevent double match on style/actor - else - replaceLen = 99999; - } - LastWasFind = !DoReplace; -} - -void SearchReplaceEngine::ReplaceAll() { - size_t count = 0; - - int regFlags = wxRE_ADVANCED; - if (!matchCase) - regFlags |= wxRE_ICASE; - wxRegEx reg; - if (isReg) - reg.Compile(LookFor, regFlags); - - SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); - bool hasSelection = !sel.empty(); - bool inSel = affect == LIMIT_SELECTED; - - for (auto diag : context->ass->Line | agi::of_type()) { - if (inSel && hasSelection && !sel.count(diag)) - continue; - - boost::flyweight *Text = get_text(diag, field); - - if (isReg) { - if (reg.Matches(*Text)) { - size_t start, len; - reg.GetMatch(&start, &len); - - // A zero length match (such as '$') will always be replaced - // maxMatches times, which is almost certainly not what the user - // wanted, so limit it to one replacement in that situation - wxString repl(*Text); - count += reg.Replace(&repl, ReplaceWith, len > 0 ? 1000 : 1); - *Text = repl; - } - } - else { - if (!matchCase) { - bool replaced = false; - wxString Left, Right = *Text; - size_t pos = 0; - Left.reserve(Right.size()); - while (pos + LookFor.size() <= Right.size()) { - if (Right.substr(pos, LookFor.size()).CmpNoCase(LookFor) == 0) { - Left.Append(Right.Left(pos)).Append(ReplaceWith); - Right = Right.substr(pos + LookFor.size()); - ++count; - replaced = true; - pos = 0; - } - else { - pos++; - } - } - if (replaced) { - *Text = Left + Right; - } - } - else if(Text->get().Contains(LookFor)) { - wxString repl(*Text); - count += repl.Replace(LookFor, ReplaceWith); - *Text = repl; - } - } - } - - if (count > 0) { - context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); - wxMessageBox(wxString::Format(_("%i matches were replaced."), (int)count)); - } - else { - wxMessageBox(_("No matches found.")); - } - LastWasFind = false; -} - -void SearchReplaceEngine::OnDialogOpen() { - wxArrayInt sels = context->subsGrid->GetSelection(); - curLine = 0; - if (sels.Count() > 0) curLine = sels[0]; - - LastWasFind = true; - pos = 0; - matchLen = 0; - replaceLen = 0; -} - -void SearchReplaceEngine::OpenDialog(bool replace) { +void DialogSearchReplace::Show(agi::Context *context, bool replace) { static DialogSearchReplace *diag = nullptr; - if (diag && replace != hasReplace) { + if (diag && replace != diag->has_replace) { // Already opened, but wrong type - destroy and create the right one diag->Destroy(); diag = nullptr; @@ -436,7 +165,6 @@ void SearchReplaceEngine::OpenDialog(bool replace) { if (!diag) diag = new DialogSearchReplace(context, replace); - diag->FindEdit->SetFocus(); - diag->Show(); - hasReplace = replace; + diag->find_edit->SetFocus(); + diag->wxDialog::Show(); } diff --git a/aegisub/src/dialog_search_replace.h b/aegisub/src/dialog_search_replace.h index 639f3c1bd..5fe13c682 100644 --- a/aegisub/src/dialog_search_replace.h +++ b/aegisub/src/dialog_search_replace.h @@ -1,29 +1,16 @@ -// Copyright (c) 2005, Rodrigo Braz Monteiro -// All rights reserved. +// Copyright (c) 2013, Thomas Goyne // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// 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. // -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the Aegisub Group nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// 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/ @@ -32,61 +19,28 @@ /// @ingroup secondary_ui /// +#include + #include -#include namespace agi { struct Context; } -class wxCheckBox; +class SearchReplaceEngine; +struct SearchReplaceSettings; class wxComboBox; -class wxRadioBox; - -class SearchReplaceEngine { - agi::Context *context; - - int curLine; - size_t pos; - size_t matchLen; - size_t replaceLen; - bool LastWasFind; - bool hasReplace; - bool isReg; - bool matchCase; - bool initialized; - int field; - int affect; - wxString LookFor; - wxString ReplaceWith; - -public: - void FindNext() { ReplaceNext(false); } - void ReplaceNext(bool DoReplace=true); - void ReplaceAll(); - void OpenDialog(bool HasReplace); - void OnDialogOpen(); - - SearchReplaceEngine(agi::Context *c); - friend class DialogSearchReplace; -}; class DialogSearchReplace : public wxDialog { - friend class SearchReplaceEngine; - agi::Context *c; - - bool hasReplace; - - wxComboBox *FindEdit; - wxComboBox *ReplaceEdit; - wxCheckBox *CheckMatchCase; - wxCheckBox *CheckRegExp; - wxCheckBox *CheckUpdateVideo; - wxRadioBox *Affect; - wxRadioBox *Field; + agi::scoped_ptr settings; + bool has_replace; + wxComboBox *find_edit; + wxComboBox *replace_edit; void UpdateDropDowns(); - void FindReplace(int mode); // 0 = find, 1 = replace next, 2 = replace all + void FindReplace(bool (SearchReplaceEngine::*func)()); public: - DialogSearchReplace(agi::Context* c, bool withReplace); + static void Show(agi::Context *context, bool with_replace); + + DialogSearchReplace(agi::Context* c, bool with_replace); ~DialogSearchReplace(); }; diff --git a/aegisub/src/frame_main.cpp b/aegisub/src/frame_main.cpp index fb63df657..e16f1a968 100644 --- a/aegisub/src/frame_main.cpp +++ b/aegisub/src/frame_main.cpp @@ -59,12 +59,12 @@ #include "command/command.h" #include "dialog_detached_video.h" #include "dialog_manager.h" -#include "dialog_search_replace.h" #include "dialog_version_check.h" #include "help_button.h" #include "libresrc/libresrc.h" #include "main.h" #include "options.h" +#include "search_replace_engine.h" #include "standard_paths.h" #include "subs_edit_box.h" #include "subs_edit_ctrl.h" diff --git a/aegisub/src/search_replace_engine.cpp b/aegisub/src/search_replace_engine.cpp new file mode 100644 index 000000000..db917ed42 --- /dev/null +++ b/aegisub/src/search_replace_engine.cpp @@ -0,0 +1,264 @@ +// Copyright (c) 2005, Rodrigo Braz Monteiro +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Aegisub Project http://www.aegisub.org/ + +#include "config.h" + +#include "search_replace_engine.h" + +#include "ass_dialogue.h" +#include "ass_file.h" +#include "include/aegisub/context.h" +#include "subs_grid.h" +#include "text_selection_controller.h" + +#include + +#include +#include + +SearchReplaceEngine::SearchReplaceEngine(agi::Context *c) +: context(c) +, cur_line(0) +, pos(0) +, match_len(0) +, replace_len(0) +, last_was_find(true) +, initialized(false) +{ +} + +static boost::flyweight *get_text(AssDialogue *cur, SearchReplaceSettings::Field field) { + switch (field) { + case SearchReplaceSettings::Field::TEXT: return &cur->Text; + case SearchReplaceSettings::Field::STYLE: return &cur->Style; + case SearchReplaceSettings::Field::ACTOR: return &cur->Actor; + case SearchReplaceSettings::Field::EFFECT: return &cur->Effect; + } + throw agi::InternalError("Bad field for search", 0); +} + +bool SearchReplaceEngine::FindReplace(bool replace) { + if (!initialized) + return false; + + wxArrayInt sels = context->subsGrid->GetSelection(); + int firstLine = sels.empty() ? 0 : sels.front(); + + // if selection has changed reset values + if (firstLine != cur_line) { + cur_line = firstLine; + last_was_find = true; + pos = 0; + match_len = 0; + replace_len = 0; + } + + // Setup + int start = cur_line; + int nrows = context->subsGrid->GetRows(); + bool found = false; + int regFlags = wxRE_ADVANCED; + if (!settings.match_case) { + if (settings.use_regex) + regFlags |= wxRE_ICASE; + else + settings.find.MakeLower(); + } + wxRegEx regex; + if (settings.use_regex) { + regex.Compile(settings.find, regFlags); + + if (!regex.IsValid()) { + last_was_find = !replace; + return true; + } + } + + // Search for it + boost::flyweight *Text = nullptr; + while (!found) { + Text = get_text(context->subsGrid->GetDialogue(cur_line), settings.field); + size_t tempPos; + if (replace && last_was_find) + tempPos = pos; + else + tempPos = pos + replace_len; + + if (settings.use_regex) { + if (regex.Matches(Text->get().substr(tempPos))) { + size_t match_start; + regex.GetMatch(&match_start, &match_len, 0); + pos = match_start + tempPos; + found = true; + } + } + else { + wxString src = Text->get().substr(tempPos); + if (!settings.match_case) src.MakeLower(); + size_t textPos = src.find(settings.find); + if (textPos != src.npos) { + pos = tempPos+textPos; + found = true; + match_len = settings.find.size(); + } + } + + // Didn't find, go to next line + if (!found) { + cur_line = (cur_line + 1) % nrows; + pos = 0; + match_len = 0; + replace_len = 0; + if (cur_line == start) break; + } + } + + if (found) { + if (!replace) + replace_len = match_len; + else { + if (settings.use_regex) { + wxString toReplace = Text->get().substr(pos,match_len); + regex.ReplaceFirst(&toReplace,settings.replace_with); + *Text = Text->get().Left(pos) + toReplace + Text->get().substr(pos+match_len); + replace_len = toReplace.size(); + } + else { + *Text = Text->get().Left(pos) + settings.replace_with + Text->get().substr(pos+match_len); + replace_len = settings.replace_with.size(); + } + + context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); + } + + context->subsGrid->SelectRow(cur_line,false); + context->subsGrid->MakeCellVisible(cur_line,0); + if (settings.field == SearchReplaceSettings::Field::TEXT) { + context->selectionController->SetActiveLine(context->subsGrid->GetDialogue(cur_line)); + context->textSelectionController->SetSelection(pos, pos + replace_len); + } + // hAx to prevent double match on style/actor + else + replace_len = 99999; + } + last_was_find = !replace; + + return true; +} + +bool SearchReplaceEngine::ReplaceAll() { + if (!initialized) + return false; + + size_t count = 0; + + int regFlags = wxRE_ADVANCED; + if (!settings.match_case) + regFlags |= wxRE_ICASE; + wxRegEx reg; + if (settings.use_regex) + reg.Compile(settings.find, regFlags); + + SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); + bool hasSelection = !sel.empty(); + bool inSel = settings.limit_to == SearchReplaceSettings::Limit::SELECTED; + + for (auto diag : context->ass->Line | agi::of_type()) { + if (inSel && hasSelection && !sel.count(diag)) + continue; + + boost::flyweight *Text = get_text(diag, settings.field); + + if (settings.use_regex) { + if (reg.Matches(*Text)) { + size_t start, len; + reg.GetMatch(&start, &len); + + // A zero length match (such as '$') will always be replaced + // maxMatches times, which is almost certainly not what the user + // wanted, so limit it to one replacement in that situation + wxString repl(*Text); + count += reg.Replace(&repl, settings.replace_with, len > 0 ? 1000 : 1); + *Text = repl; + } + } + else { + if (!settings.match_case) { + bool replaced = false; + wxString Left, Right = *Text; + size_t pos = 0; + Left.reserve(Right.size()); + while (pos + settings.find.size() <= Right.size()) { + if (Right.substr(pos, settings.find.size()).CmpNoCase(settings.find) == 0) { + Left.Append(Right.Left(pos)).Append(settings.replace_with); + Right = Right.substr(pos + settings.find.size()); + ++count; + replaced = true; + pos = 0; + } + else { + pos++; + } + } + if (replaced) { + *Text = Left + Right; + } + } + else if(Text->get().Contains(settings.find)) { + wxString repl(*Text); + count += repl.Replace(settings.find, settings.replace_with); + *Text = repl; + } + } + } + + if (count > 0) { + context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); + wxMessageBox(wxString::Format(_("%i matches were replaced."), (int)count)); + } + else { + wxMessageBox(_("No matches found.")); + } + last_was_find = false; + + return true; +} + +void SearchReplaceEngine::Configure(SearchReplaceSettings const& new_settings) { + wxArrayInt sels = context->subsGrid->GetSelection(); + cur_line = 0; + if (sels.size() > 0) cur_line = sels[0]; + + last_was_find = true; + pos = 0; + match_len = 0; + replace_len = 0; + + settings = new_settings; +} diff --git a/aegisub/src/search_replace_engine.h b/aegisub/src/search_replace_engine.h new file mode 100644 index 000000000..5a9361bd7 --- /dev/null +++ b/aegisub/src/search_replace_engine.h @@ -0,0 +1,66 @@ +// 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 + +namespace agi { struct Context; } + +struct SearchReplaceSettings { + enum class Field { + TEXT = 0, + STYLE, + ACTOR, + EFFECT + }; + + enum class Limit { + ALL = 0, + SELECTED + }; + + wxString find; + wxString replace_with; + + Field field; + Limit limit_to; + + bool match_case; + bool use_regex; +}; + +class SearchReplaceEngine { + agi::Context *context; + + int cur_line; + size_t pos; + size_t match_len; + size_t replace_len; + bool last_was_find; + bool initialized; + + SearchReplaceSettings settings; + + bool FindReplace(bool replace); + +public: + bool FindNext() { return FindReplace(false); } + bool ReplaceNext() { return FindReplace(true); } + bool ReplaceAll(); + + void Configure(SearchReplaceSettings const& new_settings); + + SearchReplaceEngine(agi::Context *c); +}; diff --git a/aegisub/src/validators.h b/aegisub/src/validators.h index 18a0ba903..fc07ec24b 100644 --- a/aegisub/src/validators.h +++ b/aegisub/src/validators.h @@ -32,6 +32,9 @@ /// @ingroup custom_control utility /// +#include + +#include #include /// A wx validator that only allows valid numbers @@ -88,3 +91,35 @@ public: DECLARE_EVENT_TABLE() }; + +template +class EnumBinder : public wxValidator { + T *value; + + wxObject *Clone() const override { return new EnumBinder(value); } + + bool TransferFromWindow() override { + if (wxRadioBox *rb = dynamic_cast(GetWindow())) + *value = static_cast(rb->GetSelection()); + else + throw agi::InternalError("Control type not supported by EnumBinder", 0); + return true; + } + + bool TransferToWindow() override { + if (wxRadioBox *rb = dynamic_cast(GetWindow())) + rb->SetSelection(static_cast(*value)); + else + throw agi::InternalError("Control type not supported by EnumBinder", 0); + return true; + } + +public: + explicit EnumBinder(T *value) : value(value) { } + EnumBinder(EnumBinder const& rhs) : value(rhs.value) { } +}; + +template +EnumBinder MakeEnumBinder(T *value) { + return EnumBinder(value); +}