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 ¤t 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