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 "ass_dialogue.h"
31 #include "ass_file.h"
32 #include "ass_style.h"
33 #include "ass_style_storage.h"
34 #include "charset_detect.h"
35 #include "compat.h"
36 #include "dialog_manager.h"
37 #include "dialog_style_editor.h"
38 #include "dialogs.h"
39 #include "format.h"
40 #include "help_button.h"
41 #include "include/aegisub/context.h"
42 #include "libresrc/libresrc.h"
43 #include "options.h"
44 #include "persist_location.h"
45 #include "selection_controller.h"
46 #include "subtitle_format.h"
47 
48 #include <libaegisub/fs.h>
49 #include <libaegisub/make_unique.h>
50 #include <libaegisub/path.h>
51 #include <libaegisub/signal.h>
52 #include <libaegisub/split.h>
53 #include <libaegisub/vfr.h>
54 
55 #include <algorithm>
56 #include <boost/algorithm/string/trim.hpp>
57 #include <functional>
58 #include <future>
59 #include <memory>
60 #include <vector>
61 #include <wx/bmpbuttn.h>
62 #include <wx/button.h>
63 #include <wx/checkbox.h>
64 #include <wx/combobox.h>
65 #include <wx/combobox.h>
66 #include <wx/filename.h>
67 #include <wx/fontenum.h>
68 #include <wx/intl.h>
69 #include <wx/listbox.h>
70 #include <wx/msgdlg.h>
71 #include <wx/radiobox.h>
72 #include <wx/sizer.h>
73 #include <wx/spinctrl.h>
74 #include <wx/textctrl.h>
75 #include <wx/textdlg.h>
76 #include <wx/choicdlg.h> // Keep this last so wxUSE_CHOICEDLG is set.
77 
78 namespace {
79 class DialogStyleManager final : public wxDialog {
80 	agi::Context *c; ///< Project context
81 	std::unique_ptr<PersistLocation> persist;
82 
83 	agi::signal::Connection commit_connection;
84 	agi::signal::Connection active_line_connection;
85 
86 	std::shared_future<wxArrayString> font_list;
87 
88 	/// Styles in the current subtitle file
89 	std::vector<AssStyle*> styleMap;
90 
91 	/// Style storage manager
92 	AssStyleStorage Store;
93 
94 	wxComboBox *CatalogList;
95 	wxListBox *StorageList;
96 	wxListBox *CurrentList;
97 
98 	wxButton *CatalogDelete;
99 
100 	wxButton *MoveToLocal;
101 	wxButton *MoveToStorage;
102 
103 	wxButton *StorageNew;
104 	wxButton *StorageEdit;
105 	wxButton *StorageCopy;
106 	wxButton *StorageDelete;
107 	wxButton *StorageMoveUp;
108 	wxButton *StorageMoveDown;
109 	wxButton *StorageMoveTop;
110 	wxButton *StorageMoveBottom;
111 	wxButton *StorageSort;
112 
113 	wxButton *CurrentNew;
114 	wxButton *CurrentEdit;
115 	wxButton *CurrentCopy;
116 	wxButton *CurrentDelete;
117 	wxButton *CurrentMoveUp;
118 	wxButton *CurrentMoveDown;
119 	wxButton *CurrentMoveTop;
120 	wxButton *CurrentMoveBottom;
121 	wxButton *CurrentSort;
122 
123 	/// Load the list of available storages
124 	void LoadCatalog();
125 	/// Load the style list from the subtitles file
126 	void LoadCurrentStyles(int commit_type);
127 	/// Enable/disable all of the buttons as appropriate
128 	void UpdateButtons();
129 	/// Move styles up or down
130 	/// @param storage Storage or current file styles
131 	/// @param type 0: up; 1: top; 2: down; 3: bottom; 4: sort
132 	void MoveStyles(bool storage, int type);
133 
134 	/// Open the style editor for the given style on the script
135 	/// @param style Style to edit, or nullptr for new
136 	/// @param new_name Default new name for copies
137 	void ShowCurrentEditor(AssStyle *style, std::string const& new_name = "");
138 
139 	/// Open the style editor for the given style in the storage
140 	/// @param style Style to edit, or nullptr for new
141 	/// @param new_name Default new name for copies
142 	void ShowStorageEditor(AssStyle *style, std::string const& new_name = "");
143 
144 	/// Save the storage and update the view after a change
145 	void UpdateStorage();
146 
147 	void OnChangeCatalog();
148 	void OnCatalogNew();
149 	void OnCatalogDelete();
150 
151 	void OnCopyToCurrent();
152 	void OnCopyToStorage();
153 
154 	void OnCurrentCopy();
155 	void OnCurrentDelete();
156 	void OnCurrentEdit();
157 	void OnCurrentImport();
158 	void OnCurrentNew();
159 
160 	void OnStorageCopy();
161 	void OnStorageDelete();
162 	void OnStorageEdit();
163 	void OnStorageNew();
164 
165 	void OnKeyDown(wxKeyEvent &event);
166 	void PasteToCurrent();
167 	void PasteToStorage();
168 
169 	template<class T>
170 	void CopyToClipboard(wxListBox *list, T const& v);
171 
172 	void OnActiveLineChanged(AssDialogue *new_line);
173 
174 public:
175 	DialogStyleManager(agi::Context *context);
176 };
177 
add_bitmap_button(wxWindow * parent,wxSizer * sizer,wxBitmap const & img,wxString const & tooltip)178 wxBitmapButton *add_bitmap_button(wxWindow *parent, wxSizer *sizer, wxBitmap const& img, wxString const& tooltip) {
179 	wxBitmapButton *btn = new wxBitmapButton(parent, -1, img);
180 	btn->SetToolTip(tooltip);
181 	sizer->Add(btn, wxSizerFlags().Expand());
182 	return btn;
183 }
184 
make_move_buttons(wxWindow * parent,wxButton ** up,wxButton ** down,wxButton ** top,wxButton ** bottom,wxButton ** sort)185 wxSizer *make_move_buttons(wxWindow *parent, wxButton **up, wxButton **down, wxButton **top, wxButton **bottom, wxButton **sort) {
186 	wxSizer *sizer = new wxBoxSizer(wxVERTICAL);
187 	sizer->AddStretchSpacer(1);
188 
189 	*up     = add_bitmap_button(parent, sizer, GETIMAGE(arrow_up_24), _("Move style up"));
190 	*down   = add_bitmap_button(parent, sizer, GETIMAGE(arrow_down_24), _("Move style down"));
191 	*top    = add_bitmap_button(parent, sizer, GETIMAGE(arrow_up_stop_24), _("Move style to top"));
192 	*bottom = add_bitmap_button(parent, sizer, GETIMAGE(arrow_down_stop_24), _("Move style to bottom"));
193 	*sort   = add_bitmap_button(parent, sizer, GETIMAGE(arrow_sort_24), _("Sort styles alphabetically"));
194 
195 	sizer->AddStretchSpacer(1);
196 	return sizer;
197 }
198 
make_edit_buttons(wxWindow * parent,wxString move_label,wxButton ** move,wxButton ** nw,wxButton ** edit,wxButton ** copy,wxButton ** del)199 wxSizer *make_edit_buttons(wxWindow *parent, wxString move_label, wxButton **move, wxButton **nw, wxButton **edit, wxButton **copy, wxButton **del) {
200 	wxSizer *sizer = new wxBoxSizer(wxHORIZONTAL);
201 
202 	*move = new wxButton(parent, -1, move_label);
203 	*nw = new wxButton(parent, -1, _("&New"));
204 	*edit = new wxButton(parent, -1, _("&Edit"));
205 	*copy = new wxButton(parent, -1, _("&Copy"));
206 	*del = new wxButton(parent, -1, _("&Delete"));
207 
208 	sizer->Add(*nw, wxSizerFlags(1).Expand().Border(wxRIGHT));
209 	sizer->Add(*edit, wxSizerFlags(1).Expand().Border(wxRIGHT));
210 	sizer->Add(*copy, wxSizerFlags(1).Expand().Border(wxRIGHT));
211 	sizer->Add(*del, wxSizerFlags(1).Expand());
212 
213 	return sizer;
214 }
215 
216 template<class Func>
unique_name(Func name_checker,std::string const & source_name)217 std::string unique_name(Func name_checker, std::string const& source_name) {
218 	if (name_checker(source_name)) {
219 		std::string name = agi::format(_("%s - Copy"), source_name);
220 		for (int i = 2; name_checker(name); ++i)
221 			name = agi::format(_("%s - Copy (%d)"), source_name, i);
222 		return name;
223 	}
224 	return source_name;
225 }
226 
227 template<class Func1, class Func2>
add_styles(Func1 name_checker,Func2 style_adder)228 void add_styles(Func1 name_checker, Func2 style_adder) {
229 	auto cb = GetClipboard();
230 	int failed_to_parse = 0;
231 	for (auto tok : agi::Split(cb, '\n')) {
232 		tok = boost::trim_copy(tok);
233 		if (tok.empty()) continue;
234 		try {
235 			AssStyle *s = new AssStyle(agi::str(tok));
236 			s->name = unique_name(name_checker, s->name);
237 			style_adder(s);
238 		}
239 		catch (...) {
240 			++failed_to_parse;
241 		}
242 	}
243 	if (failed_to_parse)
244 		wxMessageBox(_("Could not parse style"), _("Could not parse style"), wxOK | wxICON_EXCLAMATION);
245 }
246 
confirm_delete(int n,wxWindow * parent,wxString const & title)247 int confirm_delete(int n, wxWindow *parent, wxString const& title) {
248 	return wxMessageBox(
249 		fmt_plural(n, "Are you sure you want to delete this style?", "Are you sure you want to delete these %d styles?", n),
250 		title, wxYES_NO | wxICON_EXCLAMATION, parent);
251 }
252 
get_single_sel(wxListBox * lb)253 int get_single_sel(wxListBox *lb) {
254 	wxArrayInt selections;
255 	int n = lb->GetSelections(selections);
256 	return n == 1 ? selections[0] : -1;
257 }
258 
DialogStyleManager(agi::Context * context)259 DialogStyleManager::DialogStyleManager(agi::Context *context)
260 : wxDialog(context->parent, -1, _("Styles Manager"))
261 , c(context)
262 , commit_connection(c->ass->AddCommitListener(&DialogStyleManager::LoadCurrentStyles, this))
263 , active_line_connection(c->selectionController->AddActiveLineListener(&DialogStyleManager::OnActiveLineChanged, this))
264 , font_list(std::async(std::launch::async, []() -> wxArrayString {
265 	wxArrayString fontList = wxFontEnumerator::GetFacenames();
266 	fontList.Sort();
267 	return fontList;
268 }))
269 {
270 	using std::bind;
271 	SetIcon(GETICON(style_toolbutton_16));
272 
273 	// Catalog
274 	wxSizer *CatalogBox = new wxStaticBoxSizer(wxHORIZONTAL,this,_("Catalog of available storages"));
275 	CatalogList = new wxComboBox(this,-1, "", wxDefaultPosition, wxSize(-1,-1), 0, nullptr, wxCB_READONLY);
276 	wxButton *CatalogNew = new wxButton(this, -1, _("New"));
277 	CatalogDelete = new wxButton(this, -1, _("Delete"));
278 	CatalogBox->Add(CatalogList,1,wxEXPAND | wxRIGHT | wxALIGN_RIGHT,5);
279 	CatalogBox->Add(CatalogNew,0,wxRIGHT,5);
280 	CatalogBox->Add(CatalogDelete,0,0,0);
281 
282 	// Storage styles list
283 	wxSizer *StorageButtons = make_edit_buttons(this, _("Copy to &current script ->"), &MoveToLocal, &StorageNew, &StorageEdit, &StorageCopy, &StorageDelete);
284 
285 	wxSizer *StorageListSizer = new wxBoxSizer(wxHORIZONTAL);
286 	StorageList = new wxListBox(this, -1, wxDefaultPosition, wxSize(240,250), 0, nullptr, wxLB_EXTENDED);
287 	StorageListSizer->Add(StorageList,1,wxEXPAND | wxRIGHT,0);
288 	StorageListSizer->Add(make_move_buttons(this, &StorageMoveUp, &StorageMoveDown, &StorageMoveTop, &StorageMoveBottom, &StorageSort), wxSizerFlags().Expand());
289 
290 	wxSizer *StorageBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Storage"));
291 	StorageBox->Add(StorageListSizer,1,wxEXPAND | wxBOTTOM,5);
292 	StorageBox->Add(MoveToLocal,0,wxEXPAND | wxBOTTOM,5);
293 	StorageBox->Add(StorageButtons,0,wxEXPAND | wxBOTTOM,0);
294 
295 	// Local styles list
296 	wxButton *CurrentImport = new wxButton(this, -1, _("&Import from script..."));
297 	wxSizer *CurrentButtons = make_edit_buttons(this, _("<- Copy to &storage"), &MoveToStorage, &CurrentNew, &CurrentEdit, &CurrentCopy, &CurrentDelete);
298 
299 	wxSizer *MoveImportSizer = new wxBoxSizer(wxHORIZONTAL);
300 	MoveImportSizer->Add(MoveToStorage,1,wxEXPAND | wxRIGHT,5);
301 	MoveImportSizer->Add(CurrentImport,1,wxEXPAND,0);
302 
303 	wxSizer *CurrentListSizer = new wxBoxSizer(wxHORIZONTAL);
304 	CurrentList = new wxListBox(this, -1, wxDefaultPosition, wxSize(240,250), 0, nullptr, wxLB_EXTENDED);
305 	CurrentListSizer->Add(CurrentList,1,wxEXPAND | wxRIGHT,0);
306 	CurrentListSizer->Add(make_move_buttons(this, &CurrentMoveUp, &CurrentMoveDown, &CurrentMoveTop, &CurrentMoveBottom, &CurrentSort), wxSizerFlags().Expand());
307 
308 	wxSizer *CurrentBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Current script"));
309 	CurrentBox->Add(CurrentListSizer,1,wxEXPAND | wxBOTTOM,5);
310 	CurrentBox->Add(MoveImportSizer,0,wxEXPAND | wxBOTTOM,5);
311 	CurrentBox->Add(CurrentButtons,0,wxEXPAND | wxBOTTOM,0);
312 
313 	// Buttons
314 	wxStdDialogButtonSizer *buttonSizer = CreateStdDialogButtonSizer(wxCANCEL | wxHELP);
315 	buttonSizer->GetCancelButton()->SetLabel(_("Close"));
316 	Bind(wxEVT_BUTTON, bind(&HelpButton::OpenPage, "Styles Manager"), wxID_HELP);
317 
318 	// General layout
319 	wxSizer *StylesSizer = new wxBoxSizer(wxHORIZONTAL);
320 	StylesSizer->Add(StorageBox,0,wxRIGHT | wxEXPAND,5);
321 	StylesSizer->Add(CurrentBox,0,wxLEFT | wxEXPAND,0);
322 	wxSizer *MainSizer = new wxBoxSizer(wxVERTICAL);
323 	MainSizer->Add(CatalogBox,0,wxEXPAND | wxLEFT | wxRIGHT | wxTOP,5);
324 	MainSizer->Add(StylesSizer,1,wxEXPAND | wxALL,5);
325 	MainSizer->Add(buttonSizer,0,wxBOTTOM | wxEXPAND,5);
326 
327 	SetSizerAndFit(MainSizer);
328 
329 	// Position window
330 	persist = agi::make_unique<PersistLocation>(this, "Tool/Style Manager");
331 
332 	// Populate lists
333 	LoadCatalog();
334 	LoadCurrentStyles(AssFile::COMMIT_STYLES | AssFile::COMMIT_DIAG_META);
335 
336 	//Set key handlers for lists
337 	CatalogList->Bind(wxEVT_KEY_DOWN, &DialogStyleManager::OnKeyDown, this);
338 	StorageList->Bind(wxEVT_KEY_DOWN, &DialogStyleManager::OnKeyDown, this);
339 	CurrentList->Bind(wxEVT_KEY_DOWN, &DialogStyleManager::OnKeyDown, this);
340 
__anon4fde49240302(wxCommandEvent&) 341 	StorageMoveUp->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(true, 0); });
__anon4fde49240402(wxCommandEvent&) 342 	StorageMoveTop->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(true, 1); });
__anon4fde49240502(wxCommandEvent&) 343 	StorageMoveDown->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(true, 2); });
__anon4fde49240602(wxCommandEvent&) 344 	StorageMoveBottom->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(true, 3); });
__anon4fde49240702(wxCommandEvent&) 345 	StorageSort->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(true, 4); });
346 
__anon4fde49240802(wxCommandEvent&) 347 	CurrentMoveUp->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(false, 0); });
__anon4fde49240902(wxCommandEvent&) 348 	CurrentMoveTop->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(false, 1); });
__anon4fde49240a02(wxCommandEvent&) 349 	CurrentMoveDown->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(false, 2); });
__anon4fde49240b02(wxCommandEvent&) 350 	CurrentMoveBottom->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(false, 3); });
__anon4fde49240c02(wxCommandEvent&) 351 	CurrentSort->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { MoveStyles(false, 4); });
352 
__anon4fde49240d02(wxCommandEvent&) 353 	CatalogNew->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCatalogNew(); });
__anon4fde49240e02(wxCommandEvent&) 354 	CatalogDelete->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCatalogDelete(); });
355 
__anon4fde49240f02(wxCommandEvent&) 356 	StorageNew->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnStorageNew(); });
__anon4fde49241002(wxCommandEvent&) 357 	StorageEdit->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnStorageEdit(); });
__anon4fde49241102(wxCommandEvent&) 358 	StorageCopy->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnStorageCopy(); });
__anon4fde49241202(wxCommandEvent&) 359 	StorageDelete->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnStorageDelete(); });
360 
__anon4fde49241302(wxCommandEvent&) 361 	CurrentNew->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCurrentNew(); });
__anon4fde49241402(wxCommandEvent&) 362 	CurrentEdit->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCurrentEdit(); });
__anon4fde49241502(wxCommandEvent&) 363 	CurrentCopy->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCurrentCopy(); });
__anon4fde49241602(wxCommandEvent&) 364 	CurrentDelete->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCurrentDelete(); });
365 
__anon4fde49241702(wxCommandEvent&) 366 	CurrentImport->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCurrentImport(); });
367 
__anon4fde49241802(wxCommandEvent&) 368 	MoveToLocal->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCopyToCurrent(); });
__anon4fde49241902(wxCommandEvent&) 369 	MoveToStorage->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnCopyToStorage(); });
370 
__anon4fde49241a02(wxCommandEvent&) 371 	CatalogList->Bind(wxEVT_COMBOBOX, [=](wxCommandEvent&) { OnChangeCatalog(); });
372 
__anon4fde49241b02(wxCommandEvent&) 373 	StorageList->Bind(wxEVT_LISTBOX, [=](wxCommandEvent&) { UpdateButtons(); });
__anon4fde49241c02(wxCommandEvent&) 374 	StorageList->Bind(wxEVT_LISTBOX_DCLICK, [=](wxCommandEvent&) { OnStorageEdit(); });
375 
__anon4fde49241d02(wxCommandEvent&) 376 	CurrentList->Bind(wxEVT_LISTBOX, [=](wxCommandEvent&) { UpdateButtons(); });
__anon4fde49241e02(wxCommandEvent&) 377 	CurrentList->Bind(wxEVT_LISTBOX_DCLICK, [=](wxCommandEvent&) { OnCurrentEdit(); });
378 }
379 
LoadCurrentStyles(int commit_type)380 void DialogStyleManager::LoadCurrentStyles(int commit_type) {
381 	if (commit_type & AssFile::COMMIT_STYLES || commit_type == AssFile::COMMIT_NEW) {
382 		CurrentList->Clear();
383 		styleMap.clear();
384 
385 		for (auto& style : c->ass->Styles) {
386 			CurrentList->Append(to_wx(style.name));
387 			styleMap.push_back(&style);
388 		}
389 	}
390 
391 	if (commit_type & AssFile::COMMIT_DIAG_META) {
392 		AssDialogue *dia = c->selectionController->GetActiveLine();
393 		CurrentList->DeselectAll();
394 		if (dia && commit_type != AssFile::COMMIT_NEW)
395 			CurrentList->SetStringSelection(to_wx(dia->Style));
396 		else
397 			CurrentList->SetSelection(0);
398 	}
399 
400 	UpdateButtons();
401 }
402 
OnActiveLineChanged(AssDialogue * new_line)403 void DialogStyleManager::OnActiveLineChanged(AssDialogue *new_line) {
404 	if (new_line) {
405 		CurrentList->DeselectAll();
406 		CurrentList->SetStringSelection(to_wx(new_line->Style));
407 		UpdateButtons();
408 	}
409 }
410 
UpdateStorage()411 void DialogStyleManager::UpdateStorage() {
412 	Store.Save();
413 
414 	StorageList->Clear();
415 	StorageList->Append(to_wx(Store.GetNames()));
416 
417 	UpdateButtons();
418 }
419 
OnChangeCatalog()420 void DialogStyleManager::OnChangeCatalog() {
421 	std::string catalog(from_wx(CatalogList->GetStringSelection()));
422 	c->ass->Properties.style_storage = catalog;
423 	Store.LoadCatalog(catalog);
424 	UpdateStorage();
425 }
426 
LoadCatalog()427 void DialogStyleManager::LoadCatalog() {
428 	CatalogList->Clear();
429 
430 	// Get saved style catalogs
431 	auto catalogs = AssStyleStorage::GetCatalogs();
432 	for (auto const& c : catalogs)
433 		CatalogList->Append(c);
434 
435 	// Create a default storage if there are none
436 	if (CatalogList->IsListEmpty()) {
437 		Store.LoadCatalog("Default");
438 		Store.push_back(agi::make_unique<AssStyle>());
439 		Store.Save();
440 		CatalogList->Append("Default");
441 	}
442 
443 	// Set to default if available
444 	std::string pickStyle = c->ass->Properties.style_storage;
445 	if (pickStyle.empty())
446 		pickStyle = "Default";
447 
448 	int opt = CatalogList->FindString(to_wx(pickStyle), false);
449 	CatalogList->SetSelection(opt == wxNOT_FOUND ? 0 : opt);
450 
451 	OnChangeCatalog();
452 }
453 
OnCatalogNew()454 void DialogStyleManager::OnCatalogNew() {
455 	wxString name = wxGetTextFromUser(_("New storage name:"), _("New catalog entry"), "", this);
456 	if (!name) return;
457 
458 	// Remove bad characters from the name
459 	wxString badchars = wxFileName::GetForbiddenChars(wxPATH_DOS);
460 	int badchars_removed = 0;
461 	for (wxUniCharRef chr : name) {
462 		if (badchars.find(chr) != badchars.npos) {
463 			chr = '_';
464 			++badchars_removed;
465 		}
466 	}
467 
468 	// Make sure that there is no storage with the same name (case insensitive search since Windows filenames are case insensitive)
469 	if (CatalogList->FindString(name, false) != wxNOT_FOUND) {
470 		wxMessageBox(_("A catalog with that name already exists."), _("Catalog name conflict"), wxOK | wxICON_ERROR | wxCENTER);
471 		return;
472 	}
473 
474 	// Warn about bad characters
475 	if (badchars_removed) {
476 		wxMessageBox(
477 			fmt_tl("The specified catalog name contains one or more illegal characters. They have been replaced with underscores instead.\nThe catalog has been renamed to \"%s\".", name),
478 			_("Invalid characters"));
479 	}
480 
481 	// Add to list of storages
482 	CatalogList->Append(name);
483 	CatalogList->SetStringSelection(name);
484 	OnChangeCatalog();
485 }
486 
OnCatalogDelete()487 void DialogStyleManager::OnCatalogDelete() {
488 	if (CatalogList->GetCount() == 1) return;
489 
490 	wxString name = CatalogList->GetStringSelection();
491 	wxString message = fmt_tl("Are you sure you want to delete the storage \"%s\" from the catalog?", name);
492 	int option = wxMessageBox(message, _("Confirm delete"), wxYES_NO | wxICON_EXCLAMATION , this);
493 	if (option == wxYES) {
494 		agi::fs::Remove(config::path->Decode("?user/catalog/" + from_wx(name) + ".sty"));
495 		CatalogList->Delete(CatalogList->GetSelection());
496 		CatalogList->SetSelection(0);
497 		OnChangeCatalog();
498 	}
499 }
500 
OnCopyToStorage()501 void DialogStyleManager::OnCopyToStorage() {
502 	wxArrayInt selections;
503 	int n = CurrentList->GetSelections(selections);
504 	wxArrayString copied;
505 	copied.reserve(n);
506 	for (int i = 0; i < n; i++) {
507 		wxString styleName = CurrentList->GetString(selections[i]);
508 
509 		if (AssStyle *style = Store.GetStyle(from_wx(styleName))) {
510 			if (wxYES == wxMessageBox(fmt_tl("There is already a style with the name \"%s\" in the current storage. Overwrite?", styleName), _("Style name collision"), wxYES_NO)) {
511 				*style = *styleMap.at(selections[i]);
512 				copied.push_back(styleName);
513 			}
514 		}
515 		else {
516 			Store.push_back(agi::make_unique<AssStyle>(*styleMap.at(selections[i])));
517 			copied.push_back(styleName);
518 		}
519 	}
520 
521 	UpdateStorage();
522 	for (auto const& style_name : copied)
523 		StorageList->SetStringSelection(style_name, true);
524 
525 	UpdateButtons();
526 }
527 
OnCopyToCurrent()528 void DialogStyleManager::OnCopyToCurrent() {
529 	wxArrayInt selections;
530 	int n = StorageList->GetSelections(selections);
531 	wxArrayString copied;
532 	copied.reserve(n);
533 	for (int i = 0; i < n; i++) {
534 		wxString styleName = StorageList->GetString(selections[i]);
535 
536 		if (AssStyle *style = c->ass->GetStyle(from_wx(styleName))) {
537 			if (wxYES == wxMessageBox(fmt_tl("There is already a style with the name \"%s\" in the current script. Overwrite?", styleName), _("Style name collision"), wxYES_NO)) {
538 				*style = *Store[selections[i]];
539 				copied.push_back(styleName);
540 			}
541 		}
542 		else {
543 			c->ass->Styles.push_back(*new AssStyle(*Store[selections[i]]));
544 			copied.push_back(styleName);
545 		}
546 	}
547 
548 	c->ass->Commit(_("style copy"), AssFile::COMMIT_STYLES);
549 
550 	CurrentList->DeselectAll();
551 	for (auto const& style_name : copied)
552 		CurrentList->SetStringSelection(style_name, true);
553 	UpdateButtons();
554 }
555 
556 template<class T>
CopyToClipboard(wxListBox * list,T const & v)557 void DialogStyleManager::CopyToClipboard(wxListBox *list, T const& v) {
558 	wxArrayInt selections;
559 	list->GetSelections(selections);
560 
561 	std::string data;
562 	for(size_t i = 0; i < selections.size(); ++i) {
563 		if (i) data += "\r\n";
564 		AssStyle *s = v[selections[i]];
565 		s->UpdateData();
566 		data += s->GetEntryData();
567 	}
568 
569 	SetClipboard(data);
570 }
571 
PasteToCurrent()572 void DialogStyleManager::PasteToCurrent() {
573 	add_styles(
574 		[=](std::string const& str) { return c->ass->GetStyle(str); },
575 		[=](AssStyle *s) { c->ass->Styles.push_back(*s); });
576 
577 	c->ass->Commit(_("style paste"), AssFile::COMMIT_STYLES);
578 }
579 
PasteToStorage()580 void DialogStyleManager::PasteToStorage() {
581 	add_styles(
582 		[=](std::string const& str) { return Store.GetStyle(str); },
583 		[=](AssStyle *s) { Store.push_back(std::unique_ptr<AssStyle>(s)); });
584 
585 	UpdateStorage();
586 	StorageList->SetStringSelection(to_wx(Store.back()->name));
587 	UpdateButtons();
588 }
589 
ShowStorageEditor(AssStyle * style,std::string const & new_name)590 void DialogStyleManager::ShowStorageEditor(AssStyle *style, std::string const& new_name) {
591 	DialogStyleEditor editor(this, style, c, &Store, new_name, font_list.get());
592 	if (editor.ShowModal()) {
593 		UpdateStorage();
594 		StorageList->SetStringSelection(to_wx(editor.GetStyleName()));
595 		UpdateButtons();
596 	}
597 }
598 
OnStorageNew()599 void DialogStyleManager::OnStorageNew() {
600 	ShowStorageEditor(nullptr);
601 }
602 
OnStorageEdit()603 void DialogStyleManager::OnStorageEdit() {
604 	int sel = get_single_sel(StorageList);
605 	if (sel == -1) return;
606 	ShowStorageEditor(Store[sel]);
607 }
608 
OnStorageCopy()609 void DialogStyleManager::OnStorageCopy() {
610 	int sel = get_single_sel(StorageList);
611 	if (sel == -1) return;
612 
613 	ShowStorageEditor(Store[sel], unique_name(
614 		[=](std::string const& str) { return Store.GetStyle(str); }, Store[sel]->name));
615 }
616 
OnStorageDelete()617 void DialogStyleManager::OnStorageDelete() {
618 	wxArrayInt selections;
619 	int n = StorageList->GetSelections(selections);
620 
621 	if (confirm_delete(n, this, _("Confirm delete from storage")) == wxYES) {
622 		for (int i = 0; i < n; i++)
623 			Store.Delete(selections[i] - i);
624 		UpdateStorage();
625 	}
626 }
627 
ShowCurrentEditor(AssStyle * style,std::string const & new_name)628 void DialogStyleManager::ShowCurrentEditor(AssStyle *style, std::string const& new_name) {
629 	DialogStyleEditor editor(this, style, c, nullptr, new_name, font_list.get());
630 	if (editor.ShowModal()) {
631 		CurrentList->DeselectAll();
632 		CurrentList->SetStringSelection(to_wx(editor.GetStyleName()));
633 		UpdateButtons();
634 	}
635 }
636 
OnCurrentNew()637 void DialogStyleManager::OnCurrentNew() {
638 	ShowCurrentEditor(nullptr);
639 }
640 
OnCurrentEdit()641 void DialogStyleManager::OnCurrentEdit() {
642 	int sel = get_single_sel(CurrentList);
643 	if (sel == -1) return;
644 	ShowCurrentEditor(styleMap[sel]);
645 }
646 
OnCurrentCopy()647 void DialogStyleManager::OnCurrentCopy() {
648 	int sel = get_single_sel(CurrentList);
649 	if (sel == -1) return;
650 
651 	ShowCurrentEditor(styleMap[sel], unique_name(
652 		[=](std::string const& str) { return c->ass->GetStyle(str); },
653 		styleMap[sel]->name));
654 }
655 
OnCurrentDelete()656 void DialogStyleManager::OnCurrentDelete() {
657 	wxArrayInt selections;
658 	int n = CurrentList->GetSelections(selections);
659 
660 	if (confirm_delete(n, this, _("Confirm delete from current")) == wxYES) {
661 		for (int i = 0; i < n; i++) {
662 			delete styleMap.at(selections[i]);
663 		}
664 		c->ass->Commit(_("style delete"), AssFile::COMMIT_STYLES);
665 	}
666 }
667 
OnCurrentImport()668 void DialogStyleManager::OnCurrentImport() {
669 	auto filename = OpenFileSelector(_("Open subtitles file"), "Path/Last/Subtitles", "", "", SubtitleFormat::GetWildcards(0), this);
670 	if (filename.empty()) return;
671 
672 	std::string charset;
673 	try {
674 		charset = CharSetDetect::GetEncoding(filename);
675 	}
676 	catch (agi::UserCancelException const&) {
677 		return;
678 	}
679 
680 	AssFile temp;
681 	try {
682 		auto reader = SubtitleFormat::GetReader(filename, charset);
683 		if (!reader)
684 			wxMessageBox("Unsupported subtitle format", "Error", wxOK | wxICON_ERROR | wxCENTER, this);
685 		else
686 			reader->ReadFile(&temp, filename, 0, charset);
687 	}
688 	catch (agi::Exception const& err) {
689 		wxMessageBox(to_wx(err.GetMessage()), "Error", wxOK | wxICON_ERROR | wxCENTER, this);
690 	}
691 	catch (...) {
692 		wxMessageBox("Unknown error", "Error", wxOK | wxICON_ERROR | wxCENTER, this);
693 		return;
694 	}
695 
696 	// Get styles
697 	auto styles = temp.GetStyles();
698 	if (styles.empty()) {
699 		wxMessageBox(_("The selected file has no available styles."), _("Error Importing Styles"));
700 		return;
701 	}
702 
703 	// Get selection
704 	wxArrayInt selections;
705 	int res = GetSelectedChoices(this, selections, _("Choose styles to import:"), _("Import Styles"), to_wx(styles));
706 	if (res == -1 || selections.empty()) return;
707 	bool modified = false;
708 
709 	// Loop through selection
710 	for (auto const& sel : selections) {
711 		// Check if there is already a style with that name
712 		if (AssStyle *existing = c->ass->GetStyle(styles[sel])) {
713 			int answer = wxMessageBox(
714 				fmt_tl("There is already a style with the name \"%s\" in the current script. Overwrite?", styles[sel]),
715 				_("Style name collision"),
716 				wxYES_NO);
717 			if (answer == wxYES) {
718 				modified = true;
719 				*existing = *temp.GetStyle(styles[sel]);
720 			}
721 			continue;
722 		}
723 
724 		// Copy
725 		modified = true;
726 		c->ass->Styles.push_back(*new AssStyle(*temp.GetStyle(styles[sel])));
727 	}
728 
729 	// Update
730 	if (modified)
731 		c->ass->Commit(_("style import"), AssFile::COMMIT_STYLES);
732 }
733 
UpdateButtons()734 void DialogStyleManager::UpdateButtons() {
735 	CatalogDelete->Enable(CatalogList->GetCount() > 1);
736 
737 	// Get storage selection
738 	wxArrayInt sels;
739 	int n = StorageList->GetSelections(sels);
740 
741 	StorageEdit->Enable(n == 1);
742 	StorageCopy->Enable(n == 1);
743 	StorageDelete->Enable(n > 0);
744 	MoveToLocal->Enable(n > 0);
745 
746 	int firstStor = -1;
747 	int lastStor = -1;
748 	if (n) {
749 		firstStor = sels[0];
750 		lastStor = sels[n-1];
751 	}
752 
753 	// Check if selection is continuous
754 	bool contStor = true;
755 	for (int i = 1; i < n; ++i) {
756 		if (sels[i] != sels[i-1]+1) {
757 			contStor = false;
758 			break;
759 		}
760 	}
761 
762 	int itemsStor = StorageList->GetCount();
763 	StorageMoveUp->Enable(contStor && firstStor > 0);
764 	StorageMoveTop->Enable(contStor && firstStor > 0);
765 	StorageMoveDown->Enable(contStor && lastStor != -1 && lastStor < itemsStor-1);
766 	StorageMoveBottom->Enable(contStor && lastStor != -1 && lastStor < itemsStor-1);
767 	StorageSort->Enable(itemsStor > 1);
768 
769 	// Get current selection
770 	n = CurrentList->GetSelections(sels);
771 
772 	CurrentEdit->Enable(n == 1);
773 	CurrentCopy->Enable(n == 1);
774 	CurrentDelete->Enable(n > 0);
775 	MoveToStorage->Enable(n > 0);
776 
777 	int firstCurr = -1;
778 	int lastCurr = -1;
779 	if (n) {
780 		firstCurr = sels[0];
781 		lastCurr = sels[n-1];
782 	}
783 
784 	// Check if selection is continuous
785 	bool contCurr = true;
786 	for (int i = 1; i < n; ++i) {
787 		if (sels[i] != sels[i-1]+1) {
788 			contCurr = false;
789 			break;
790 		}
791 	}
792 
793 	int itemsCurr = CurrentList->GetCount();
794 	CurrentMoveUp->Enable(contCurr && firstCurr > 0);
795 	CurrentMoveTop->Enable(contCurr && firstCurr > 0);
796 	CurrentMoveDown->Enable(contCurr && lastCurr != -1 && lastCurr < itemsCurr-1);
797 	CurrentMoveBottom->Enable(contCurr && lastCurr != -1 && lastCurr < itemsCurr-1);
798 	CurrentSort->Enable(itemsCurr > 1);
799 }
800 
801 struct cmp_name {
802 	template<typename T>
operator ()__anon4fde49240111::cmp_name803 	bool operator()(T const& lft, T const& rgt) const { return lft->name < rgt->name; }
804 };
805 
806 template<class Cont>
do_move(Cont & styls,int type,int & first,int & last,bool storage)807 static void do_move(Cont& styls, int type, int& first, int& last, bool storage) {
808 	auto begin = styls.begin();
809 
810 	// Move up
811 	if (type == 0) {
812 		if (first == 0) return;
813 		rotate(begin + first - 1, begin + first, begin + last + 1);
814 		first--;
815 		last--;
816 	}
817 	// Move to top
818 	else if (type == 1) {
819 		rotate(begin, begin + first, begin + last + 1);
820 		last = last - first;
821 		first = 0;
822 	}
823 	// Move down
824 	else if (type == 2) {
825 		if (last + 1 == (int)styls.size()) return;
826 		rotate(begin + first, begin + last + 1, begin + last + 2);
827 		first++;
828 		last++;
829 	}
830 	// Move to bottom
831 	else if (type == 3) {
832 		rotate(begin + first, begin + last + 1, styls.end());
833 		first = styls.size() - (last - first + 1);
834 		last = styls.size() - 1;
835 	}
836 	// Sort
837 	else if (type == 4) {
838 		// Get confirmation
839 		if (storage) {
840 			int res = wxMessageBox(_("Are you sure? This cannot be undone!"), _("Sort styles"), wxYES_NO | wxCENTER);
841 			if (res == wxNO) return;
842 		}
843 
844 		sort(styls.begin(), styls.end(), cmp_name());
845 
846 		first = 0;
847 		last = 0;
848 	}
849 }
850 
MoveStyles(bool storage,int type)851 void DialogStyleManager::MoveStyles(bool storage, int type) {
852 	wxListBox *list = storage ? StorageList : CurrentList;
853 
854 	// Get selection
855 	wxArrayInt sels;
856 	int n = list->GetSelections(sels);
857 	if (n == 0 && type != 4) return;
858 
859 	int first = 0, last = 0;
860 	if (n) {
861 		first = sels.front();
862 		last = sels.back();
863 	}
864 
865 	if (storage) {
866 		do_move(Store, type, first, last, true);
867 		UpdateStorage();
868 	}
869 	else {
870 		do_move(styleMap, type, first, last, false);
871 
872 		// Replace styles
873 		size_t curn = 0;
874 		for (auto it = c->ass->Styles.begin(); it != c->ass->Styles.end(); ++it) {
875 			auto new_style_at_pos = c->ass->Styles.iterator_to(*styleMap[curn]);
876 			EntryList<AssStyle>::node_algorithms::swap_nodes(it.pointed_node(), new_style_at_pos.pointed_node());
877 			if (++curn == styleMap.size()) break;
878 			it = new_style_at_pos;
879 		}
880 
881 		c->ass->Commit(_("style move"), AssFile::COMMIT_STYLES);
882 	}
883 
884 	for (int i = 0 ; i < (int)list->GetCount(); ++i) {
885 		if (i < first || i > last)
886 			list->Deselect(i);
887 		else
888 			list->Select(i);
889 	}
890 
891 	UpdateButtons();
892 }
893 
OnKeyDown(wxKeyEvent & event)894 void DialogStyleManager::OnKeyDown(wxKeyEvent &event) {
895 	wxWindow *focus = wxWindow::FindFocus();
896 
897 	switch(event.GetKeyCode()) {
898 		case WXK_DELETE :
899 			if (focus == StorageList)
900 				OnStorageDelete();
901 			else if (focus == CurrentList)
902 				OnCurrentDelete();
903 			break;
904 
905 		case 'C' :
906 		case 'c' :
907 			if (event.CmdDown()) {
908 				if (focus == StorageList)
909 					CopyToClipboard(StorageList, Store);
910 				else if (focus == CurrentList)
911 					CopyToClipboard(CurrentList, styleMap);
912 			}
913 			break;
914 
915 		case 'V' :
916 		case 'v' :
917 			if (event.CmdDown()) {
918 				if (focus == StorageList)
919 					PasteToStorage();
920 				else if (focus == CurrentList)
921 					PasteToCurrent();
922 			}
923 			break;
924 		default:
925 			event.Skip();
926 			break;
927 	}
928 }
929 }
930 
ShowStyleManagerDialog(agi::Context * c)931 void ShowStyleManagerDialog(agi::Context *c) {
932 	c->dialog->Show<DialogStyleManager>(c);
933 }
934