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