1 // Copyright (c) 2005, Rodrigo Braz Monteiro
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 //   * Redistributions of source code must retain the above copyright notice,
8 //     this list of conditions and the following disclaimer.
9 //   * Redistributions in binary form must reproduce the above copyright notice,
10 //     this list of conditions and the following disclaimer in the documentation
11 //     and/or other materials provided with the distribution.
12 //   * Neither the name of the Aegisub Group nor the names of its contributors
13 //     may be used to endorse or promote products derived from this software
14 //     without specific prior written permission.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 // POSSIBILITY OF SUCH DAMAGE.
27 //
28 // Aegisub Project http://www.aegisub.org/
29 
30 #include "subs_edit_ctrl.h"
31 
32 #include "ass_dialogue.h"
33 #include "command/command.h"
34 #include "compat.h"
35 #include "format.h"
36 #include "options.h"
37 #include "include/aegisub/context.h"
38 #include "include/aegisub/spellchecker.h"
39 #include "selection_controller.h"
40 #include "text_selection_controller.h"
41 #include "thesaurus.h"
42 #include "utils.h"
43 
44 #include <libaegisub/ass/dialogue_parser.h>
45 #include <libaegisub/calltip_provider.h>
46 #include <libaegisub/character_count.h>
47 #include <libaegisub/make_unique.h>
48 #include <libaegisub/spellchecker.h>
49 
50 #include <boost/algorithm/string/predicate.hpp>
51 #include <boost/algorithm/string/replace.hpp>
52 #include <functional>
53 
54 #include <wx/clipbrd.h>
55 #include <wx/intl.h>
56 #include <wx/menu.h>
57 #include <wx/settings.h>
58 
59 /// Event ids
60 enum {
61 	EDIT_MENU_SPLIT_PRESERVE = 1400,
62 	EDIT_MENU_SPLIT_ESTIMATE,
63 	EDIT_MENU_SPLIT_VIDEO,
64 	EDIT_MENU_CUT,
65 	EDIT_MENU_COPY,
66 	EDIT_MENU_PASTE,
67 	EDIT_MENU_SELECT_ALL,
68 	EDIT_MENU_ADD_TO_DICT,
69 	EDIT_MENU_REMOVE_FROM_DICT,
70 	EDIT_MENU_SUGGESTION,
71 	EDIT_MENU_SUGGESTIONS,
72 	EDIT_MENU_THESAURUS = 1450,
73 	EDIT_MENU_THESAURUS_SUGS,
74 	EDIT_MENU_DIC_LANGUAGE = 1600,
75 	EDIT_MENU_DIC_LANGS,
76 	EDIT_MENU_THES_LANGUAGE = 1700,
77 	EDIT_MENU_THES_LANGS
78 };
79 
SubsTextEditCtrl(wxWindow * parent,wxSize wsize,long style,agi::Context * context)80 SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, agi::Context *context)
81 : wxStyledTextCtrl(parent, -1, wxDefaultPosition, wsize, style)
82 , spellchecker(SpellCheckerFactory::GetSpellChecker())
83 , thesaurus(agi::make_unique<Thesaurus>())
84 , context(context)
85 {
86 	osx::ime::inject(this);
87 
88 	// Set properties
89 	SetWrapMode(wxSTC_WRAP_WORD);
90 	SetMarginWidth(1,0);
91 	UsePopUp(false);
92 	SetStyles();
93 
94 	// Set hotkeys
95 	CmdKeyClear(wxSTC_KEY_RETURN,wxSTC_SCMOD_CTRL);
96 	CmdKeyClear(wxSTC_KEY_RETURN,wxSTC_SCMOD_SHIFT);
97 	CmdKeyClear(wxSTC_KEY_RETURN,wxSTC_SCMOD_NORM);
98 	CmdKeyClear(wxSTC_KEY_TAB,wxSTC_SCMOD_NORM);
99 	CmdKeyClear(wxSTC_KEY_TAB,wxSTC_SCMOD_SHIFT);
100 	CmdKeyClear('D',wxSTC_SCMOD_CTRL);
101 	CmdKeyClear('L',wxSTC_SCMOD_CTRL);
102 	CmdKeyClear('L',wxSTC_SCMOD_CTRL | wxSTC_SCMOD_SHIFT);
103 	CmdKeyClear('T',wxSTC_SCMOD_CTRL);
104 	CmdKeyClear('T',wxSTC_SCMOD_CTRL | wxSTC_SCMOD_SHIFT);
105 	CmdKeyClear('U',wxSTC_SCMOD_CTRL);
106 
107 	using std::bind;
108 
109 	Bind(wxEVT_CHAR_HOOK, &SubsTextEditCtrl::OnKeyDown, this);
110 
111 	Bind(wxEVT_MENU, bind(&SubsTextEditCtrl::Cut, this), EDIT_MENU_CUT);
112 	Bind(wxEVT_MENU, bind(&SubsTextEditCtrl::Copy, this), EDIT_MENU_COPY);
113 	Bind(wxEVT_MENU, bind(&SubsTextEditCtrl::Paste, this), EDIT_MENU_PASTE);
114 	Bind(wxEVT_MENU, bind(&SubsTextEditCtrl::SelectAll, this), EDIT_MENU_SELECT_ALL);
115 
116 	if (context) {
117 		Bind(wxEVT_MENU, bind(&cmd::call, "edit/line/split/preserve", context), EDIT_MENU_SPLIT_PRESERVE);
118 		Bind(wxEVT_MENU, bind(&cmd::call, "edit/line/split/estimate", context), EDIT_MENU_SPLIT_ESTIMATE);
119 		Bind(wxEVT_MENU, bind(&cmd::call, "edit/line/split/video", context), EDIT_MENU_SPLIT_VIDEO);
120 	}
121 
122 	Bind(wxEVT_CONTEXT_MENU, &SubsTextEditCtrl::OnContextMenu, this);
123 	Bind(wxEVT_IDLE, std::bind(&SubsTextEditCtrl::UpdateCallTip, this));
124 	Bind(wxEVT_STC_DOUBLECLICK, &SubsTextEditCtrl::OnDoubleClick, this);
125 	Bind(wxEVT_STC_STYLENEEDED, [=](wxStyledTextEvent&) {
126 		{
127 			std::string text = GetTextRaw().data();
128 			if (text == line_text) return;
129 			line_text = move(text);
130 		}
131 
132 		UpdateStyle();
133 	});
134 
135 	OPT_SUB("Subtitle/Edit Box/Font Face", &SubsTextEditCtrl::SetStyles, this);
136 	OPT_SUB("Subtitle/Edit Box/Font Size", &SubsTextEditCtrl::SetStyles, this);
137 	Subscribe("Normal");
138 	Subscribe("Comment");
139 	Subscribe("Drawing");
140 	Subscribe("Brackets");
141 	Subscribe("Slashes");
142 	Subscribe("Tags");
143 	Subscribe("Error");
144 	Subscribe("Parameters");
145 	Subscribe("Line Break");
146 	Subscribe("Karaoke Template");
147 	Subscribe("Karaoke Variable");
148 
149 	OPT_SUB("Colour/Subtitle/Background", &SubsTextEditCtrl::SetStyles, this);
150 	OPT_SUB("Subtitle/Highlight/Syntax", &SubsTextEditCtrl::UpdateStyle, this);
151 	OPT_SUB("App/Call Tips", &SubsTextEditCtrl::UpdateCallTip, this);
152 
153 	Bind(wxEVT_MENU, [=](wxCommandEvent&) {
154 		if (spellchecker) spellchecker->AddWord(currentWord);
155 		UpdateStyle();
156 		SetFocus();
157 	}, EDIT_MENU_ADD_TO_DICT);
158 
159 	Bind(wxEVT_MENU, [=](wxCommandEvent&) {
160 		if (spellchecker) spellchecker->RemoveWord(currentWord);
161 		UpdateStyle();
162 		SetFocus();
163 	}, EDIT_MENU_REMOVE_FROM_DICT);
164 }
165 
~SubsTextEditCtrl()166 SubsTextEditCtrl::~SubsTextEditCtrl() {
167 }
168 
Subscribe(std::string const & name)169 void SubsTextEditCtrl::Subscribe(std::string const& name) {
170 	OPT_SUB("Colour/Subtitle/Syntax/" + name, &SubsTextEditCtrl::SetStyles, this);
171 	OPT_SUB("Colour/Subtitle/Syntax/Background/" + name, &SubsTextEditCtrl::SetStyles, this);
172 	OPT_SUB("Colour/Subtitle/Syntax/Bold/" + name, &SubsTextEditCtrl::SetStyles, this);
173 }
174 
BEGIN_EVENT_TABLE(SubsTextEditCtrl,wxStyledTextCtrl)175 BEGIN_EVENT_TABLE(SubsTextEditCtrl,wxStyledTextCtrl)
176 	EVT_KILL_FOCUS(SubsTextEditCtrl::OnLoseFocus)
177 
178 	EVT_MENU_RANGE(EDIT_MENU_SUGGESTIONS,EDIT_MENU_THESAURUS-1,SubsTextEditCtrl::OnUseSuggestion)
179 	EVT_MENU_RANGE(EDIT_MENU_THESAURUS_SUGS,EDIT_MENU_DIC_LANGUAGE-1,SubsTextEditCtrl::OnUseSuggestion)
180 	EVT_MENU_RANGE(EDIT_MENU_DIC_LANGS,EDIT_MENU_THES_LANGUAGE-1,SubsTextEditCtrl::OnSetDicLanguage)
181 	EVT_MENU_RANGE(EDIT_MENU_THES_LANGS,EDIT_MENU_THES_LANGS+100,SubsTextEditCtrl::OnSetThesLanguage)
182 END_EVENT_TABLE()
183 
184 void SubsTextEditCtrl::OnLoseFocus(wxFocusEvent &event) {
185 	CallTipCancel();
186 	event.Skip();
187 }
188 
OnKeyDown(wxKeyEvent & event)189 void SubsTextEditCtrl::OnKeyDown(wxKeyEvent &event) {
190 	if (osx::ime::process_key_event(this, event)) return;
191 	event.Skip();
192 
193 	// Workaround for wxSTC eating tabs.
194 	if (event.GetKeyCode() == WXK_TAB)
195 		Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
196 	else if (event.GetKeyCode() == WXK_RETURN && event.GetModifiers() == wxMOD_SHIFT) {
197 		auto sel_start = GetSelectionStart(), sel_end = GetSelectionEnd();
198 		wxCharBuffer old = GetTextRaw();
199 		std::string data(old.data(), sel_start);
200 		data.append("\\N");
201 		data.append(old.data() + sel_end, old.length() - sel_end);
202 		SetTextRaw(data.c_str());
203 
204 		SetSelection(sel_start + 2, sel_start + 2);
205 		event.Skip(false);
206 	}
207 }
208 
SetSyntaxStyle(int id,wxFont & font,std::string const & name,wxColor const & default_background)209 void SubsTextEditCtrl::SetSyntaxStyle(int id, wxFont &font, std::string const& name, wxColor const& default_background) {
210 	StyleSetFont(id, font);
211 	StyleSetBold(id, OPT_GET("Colour/Subtitle/Syntax/Bold/" + name)->GetBool());
212 	StyleSetForeground(id, to_wx(OPT_GET("Colour/Subtitle/Syntax/" + name)->GetColor()));
213 	const agi::OptionValue *background = OPT_GET("Colour/Subtitle/Syntax/Background/" + name);
214 	if (background->GetType() == agi::OptionType::Color)
215 		StyleSetBackground(id, to_wx(background->GetColor()));
216 	else
217 		StyleSetBackground(id, default_background);
218 }
219 
SetStyles()220 void SubsTextEditCtrl::SetStyles() {
221 	wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
222 	font.SetEncoding(wxFONTENCODING_DEFAULT); // this solves problems with some fonts not working properly
223 	wxString fontname = FontFace("Subtitle/Edit Box");
224 	if (!fontname.empty()) font.SetFaceName(fontname);
225 	font.SetPointSize(OPT_GET("Subtitle/Edit Box/Font Size")->GetInt());
226 
227 	auto default_background = to_wx(OPT_GET("Colour/Subtitle/Background")->GetColor());
228 
229 	namespace ss = agi::ass::SyntaxStyle;
230 	SetSyntaxStyle(ss::NORMAL, font, "Normal", default_background);
231 	SetSyntaxStyle(ss::COMMENT, font, "Comment", default_background);
232 	SetSyntaxStyle(ss::DRAWING, font, "Drawing", default_background);
233 	SetSyntaxStyle(ss::OVERRIDE, font, "Brackets", default_background);
234 	SetSyntaxStyle(ss::PUNCTUATION, font, "Slashes", default_background);
235 	SetSyntaxStyle(ss::TAG, font, "Tags", default_background);
236 	SetSyntaxStyle(ss::ERROR, font, "Error", default_background);
237 	SetSyntaxStyle(ss::PARAMETER, font, "Parameters", default_background);
238 	SetSyntaxStyle(ss::LINE_BREAK, font, "Line Break", default_background);
239 	SetSyntaxStyle(ss::KARAOKE_TEMPLATE, font, "Karaoke Template", default_background);
240 	SetSyntaxStyle(ss::KARAOKE_VARIABLE, font, "Karaoke Variable", default_background);
241 
242 	SetCaretForeground(StyleGetForeground(ss::NORMAL));
243 	StyleSetBackground(wxSTC_STYLE_DEFAULT, default_background);
244 
245 	// Misspelling indicator
246 	IndicatorSetStyle(0,wxSTC_INDIC_SQUIGGLE);
247 	IndicatorSetForeground(0,wxColour(255,0,0));
248 
249 	// IME pending text indicator
250 	IndicatorSetStyle(1, wxSTC_INDIC_PLAIN);
251 	IndicatorSetUnder(1, true);
252 }
253 
UpdateStyle()254 void SubsTextEditCtrl::UpdateStyle() {
255 	AssDialogue *diag = context ? context->selectionController->GetActiveLine() : nullptr;
256 	bool template_line = diag && diag->Comment && boost::istarts_with(diag->Effect.get(), "template");
257 
258 	tokenized_line = agi::ass::TokenizeDialogueBody(line_text, template_line);
259 	agi::ass::SplitWords(line_text, tokenized_line);
260 
261 	cursor_pos = -1;
262 	UpdateCallTip();
263 
264 	StartStyling(0,255);
265 
266 	if (!OPT_GET("Subtitle/Highlight/Syntax")->GetBool()) {
267 		SetStyling(line_text.size(), 0);
268 		return;
269 	}
270 
271 	if (line_text.empty()) return;
272 
273 	for (auto const& style_range : agi::ass::SyntaxHighlight(line_text, tokenized_line, spellchecker.get()))
274 		SetStyling(style_range.length, style_range.type);
275 }
276 
UpdateCallTip()277 void SubsTextEditCtrl::UpdateCallTip() {
278 	if (!OPT_GET("App/Call Tips")->GetBool()) return;
279 
280 	int pos = GetCurrentPos();
281 	if (pos == cursor_pos) return;
282 	cursor_pos = pos;
283 
284 	agi::Calltip new_calltip = agi::GetCalltip(tokenized_line, line_text, pos);
285 
286 	if (!new_calltip.text) {
287 		CallTipCancel();
288 		return;
289 	}
290 
291 	if (!CallTipActive() || calltip_position != new_calltip.tag_position || calltip_text != new_calltip.text)
292 		CallTipShow(new_calltip.tag_position, wxString::FromUTF8Unchecked(new_calltip.text));
293 
294 	calltip_position = new_calltip.tag_position;
295 	calltip_text = new_calltip.text;
296 
297 	CallTipSetHighlight(new_calltip.highlight_start, new_calltip.highlight_end);
298 }
299 
SetTextTo(std::string const & text)300 void SubsTextEditCtrl::SetTextTo(std::string const& text) {
301 	osx::ime::invalidate(this);
302 	SetEvtHandlerEnabled(false);
303 	Freeze();
304 
305 	auto insertion_point = GetInsertionPoint();
306 	if (static_cast<size_t>(insertion_point) > line_text.size())
307 		line_text = GetTextRaw().data();
308 	auto old_pos = agi::CharacterCount(line_text.begin(), line_text.begin() + insertion_point, 0);
309 	line_text.clear();
310 
311 	if (context) {
312 		context->textSelectionController->SetSelection(0, 0);
313 		SetTextRaw(text.c_str());
314 		auto pos = agi::IndexOfCharacter(text, old_pos);
315 		context->textSelectionController->SetSelection(pos, pos);
316 	}
317 	else {
318 		SetSelection(0, 0);
319 		SetTextRaw(text.c_str());
320 		auto pos = agi::IndexOfCharacter(text, old_pos);
321 		SetSelection(pos, pos);
322 	}
323 
324 	SetEvtHandlerEnabled(true);
325 	Thaw();
326 }
327 
Paste()328 void SubsTextEditCtrl::Paste() {
329 	std::string data = GetClipboard();
330 
331 	boost::replace_all(data, "\r\n", "\\N");
332 	boost::replace_all(data, "\n", "\\N");
333 	boost::replace_all(data, "\r", "\\N");
334 
335 	wxCharBuffer old = GetTextRaw();
336 	data.insert(0, old.data(), GetSelectionStart());
337 	int sel_start = data.size();
338 	data.append(old.data() + GetSelectionEnd());
339 
340 	SetTextRaw(data.c_str());
341 
342 	SetSelectionStart(sel_start);
343 	SetSelectionEnd(sel_start);
344 }
345 
OnContextMenu(wxContextMenuEvent & event)346 void SubsTextEditCtrl::OnContextMenu(wxContextMenuEvent &event) {
347 	wxPoint pos = event.GetPosition();
348 	int activePos;
349 	if (pos == wxDefaultPosition)
350 		activePos = GetCurrentPos();
351 	else
352 		activePos = PositionFromPoint(ScreenToClient(pos));
353 
354 	currentWordPos = GetBoundsOfWordAtPosition(activePos);
355 	currentWord = line_text.substr(currentWordPos.first, currentWordPos.second);
356 
357 	wxMenu menu;
358 	if (spellchecker)
359 		AddSpellCheckerEntries(menu);
360 
361 	// Append language list
362 	menu.Append(-1,_("Spell checker language"), GetLanguagesMenu(
363 		EDIT_MENU_DIC_LANGS,
364 		to_wx(OPT_GET("Tool/Spell Checker/Language")->GetString()),
365 		to_wx(spellchecker->GetLanguageList())));
366 	menu.AppendSeparator();
367 
368 	AddThesaurusEntries(menu);
369 
370 	// Standard actions
371 	menu.Append(EDIT_MENU_CUT,_("Cu&t"))->Enable(GetSelectionStart()-GetSelectionEnd() != 0);
372 	menu.Append(EDIT_MENU_COPY,_("&Copy"))->Enable(GetSelectionStart()-GetSelectionEnd() != 0);
373 	menu.Append(EDIT_MENU_PASTE,_("&Paste"))->Enable(CanPaste());
374 	menu.AppendSeparator();
375 	menu.Append(EDIT_MENU_SELECT_ALL,_("Select &All"));
376 
377 	// Split
378 	if (context) {
379 		menu.AppendSeparator();
380 		menu.Append(EDIT_MENU_SPLIT_PRESERVE, _("Split at cursor (preserve times)"));
381 		menu.Append(EDIT_MENU_SPLIT_ESTIMATE, _("Split at cursor (estimate times)"));
382 		cmd::Command *split_video = cmd::get("edit/line/split/video");
383 		menu.Append(EDIT_MENU_SPLIT_VIDEO, split_video->StrMenu(context))->Enable(split_video->Validate(context));
384 	}
385 
386 	PopupMenu(&menu);
387 }
388 
OnDoubleClick(wxStyledTextEvent & evt)389 void SubsTextEditCtrl::OnDoubleClick(wxStyledTextEvent &evt) {
390 	int pos = evt.GetPosition();
391 	if (pos == -1 && !tokenized_line.empty()) {
392 		auto tok = tokenized_line.back();
393 		SetSelection(line_text.size() - tok.length, line_text.size());
394 	}
395 	else {
396 		auto bounds = GetBoundsOfWordAtPosition(evt.GetPosition());
397 		if (bounds.second != 0)
398 			SetSelection(bounds.first, bounds.first + bounds.second);
399 		else
400 			evt.Skip();
401 	}
402 }
403 
AddSpellCheckerEntries(wxMenu & menu)404 void SubsTextEditCtrl::AddSpellCheckerEntries(wxMenu &menu) {
405 	if (currentWord.empty()) return;
406 
407 	if (spellchecker->CanRemoveWord(currentWord))
408 		menu.Append(EDIT_MENU_REMOVE_FROM_DICT, fmt_tl("Remove \"%s\" from dictionary", currentWord));
409 
410 	sugs = spellchecker->GetSuggestions(currentWord);
411 	if (spellchecker->CheckWord(currentWord)) {
412 		if (sugs.empty())
413 			menu.Append(EDIT_MENU_SUGGESTION,_("No spell checker suggestions"))->Enable(false);
414 		else {
415 			auto subMenu = new wxMenu;
416 			for (size_t i = 0; i < sugs.size(); ++i)
417 				subMenu->Append(EDIT_MENU_SUGGESTIONS+i, to_wx(sugs[i]));
418 
419 			menu.Append(-1, fmt_tl("Spell checker suggestions for \"%s\"", currentWord), subMenu);
420 		}
421 	}
422 	else {
423 		if (sugs.empty())
424 			menu.Append(EDIT_MENU_SUGGESTION,_("No correction suggestions"))->Enable(false);
425 
426 		for (size_t i = 0; i < sugs.size(); ++i)
427 			menu.Append(EDIT_MENU_SUGGESTIONS+i, to_wx(sugs[i]));
428 
429 		// Append "add word"
430 		menu.Append(EDIT_MENU_ADD_TO_DICT, fmt_tl("Add \"%s\" to dictionary", currentWord))->Enable(spellchecker->CanAddWord(currentWord));
431 	}
432 }
433 
AddThesaurusEntries(wxMenu & menu)434 void SubsTextEditCtrl::AddThesaurusEntries(wxMenu &menu) {
435 	if (currentWord.empty()) return;
436 
437 	auto results = thesaurus->Lookup(currentWord);
438 
439 	thesSugs.clear();
440 
441 	if (results.size()) {
442 		auto thesMenu = new wxMenu;
443 
444 		int curThesEntry = 0;
445 		for (auto const& result : results) {
446 			// Single word, insert directly
447 			if (result.second.empty()) {
448 				thesMenu->Append(EDIT_MENU_THESAURUS_SUGS+curThesEntry, to_wx(result.first));
449 				thesSugs.push_back(result.first);
450 				++curThesEntry;
451 			}
452 			// Multiple, create submenu
453 			else {
454 				auto subMenu = new wxMenu;
455 				for (auto const& sug : result.second) {
456 					subMenu->Append(EDIT_MENU_THESAURUS_SUGS+curThesEntry, to_wx(sug));
457 					thesSugs.push_back(sug);
458 					++curThesEntry;
459 				}
460 
461 				thesMenu->Append(-1, to_wx(result.first), subMenu);
462 			}
463 		}
464 
465 		menu.Append(-1, fmt_tl("Thesaurus suggestions for \"%s\"", currentWord), thesMenu);
466 	}
467 	else
468 		menu.Append(EDIT_MENU_THESAURUS,_("No thesaurus suggestions"))->Enable(false);
469 
470 	// Append language list
471 	menu.Append(-1,_("Thesaurus language"), GetLanguagesMenu(
472 		EDIT_MENU_THES_LANGS,
473 		to_wx(OPT_GET("Tool/Thesaurus/Language")->GetString()),
474 		to_wx(thesaurus->GetLanguageList())));
475 	menu.AppendSeparator();
476 }
477 
GetLanguagesMenu(int base_id,wxString const & curLang,wxArrayString const & langs)478 wxMenu *SubsTextEditCtrl::GetLanguagesMenu(int base_id, wxString const& curLang, wxArrayString const& langs) {
479 	auto languageMenu = new wxMenu;
480 	languageMenu->AppendRadioItem(base_id, _("Disable"))->Check(curLang.empty());
481 
482 	for (size_t i = 0; i < langs.size(); ++i)
483 		languageMenu->AppendRadioItem(base_id + i + 1, LocalizedLanguageName(langs[i]))->Check(langs[i] == curLang);
484 
485 	return languageMenu;
486 }
487 
OnUseSuggestion(wxCommandEvent & event)488 void SubsTextEditCtrl::OnUseSuggestion(wxCommandEvent &event) {
489 	std::string suggestion;
490 	int sugIdx = event.GetId() - EDIT_MENU_THESAURUS_SUGS;
491 	if (sugIdx >= 0)
492 		suggestion = thesSugs[sugIdx];
493 	else
494 		suggestion = sugs[event.GetId() - EDIT_MENU_SUGGESTIONS];
495 
496 	size_t pos;
497 	while ((pos = suggestion.rfind('(')) != std::string::npos) {
498 		// If there's only one suggestion for a word it'll be in the form "(noun) word",
499 		// so we need to trim the "(noun) " part
500 		if (pos == 0) {
501 			pos = suggestion.find(')');
502 			if (pos != std::string::npos) {
503 				if (pos + 1< suggestion.size() && suggestion[pos + 1] == ' ') ++pos;
504 				suggestion.erase(0, pos + 1);
505 			}
506 			break;
507 		}
508 
509 		// Some replacements have notes about their usage after the word in the
510 		// form "word (generic term)" that we need to remove (plus the leading space)
511 		suggestion.resize(pos - 1);
512 	}
513 
514 	// line_text needs to get cleared before SetTextRaw to ensure it gets reparsed
515 	std::string new_text;
516 	swap(line_text, new_text);
517 	SetTextRaw(new_text.replace(currentWordPos.first, currentWordPos.second, suggestion).c_str());
518 
519 	SetSelection(currentWordPos.first, currentWordPos.first + suggestion.size());
520 	SetFocus();
521 }
522 
OnSetDicLanguage(wxCommandEvent & event)523 void SubsTextEditCtrl::OnSetDicLanguage(wxCommandEvent &event) {
524 	std::vector<std::string> langs = spellchecker->GetLanguageList();
525 
526 	int index = event.GetId() - EDIT_MENU_DIC_LANGS - 1;
527 	std::string lang;
528 	if (index >= 0)
529 		lang = langs[index];
530 
531 	OPT_SET("Tool/Spell Checker/Language")->SetString(lang);
532 
533 	UpdateStyle();
534 }
535 
OnSetThesLanguage(wxCommandEvent & event)536 void SubsTextEditCtrl::OnSetThesLanguage(wxCommandEvent &event) {
537 	if (!thesaurus) return;
538 
539 	std::vector<std::string> langs = thesaurus->GetLanguageList();
540 
541 	int index = event.GetId() - EDIT_MENU_THES_LANGS - 1;
542 	std::string lang;
543 	if (index >= 0) lang = langs[index];
544 	OPT_SET("Tool/Thesaurus/Language")->SetString(lang);
545 
546 	UpdateStyle();
547 }
548 
GetBoundsOfWordAtPosition(int pos)549 std::pair<int, int> SubsTextEditCtrl::GetBoundsOfWordAtPosition(int pos) {
550 	int len = 0;
551 	for (auto const& tok : tokenized_line) {
552 		if (len + (int)tok.length > pos) {
553 			if (tok.type == agi::ass::DialogueTokenType::WORD)
554 				return {len, tok.length};
555 			return {0, 0};
556 		}
557 		len += tok.length;
558 	}
559 
560 	return {0, 0};
561 }
562