1 // Copyright (c) 2012, Thomas Goyne <plorkyeran@aegisub.org>
2 //
3 // Permission to use, copy, modify, and distribute this software for any
4 // purpose with or without fee is hereby granted, provided that the above
5 // copyright notice and this permission notice appear in all copies.
6 //
7 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 //
15 // Aegisub Project http://www.aegisub.org/
16
17 #include "font_file_lister.h"
18
19 #include "compat.h"
20 #include "dialog_manager.h"
21 #include "format.h"
22 #include "help_button.h"
23 #include "include/aegisub/context.h"
24 #include "libresrc/libresrc.h"
25 #include "options.h"
26 #include "utils.h"
27
28 #include <libaegisub/dispatch.h>
29 #include <libaegisub/format_path.h>
30 #include <libaegisub/fs.h>
31 #include <libaegisub/path.h>
32 #include <libaegisub/make_unique.h>
33
34 #include <wx/button.h>
35 #include <wx/dialog.h>
36 #include <wx/dirdlg.h>
37 #include <wx/filedlg.h>
38 #include <wx/filename.h>
39 #include <wx/msgdlg.h>
40 #include <wx/radiobox.h>
41 #include <wx/sizer.h>
42 #include <wx/statbox.h>
43 #include <wx/stattext.h>
44 #include <wx/stc/stc.h>
45 #include <wx/textctrl.h>
46 #include <wx/wfstream.h>
47 #include <wx/zipstrm.h>
48
49 namespace {
50 class DialogFontsCollector final : public wxDialog {
51 AssFile *subs;
52
53 wxStyledTextCtrl *collection_log;
54 wxButton *close_btn;
55 wxButton *dest_browse_button;
56 wxButton *start_btn;
57 wxRadioBox *collection_mode;
58 wxStaticText *dest_label;
59 wxTextCtrl *dest_ctrl;
60
61 void OnStart(wxCommandEvent &);
62 void OnBrowse(wxCommandEvent &);
63 void OnRadio(wxCommandEvent &e);
64
65 /// Append text to log message from worker thread
66 void OnAddText(wxThreadEvent &event);
67 /// Collection complete notification from the worker thread to reenable buttons
68 void OnCollectionComplete(wxThreadEvent &);
69
70 public:
71 DialogFontsCollector(agi::Context *c);
72 };
73
74 enum FcMode {
75 CheckFontsOnly = 0,
76 CopyToFolder = 1,
77 CopyToScriptFolder = 2,
78 CopyToZip = 3,
79 SymlinkToFolder = 4
80 };
81
82 wxDEFINE_EVENT(EVT_ADD_TEXT, wxThreadEvent);
83 wxDEFINE_EVENT(EVT_COLLECTION_DONE, wxThreadEvent);
84
FontsCollectorThread(AssFile * subs,agi::fs::path const & destination,FcMode oper,wxEvtHandler * collector)85 void FontsCollectorThread(AssFile *subs, agi::fs::path const& destination, FcMode oper, wxEvtHandler *collector) {
86 agi::dispatch::Background().Async([=]{
87 auto AppendText = [&](wxString text, int colour) {
88 wxThreadEvent event(EVT_ADD_TEXT);
89 event.SetPayload(std::make_pair(colour, text));
90 collector->AddPendingEvent(event);
91 };
92
93 auto paths = FontCollector(AppendText).GetFontPaths(subs);
94 if (paths.empty()) {
95 collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
96 return;
97 }
98
99 // Copy fonts
100 switch (oper) {
101 case CheckFontsOnly:
102 collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
103 return;
104 case SymlinkToFolder:
105 AppendText(_("Symlinking fonts to folder...\n"), 0);
106 break;
107 case CopyToScriptFolder:
108 case CopyToFolder:
109 AppendText(_("Copying fonts to folder...\n"), 0);
110 break;
111 case CopyToZip:
112 AppendText(_("Copying fonts to archive...\n"), 0);
113 break;
114 }
115
116 // Open zip stream if saving to compressed archive
117 std::unique_ptr<wxFFileOutputStream> out;
118 std::unique_ptr<wxZipOutputStream> zip;
119 if (oper == CopyToZip) {
120 try {
121 agi::fs::CreateDirectory(destination.parent_path());
122 }
123 catch (agi::fs::FileSystemError const& e) {
124 AppendText(fmt_tl("* Failed to create directory '%s': %s.\n",
125 destination.parent_path().wstring(), to_wx(e.GetMessage())), 2);
126 collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
127 return;
128 }
129
130 out = agi::make_unique<wxFFileOutputStream>(destination.wstring());
131 if (out->IsOk())
132 zip = agi::make_unique<wxZipOutputStream>(*out);
133
134 if (!out->IsOk() || !zip || !zip->IsOk()) {
135 AppendText(fmt_tl("* Failed to open %s.\n", destination), 2);
136 collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
137 return;
138 }
139 }
140
141 int64_t total_size = 0;
142 bool allOk = true;
143 for (auto path : paths) {
144 path.make_preferred();
145
146 int ret = 0;
147 total_size += agi::fs::Size(path);
148
149 switch (oper) {
150 case SymlinkToFolder:
151 case CopyToScriptFolder:
152 case CopyToFolder: {
153 auto dest = destination/path.filename();
154 if (agi::fs::FileExists(dest))
155 ret = 2;
156 #ifndef _WIN32
157 else if (oper == SymlinkToFolder) {
158 // returns 0 on success, -1 on error...
159 if (symlink(path.c_str(), dest.c_str()))
160 ret = 0;
161 else
162 ret = 3;
163 }
164 #endif
165 else {
166 try {
167 agi::fs::Copy(path, dest);
168 ret = true;
169 }
170 catch (...) {
171 ret = false;
172 }
173 }
174 }
175 break;
176
177 case CopyToZip: {
178 wxFFileInputStream in(path.wstring());
179 if (!in.IsOk())
180 ret = false;
181 else {
182 ret = zip->PutNextEntry(path.filename().wstring());
183 zip->Write(in);
184 }
185 }
186 default: break;
187 }
188
189 if (ret == 1)
190 AppendText(fmt_tl("* Copied %s.\n", path), 1);
191 else if (ret == 2)
192 AppendText(fmt_tl("* %s already exists on destination.\n", path.filename()), 3);
193 else if (ret == 3)
194 AppendText(fmt_tl("* Symlinked %s.\n", path), 1);
195 else {
196 AppendText(fmt_tl("* Failed to copy %s.\n", path), 2);
197 allOk = false;
198 }
199 }
200
201 if (allOk)
202 AppendText(_("Done. All fonts copied."), 1);
203 else
204 AppendText(_("Done. Some fonts could not be copied."), 2);
205
206 if (total_size > 32 * 1024 * 1024)
207 AppendText(_("\nOver 32 MB of fonts were copied. Some of the fonts may not be loaded by the player if they are all attached to a Matroska file."), 2);
208
209 AppendText("\n", 0);
210
211 collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
212 });
213 }
214
DialogFontsCollector(agi::Context * c)215 DialogFontsCollector::DialogFontsCollector(agi::Context *c)
216 : wxDialog(c->parent, -1, _("Fonts Collector"))
217 , subs(c->ass.get())
218 {
219 SetIcon(GETICON(font_collector_button_16));
220
221 wxString modes[] = {
222 _("Check fonts for availability")
223 ,_("Copy fonts to folder")
224 ,_("Copy fonts to subtitle file's folder")
225 ,_("Copy fonts to zipped archive")
226 #ifndef _WIN32
227 ,_("Symlink fonts to folder")
228 #endif
229 };
230 collection_mode = new wxRadioBox(this, -1, _("Action"), wxDefaultPosition, wxDefaultSize, countof(modes), modes, 1);
231 collection_mode->SetSelection(mid<int>(0, OPT_GET("Tool/Fonts Collector/Action")->GetInt(), 4));
232
233 if (config::path->Decode("?script") == "?script")
234 collection_mode->Enable(2, false);
235
236 wxStaticBoxSizer *destination_box = new wxStaticBoxSizer(wxVERTICAL, this, _("Destination"));
237
238 dest_label = new wxStaticText(this, -1, " ");
239 dest_ctrl = new wxTextCtrl(this, -1, config::path->Decode(OPT_GET("Path/Fonts Collector Destination")->GetString()).wstring());
240 dest_browse_button = new wxButton(this, -1, _("&Browse..."));
241
242 wxSizer *dest_browse_sizer = new wxBoxSizer(wxHORIZONTAL);
243 dest_browse_sizer->Add(dest_ctrl, wxSizerFlags(1).Border(wxRIGHT).Align(wxALIGN_CENTER_VERTICAL));
244 dest_browse_sizer->Add(dest_browse_button, wxSizerFlags());
245
246 destination_box->Add(dest_label, wxSizerFlags().Border(wxBOTTOM));
247 destination_box->Add(dest_browse_sizer, wxSizerFlags().Expand());
248
249 wxStaticBoxSizer *log_box = new wxStaticBoxSizer(wxVERTICAL, this, _("Log"));
250 collection_log = new wxStyledTextCtrl(this, -1, wxDefaultPosition, wxSize(600, 300));
251 collection_log->SetWrapMode(wxSTC_WRAP_WORD);
252 collection_log->SetMarginWidth(1, 0);
253 collection_log->SetReadOnly(true);
254 collection_log->StyleSetForeground(1, wxColour(0, 200, 0));
255 collection_log->StyleSetForeground(2, wxColour(200, 0, 0));
256 collection_log->StyleSetForeground(3, wxColour(200, 100, 0));
257 log_box->Add(collection_log, wxSizerFlags().Border());
258
259 wxStdDialogButtonSizer *button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL | wxHELP);
260 start_btn = button_sizer->GetAffirmativeButton();
261 close_btn = button_sizer->GetCancelButton();
262 start_btn->SetLabel(_("&Start!"));
263 start_btn->SetDefault();
264
265 wxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
266 main_sizer->Add(collection_mode, wxSizerFlags().Expand().Border());
267 main_sizer->Add(destination_box, wxSizerFlags().Expand().Border(wxALL & ~wxTOP));
268 main_sizer->Add(log_box, wxSizerFlags().Border(wxALL & ~wxTOP));
269 main_sizer->Add(button_sizer, wxSizerFlags().Right().Border(wxALL & ~wxTOP));
270
271 SetSizerAndFit(main_sizer);
272 CenterOnParent();
273
274 // Update the browse button and label
275 wxCommandEvent evt;
276 OnRadio(evt);
277
278 start_btn->Bind(wxEVT_BUTTON, &DialogFontsCollector::OnStart, this);
279 dest_browse_button->Bind(wxEVT_BUTTON, &DialogFontsCollector::OnBrowse, this);
280 collection_mode->Bind(wxEVT_RADIOBOX, &DialogFontsCollector::OnRadio, this);
281 button_sizer->GetHelpButton()->Bind(wxEVT_BUTTON, std::bind(&HelpButton::OpenPage, "Fonts Collector"));
282 Bind(EVT_ADD_TEXT, &DialogFontsCollector::OnAddText, this);
283 Bind(EVT_COLLECTION_DONE, &DialogFontsCollector::OnCollectionComplete, this);
284 }
285
OnStart(wxCommandEvent &)286 void DialogFontsCollector::OnStart(wxCommandEvent &) {
287 collection_log->SetReadOnly(false);
288 collection_log->ClearAll();
289 collection_log->SetReadOnly(true);
290
291 agi::fs::path dest;
292 int action = collection_mode->GetSelection();
293 OPT_SET("Tool/Fonts Collector/Action")->SetInt(action);
294 if (action != CheckFontsOnly) {
295 dest = config::path->Decode(action == CopyToScriptFolder ? "?script/" : from_wx(dest_ctrl->GetValue()));
296
297 if (action != CopyToZip) {
298 if (agi::fs::FileExists(dest))
299 wxMessageBox(_("Invalid destination."), _("Error"), wxOK | wxICON_ERROR | wxCENTER, this);
300 try {
301 agi::fs::CreateDirectory(dest);
302 }
303 catch (agi::Exception const&) {
304 wxMessageBox(_("Could not create destination folder."), _("Error"), wxOK | wxICON_ERROR | wxCENTER, this);
305 return;
306 }
307 }
308 else if (agi::fs::DirectoryExists(dest) || dest.filename().empty()) {
309 wxMessageBox(_("Invalid path for .zip file."), _("Error"), wxOK | wxICON_ERROR | wxCENTER, this);
310 return;
311 }
312 }
313
314 if (action != CheckFontsOnly)
315 OPT_SET("Path/Fonts Collector Destination")->SetString(dest.string());
316
317 // Disable the UI while it runs as we don't support canceling
318 EnableCloseButton(false);
319 start_btn->Enable(false);
320 dest_browse_button->Enable(false);
321 dest_ctrl->Enable(false);
322 close_btn->Enable(false);
323 collection_mode->Enable(false);
324 dest_label->Enable(false);
325
326 FontsCollectorThread(subs, dest, static_cast<FcMode>(action), GetEventHandler());
327 }
328
OnBrowse(wxCommandEvent &)329 void DialogFontsCollector::OnBrowse(wxCommandEvent &) {
330 wxString dest;
331 if (collection_mode->GetSelection() == CopyToZip) {
332 dest = wxFileSelector(
333 _("Select archive file name"),
334 dest_ctrl->GetValue(),
335 wxFileName(dest_ctrl->GetValue()).GetFullName(),
336 ".zip", "Zip Archives (*.zip)|*.zip",
337 wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
338 }
339 else
340 dest = wxDirSelector(_("Select folder to save fonts on"), dest_ctrl->GetValue(), 0);
341
342 if (!dest.empty())
343 dest_ctrl->SetValue(dest);
344 }
345
OnRadio(wxCommandEvent &)346 void DialogFontsCollector::OnRadio(wxCommandEvent &) {
347 int value = collection_mode->GetSelection();
348 wxString dst = dest_ctrl->GetValue();
349
350 if (value == CheckFontsOnly || value == CopyToScriptFolder) {
351 dest_ctrl->Enable(false);
352 dest_browse_button->Enable(false);
353 dest_label->Enable(false);
354 dest_label->SetLabel(_("N/A"));
355 }
356 else {
357 dest_ctrl->Enable(true);
358 dest_browse_button->Enable(true);
359 dest_label->Enable(true);
360
361 if (value == CopyToFolder || value == SymlinkToFolder) {
362 dest_label->SetLabel(_("Choose the folder where the fonts will be collected to. It will be created if it doesn't exist."));
363
364 // Remove filename from browse box
365 if (dst.Right(4) == ".zip")
366 dest_ctrl->SetValue(wxFileName(dst).GetPath());
367 }
368 else {
369 dest_label->SetLabel(_("Enter the name of the destination zip file to collect the fonts to. If a folder is entered, a default name will be used."));
370
371 // Add filename to browse box
372 if (!dst.EndsWith(".zip")) {
373 wxFileName fn(dst + "//");
374 fn.SetFullName("fonts.zip");
375 dest_ctrl->SetValue(fn.GetFullPath());
376 }
377 }
378 }
379 }
380
OnAddText(wxThreadEvent & event)381 void DialogFontsCollector::OnAddText(wxThreadEvent &event) {
382 std::pair<int, wxString> str = event.GetPayload<std::pair<int, wxString>>();
383 collection_log->SetReadOnly(false);
384 int pos = collection_log->GetLength();
385 auto const& utf8 = str.second.utf8_str();
386 collection_log->AppendTextRaw(utf8.data(), utf8.length());
387 if (str.first) {
388 collection_log->StartStyling(pos, 31);
389 collection_log->SetStyling(utf8.length(), str.first);
390 }
391 collection_log->GotoPos(pos + utf8.length());
392 collection_log->SetReadOnly(true);
393 }
394
OnCollectionComplete(wxThreadEvent &)395 void DialogFontsCollector::OnCollectionComplete(wxThreadEvent &) {
396 EnableCloseButton(true);
397 start_btn->Enable();
398 close_btn->Enable();
399 collection_mode->Enable();
400 if (config::path->Decode("?script") == "?script")
401 collection_mode->Enable(2, false);
402
403 wxCommandEvent evt;
404 OnRadio(evt);
405 }
406 }
407
ShowFontsCollectorDialog(agi::Context * c)408 void ShowFontsCollectorDialog(agi::Context *c) {
409 c->dialog->Show<DialogFontsCollector>(c);
410 }
411