1 // Copyright (c) 2013, 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 /// @file visual_tool.cpp
18 /// @brief Base class for visual typesetting functions
19 /// @ingroup visual_ts
20 
21 #include "visual_tool.h"
22 
23 #include "ass_dialogue.h"
24 #include "ass_file.h"
25 #include "ass_style.h"
26 #include "include/aegisub/context.h"
27 #include "selection_controller.h"
28 #include "video_controller.h"
29 #include "video_display.h"
30 #include "visual_tool_clip.h"
31 #include "visual_tool_drag.h"
32 #include "visual_tool_vector_clip.h"
33 
34 #include <libaegisub/ass/time.h>
35 #include <libaegisub/format.h>
36 #include <libaegisub/of_type_adaptor.h>
37 
38 #include <algorithm>
39 
40 const wxColour VisualToolBase::colour[] = {
41 	wxColour(106,32,19),
42 	wxColour(255,169,40),
43 	wxColour(255,253,185),
44 	wxColour(187,0,0)
45 };
46 
VisualToolBase(VideoDisplay * parent,agi::Context * context)47 VisualToolBase::VisualToolBase(VideoDisplay *parent, agi::Context *context)
48 : c(context)
49 , parent(parent)
50 , frame_number(c->videoController->GetFrameN())
51 , file_changed_connection(c->ass->AddCommitListener(&VisualToolBase::OnCommit, this))
52 {
53 	int script_w, script_h;
54 	c->ass->GetResolution(script_w, script_h);
55 	script_res = Vector2D(script_w, script_h);
56 	active_line = GetActiveDialogueLine();
57 	connections.push_back(c->selectionController->AddActiveLineListener(&VisualToolBase::OnActiveLineChanged, this));
58 	connections.push_back(c->videoController->AddSeekListener(&VisualToolBase::OnSeek, this));
59 	parent->Bind(wxEVT_MOUSE_CAPTURE_LOST, &VisualToolBase::OnMouseCaptureLost, this);
60 }
61 
OnCommit(int type)62 void VisualToolBase::OnCommit(int type) {
63 	holding = false;
64 	dragging = false;
65 
66 	if (type == AssFile::COMMIT_NEW || type & AssFile::COMMIT_SCRIPTINFO) {
67 		int script_w, script_h;
68 		c->ass->GetResolution(script_w, script_h);
69 		script_res = Vector2D(script_w, script_h);
70 		OnCoordinateSystemsChanged();
71 	}
72 
73 	if (type & AssFile::COMMIT_DIAG_FULL || type & AssFile::COMMIT_DIAG_ADDREM) {
74 		active_line = GetActiveDialogueLine();
75 		OnFileChanged();
76 	}
77 }
78 
OnSeek(int new_frame)79 void VisualToolBase::OnSeek(int new_frame) {
80 	if (frame_number == new_frame) return;
81 
82 	frame_number = new_frame;
83 	OnFrameChanged();
84 
85 	AssDialogue *new_line = GetActiveDialogueLine();
86 	if (new_line != active_line) {
87 		dragging = false;
88 		active_line = new_line;
89 		OnLineChanged();
90 	}
91 }
92 
OnMouseCaptureLost(wxMouseCaptureLostEvent &)93 void VisualToolBase::OnMouseCaptureLost(wxMouseCaptureLostEvent &) {
94 	holding = false;
95 	dragging = false;
96 }
97 
OnActiveLineChanged(AssDialogue * new_line)98 void VisualToolBase::OnActiveLineChanged(AssDialogue *new_line) {
99 	if (!IsDisplayed(new_line))
100 		new_line = nullptr;
101 
102 	holding = false;
103 	dragging = false;
104 	if (new_line != active_line) {
105 		active_line = new_line;
106 		OnLineChanged();
107 		parent->Render();
108 	}
109 }
110 
IsDisplayed(AssDialogue * line) const111 bool VisualToolBase::IsDisplayed(AssDialogue *line) const {
112 	int frame = c->videoController->GetFrameN();
113 	return line
114 		&& !line->Comment
115 		&& c->videoController->FrameAtTime(line->Start, agi::vfr::START) <= frame
116 		&& c->videoController->FrameAtTime(line->End, agi::vfr::END) >= frame;
117 }
118 
Commit(wxString message)119 void VisualToolBase::Commit(wxString message) {
120 	file_changed_connection.Block();
121 	if (message.empty())
122 		message = _("visual typesetting");
123 
124 	commit_id = c->ass->Commit(message, AssFile::COMMIT_DIAG_TEXT, commit_id);
125 	file_changed_connection.Unblock();
126 }
127 
GetActiveDialogueLine()128 AssDialogue* VisualToolBase::GetActiveDialogueLine() {
129 	AssDialogue *diag = c->selectionController->GetActiveLine();
130 	if (IsDisplayed(diag))
131 		return diag;
132 	return nullptr;
133 }
134 
SetDisplayArea(int x,int y,int w,int h)135 void VisualToolBase::SetDisplayArea(int x, int y, int w, int h) {
136 	if (x == video_pos.X() && y == video_pos.Y() && w == video_res.X() && h == video_res.Y()) return;
137 
138 	video_pos = Vector2D(x, y);
139 	video_res = Vector2D(w, h);
140 
141 	holding = false;
142 	dragging = false;
143 	if (parent->HasCapture())
144 		parent->ReleaseMouse();
145 	OnCoordinateSystemsChanged();
146 }
147 
ToScriptCoords(Vector2D point) const148 Vector2D VisualToolBase::ToScriptCoords(Vector2D point) const {
149 	return (point - video_pos) * script_res / video_res;
150 }
151 
FromScriptCoords(Vector2D point) const152 Vector2D VisualToolBase::FromScriptCoords(Vector2D point) const {
153 	return (point * video_res / script_res) + video_pos;
154 }
155 
156 template<class FeatureType>
VisualTool(VideoDisplay * parent,agi::Context * context)157 VisualTool<FeatureType>::VisualTool(VideoDisplay *parent, agi::Context *context)
158 : VisualToolBase(parent, context)
159 {
160 }
161 
162 template<class FeatureType>
OnMouseEvent(wxMouseEvent & event)163 void VisualTool<FeatureType>::OnMouseEvent(wxMouseEvent &event) {
164 	bool left_click = event.LeftDown();
165 	bool left_double = event.LeftDClick();
166 	shift_down = event.ShiftDown();
167 	ctrl_down = event.CmdDown();
168 	alt_down = event.AltDown();
169 
170 	mouse_pos = event.GetPosition();
171 
172 	if (event.Leaving()) {
173 		mouse_pos = Vector2D();
174 		parent->Render();
175 		return;
176 	}
177 
178 	if (!dragging) {
179 		int max_layer = INT_MIN;
180 		active_feature = nullptr;
181 		for (auto& feature : features) {
182 			if (feature.IsMouseOver(mouse_pos) && feature.layer >= max_layer) {
183 				active_feature = &feature;
184 				max_layer = feature.layer;
185 			}
186 		}
187 	}
188 
189 	if (dragging) {
190 		// continue drag
191 		if (event.LeftIsDown()) {
192 			for (auto sel : sel_features)
193 				sel->UpdateDrag(mouse_pos - drag_start, shift_down);
194 			for (auto sel : sel_features)
195 				UpdateDrag(sel);
196 			Commit();
197 		}
198 		// end drag
199 		else {
200 			dragging = false;
201 
202 			// mouse didn't move, fiddle with selection
203 			if (active_feature && !active_feature->HasMoved()) {
204 				// Don't deselect stuff that was selected in this click's mousedown event
205 				if (!sel_changed) {
206 					if (ctrl_down)
207 						RemoveSelection(active_feature);
208 					else
209 						SetSelection(active_feature, true);
210 				}
211 			}
212 
213 			active_feature = nullptr;
214 			parent->ReleaseMouse();
215 			parent->SetFocus();
216 		}
217 	}
218 	else if (holding) {
219 		if (!event.LeftIsDown()) {
220 			holding = false;
221 
222 			parent->ReleaseMouse();
223 			parent->SetFocus();
224 		}
225 
226 		UpdateHold();
227 		Commit();
228 
229 	}
230 	else if (left_click) {
231 		drag_start = mouse_pos;
232 
233 		// start drag
234 		if (active_feature) {
235 			if (!sel_features.count(active_feature)) {
236 				sel_changed = true;
237 				SetSelection(active_feature, !ctrl_down);
238 			}
239 			else
240 				sel_changed = false;
241 
242 			if (active_feature->line)
243 				c->selectionController->SetActiveLine(active_feature->line);
244 
245 			if (InitializeDrag(active_feature)) {
246 				for (auto sel : sel_features) sel->StartDrag();
247 				dragging = true;
248 				parent->CaptureMouse();
249 			}
250 		}
251 		// start hold
252 		else {
253 			if (!alt_down && features.size() > 1) {
254 				sel_features.clear();
255 				c->selectionController->SetSelectedSet({ c->selectionController->GetActiveLine() });
256 			}
257 			if (active_line && InitializeHold()) {
258 				holding = true;
259 				parent->CaptureMouse();
260 			}
261 		}
262 	}
263 
264 	if (active_line && left_double)
265 		OnDoubleClick();
266 
267 	parent->Render();
268 
269 	// Only coalesce the changes made in a single drag
270 	if (!event.LeftIsDown())
271 		commit_id = -1;
272 }
273 
274 template<class FeatureType>
DrawAllFeatures()275 void VisualTool<FeatureType>::DrawAllFeatures() {
276 	gl.SetLineColour(colour[0], 1.0f, 1);
277 	for (auto& feature : features) {
278 		int fill = 1;
279 		if (&feature == active_feature)
280 			fill = 2;
281 		else if (sel_features.count(&feature))
282 			fill = 3;
283 		gl.SetFillColour(colour[fill], 0.3f);
284 		feature.Draw(gl);
285 	}
286 }
287 
288 template<class FeatureType>
SetSelection(FeatureType * feat,bool clear)289 void VisualTool<FeatureType>::SetSelection(FeatureType *feat, bool clear) {
290 	if (clear)
291 		sel_features.clear();
292 
293 	if (sel_features.insert(feat).second && feat->line) {
294 		Selection sel;
295 		if (!clear)
296 			sel = c->selectionController->GetSelectedSet();
297 		if (sel.insert(feat->line).second)
298 			c->selectionController->SetSelectedSet(std::move(sel));
299 	}
300 }
301 
302 template<class FeatureType>
RemoveSelection(FeatureType * feat)303 void VisualTool<FeatureType>::RemoveSelection(FeatureType *feat) {
304 	if (!sel_features.erase(feat) || !feat->line) return;
305 	for (auto sel : sel_features)
306 		if (sel->line == feat->line) return;
307 
308 	auto sel = c->selectionController->GetSelectedSet();
309 
310 	// Don't deselect the only selected line
311 	if (sel.size() <= 1) return;
312 
313 	sel.erase(feat->line);
314 
315 	// Set the active line to an arbitrary selected line if we just
316 	// deselected the active line
317 	AssDialogue *new_active = c->selectionController->GetActiveLine();
318 	if (feat->line == new_active)
319 		new_active = *sel.begin();
320 
321 	c->selectionController->SetSelectionAndActive(std::move(sel), new_active);
322 }
323 
324 //////// PARSERS
325 
326 typedef const std::vector<AssOverrideParameter> * param_vec;
327 
328 // Find a tag's parameters in a line or return nullptr if it's not found
find_tag(std::vector<std::unique_ptr<AssDialogueBlock>> & blocks,std::string const & tag_name)329 static param_vec find_tag(std::vector<std::unique_ptr<AssDialogueBlock>>& blocks, std::string const& tag_name) {
330 	for (auto ovr : blocks | agi::of_type<AssDialogueBlockOverride>()) {
331 		for (auto const& tag : ovr->Tags) {
332 			if (tag.Name == tag_name)
333 				return &tag.Params;
334 		}
335 	}
336 
337 	return nullptr;
338 }
339 
340 // Get a Vector2D from the given tag parameters, or Vector2D::Bad() if they are not valid
vec_or_bad(param_vec tag,size_t x_idx,size_t y_idx)341 static Vector2D vec_or_bad(param_vec tag, size_t x_idx, size_t y_idx) {
342 	if (!tag ||
343 		tag->size() <= x_idx || tag->size() <= y_idx ||
344 		(*tag)[x_idx].omitted || (*tag)[y_idx].omitted)
345 	{
346 		return Vector2D();
347 	}
348 	return Vector2D((*tag)[x_idx].Get<float>(), (*tag)[y_idx].Get<float>());
349 }
350 
GetLinePosition(AssDialogue * diag)351 Vector2D VisualToolBase::GetLinePosition(AssDialogue *diag) {
352 	auto blocks = diag->ParseTags();
353 
354 	if (Vector2D ret = vec_or_bad(find_tag(blocks, "\\pos"), 0, 1)) return ret;
355 	if (Vector2D ret = vec_or_bad(find_tag(blocks, "\\move"), 0, 1)) return ret;
356 
357 	// Get default position
358 	auto margin = diag->Margin;
359 	int align = 2;
360 
361 	if (AssStyle *style = c->ass->GetStyle(diag->Style)) {
362 		align = style->alignment;
363 		for (int i = 0; i < 3; i++) {
364 			if (margin[i] == 0)
365 				margin[i] = style->Margin[i];
366 		}
367 	}
368 
369 	param_vec align_tag;
370 	int ovr_align = 0;
371 	if ((align_tag = find_tag(blocks, "\\an")))
372 		ovr_align = (*align_tag)[0].Get<int>(ovr_align);
373 	else if ((align_tag = find_tag(blocks, "\\a")))
374 		ovr_align = AssStyle::SsaToAss((*align_tag)[0].Get<int>(2));
375 
376 	if (ovr_align > 0 && ovr_align <= 9)
377 		align = ovr_align;
378 
379 	// Alignment type
380 	int hor = (align - 1) % 3;
381 	int vert = (align - 1) / 3;
382 
383 	// Calculate positions
384 	int x, y;
385 	if (hor == 0)
386 		x = margin[0];
387 	else if (hor == 1)
388 		x = (script_res.X() + margin[0] - margin[1]) / 2;
389 	else
390 		x = script_res.X() - margin[1];
391 
392 	if (vert == 0)
393 		y = script_res.Y() - margin[2];
394 	else if (vert == 1)
395 		y = script_res.Y() / 2;
396 	else
397 		y = margin[2];
398 
399 	return Vector2D(x, y);
400 }
401 
GetLineOrigin(AssDialogue * diag)402 Vector2D VisualToolBase::GetLineOrigin(AssDialogue *diag) {
403 	auto blocks = diag->ParseTags();
404 	return vec_or_bad(find_tag(blocks, "\\org"), 0, 1);
405 }
406 
GetLineMove(AssDialogue * diag,Vector2D & p1,Vector2D & p2,int & t1,int & t2)407 bool VisualToolBase::GetLineMove(AssDialogue *diag, Vector2D &p1, Vector2D &p2, int &t1, int &t2) {
408 	auto blocks = diag->ParseTags();
409 
410 	param_vec tag = find_tag(blocks, "\\move");
411 	if (!tag)
412 		return false;
413 
414 	p1 = vec_or_bad(tag, 0, 1);
415 	p2 = vec_or_bad(tag, 2, 3);
416 	// VSFilter actually defaults to -1, but it uses <= 0 to check for default and 0 seems less bug-prone
417 	t1 = (*tag)[4].Get<int>(0);
418 	t2 = (*tag)[5].Get<int>(0);
419 
420 	return p1 && p2;
421 }
422 
GetLineRotation(AssDialogue * diag,float & rx,float & ry,float & rz)423 void VisualToolBase::GetLineRotation(AssDialogue *diag, float &rx, float &ry, float &rz) {
424 	rx = ry = rz = 0.f;
425 
426 	if (AssStyle *style = c->ass->GetStyle(diag->Style))
427 		rz = style->angle;
428 
429 	auto blocks = diag->ParseTags();
430 
431 	if (param_vec tag = find_tag(blocks, "\\frx"))
432 		rx = tag->front().Get(rx);
433 	if (param_vec tag = find_tag(blocks, "\\fry"))
434 		ry = tag->front().Get(ry);
435 	if (param_vec tag = find_tag(blocks, "\\frz"))
436 		rz = tag->front().Get(rz);
437 	else if ((tag = find_tag(blocks, "\\fr")))
438 		rz = tag->front().Get(rz);
439 }
440 
GetLineShear(AssDialogue * diag,float & fax,float & fay)441 void VisualToolBase::GetLineShear(AssDialogue *diag, float& fax, float& fay) {
442 	fax = fay = 0.f;
443 
444 	auto blocks = diag->ParseTags();
445 
446 	if (param_vec tag = find_tag(blocks, "\\fax"))
447 		fax = tag->front().Get(fax);
448 	if (param_vec tag = find_tag(blocks, "\\fay"))
449 		fay = tag->front().Get(fay);
450 }
451 
GetLineScale(AssDialogue * diag,Vector2D & scale)452 void VisualToolBase::GetLineScale(AssDialogue *diag, Vector2D &scale) {
453 	float x = 100.f, y = 100.f;
454 
455 	if (AssStyle *style = c->ass->GetStyle(diag->Style)) {
456 		x = style->scalex;
457 		y = style->scaley;
458 	}
459 
460 	auto blocks = diag->ParseTags();
461 
462 	if (param_vec tag = find_tag(blocks, "\\fscx"))
463 		x = tag->front().Get(x);
464 	if (param_vec tag = find_tag(blocks, "\\fscy"))
465 		y = tag->front().Get(y);
466 
467 	scale = Vector2D(x, y);
468 }
469 
GetLineClip(AssDialogue * diag,Vector2D & p1,Vector2D & p2,bool & inverse)470 void VisualToolBase::GetLineClip(AssDialogue *diag, Vector2D &p1, Vector2D &p2, bool &inverse) {
471 	inverse = false;
472 
473 	auto blocks = diag->ParseTags();
474 	param_vec tag = find_tag(blocks, "\\iclip");
475 	if (tag)
476 		inverse = true;
477 	else
478 		tag = find_tag(blocks, "\\clip");
479 
480 	if (tag && tag->size() == 4) {
481 		p1 = vec_or_bad(tag, 0, 1);
482 		p2 = vec_or_bad(tag, 2, 3);
483 	}
484 	else {
485 		p1 = Vector2D(0, 0);
486 		p2 = script_res - 1;
487 	}
488 }
489 
GetLineVectorClip(AssDialogue * diag,int & scale,bool & inverse)490 std::string VisualToolBase::GetLineVectorClip(AssDialogue *diag, int &scale, bool &inverse) {
491 	auto blocks = diag->ParseTags();
492 
493 	scale = 1;
494 	inverse = false;
495 
496 	param_vec tag = find_tag(blocks, "\\iclip");
497 	if (tag)
498 		inverse = true;
499 	else
500 		tag = find_tag(blocks, "\\clip");
501 
502 	if (tag && tag->size() == 4) {
503 		return agi::format("m %d %d l %d %d %d %d %d %d"
504 			, (*tag)[0].Get<int>(), (*tag)[1].Get<int>()
505 			, (*tag)[2].Get<int>(), (*tag)[1].Get<int>()
506 			, (*tag)[2].Get<int>(), (*tag)[3].Get<int>()
507 			, (*tag)[0].Get<int>(), (*tag)[3].Get<int>());
508 	}
509 	if (tag) {
510 		scale = std::max((*tag)[0].Get(scale), 1);
511 		return (*tag)[1].Get<std::string>("");
512 	}
513 
514 	return "";
515 }
516 
SetSelectedOverride(std::string const & tag,std::string const & value)517 void VisualToolBase::SetSelectedOverride(std::string const& tag, std::string const& value) {
518 	for (auto line : c->selectionController->GetSelectedSet())
519 		SetOverride(line, tag, value);
520 }
521 
SetOverride(AssDialogue * line,std::string const & tag,std::string const & value)522 void VisualToolBase::SetOverride(AssDialogue* line, std::string const& tag, std::string const& value) {
523 	if (!line) return;
524 
525 	std::string removeTag;
526 	if (tag == "\\1c") removeTag = "\\c";
527 	else if (tag == "\\frz") removeTag = "\\fr";
528 	else if (tag == "\\pos") removeTag = "\\move";
529 	else if (tag == "\\move") removeTag = "\\pos";
530 	else if (tag == "\\clip") removeTag = "\\iclip";
531 	else if (tag == "\\iclip") removeTag = "\\clip";
532 
533 	// Get block at start
534 	auto blocks = line->ParseTags();
535 	AssDialogueBlock *block = blocks.front().get();
536 
537 	if (AssDialogueBlockOverride *ovr = dynamic_cast<AssDialogueBlockOverride*>(block)) {
538 		// Remove old of same
539 		for (size_t i = 0; i < ovr->Tags.size(); i++) {
540 			std::string const& name = ovr->Tags[i].Name;
541 			if (tag == name || removeTag == name) {
542 				ovr->Tags.erase(ovr->Tags.begin() + i);
543 				i--;
544 			}
545 		}
546 		ovr->AddTag(tag + value);
547 
548 		line->UpdateText(blocks);
549 	}
550 	else
551 		line->Text = "{" + tag + value + "}" + line->Text.get();
552 }
553 
554 // If only export worked
555 template class VisualTool<VisualDraggableFeature>;
556 template class VisualTool<ClipCorner>;
557 template class VisualTool<VisualToolDragDraggableFeature>;
558 template class VisualTool<VisualToolVectorClipDraggableFeature>;
559