1 // Copyright (c) 2010, Niels Martin Hansen
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 "audio_marker.h"
33 #include "audio_rendering_style.h"
34 #include "audio_timing.h"
35 #include "command/command.h"
36 #include "include/aegisub/context.h"
37 #include "options.h"
38 #include "pen.h"
39 #include "selection_controller.h"
40 #include "utils.h"
41 
42 #include <libaegisub/ass/time.h>
43 #include <libaegisub/make_unique.h>
44 
45 #include <boost/range/algorithm.hpp>
46 #include <wx/pen.h>
47 
48 namespace {
49 class TimeableLine;
50 
51 /// @class DialogueTimingMarker
52 /// @brief AudioMarker implementation for AudioTimingControllerDialogue
53 ///
54 /// Audio marker intended to live in pairs of two, taking styles depending
55 /// on which marker in the pair is to the left and which is to the right.
56 class DialogueTimingMarker final : public AudioMarker {
57 	/// Current ms position of this marker
58 	int position;
59 
60 	/// Draw style for the marker
61 	const Pen *style;
62 
63 	/// Feet style for the marker
64 	FeetStyle feet;
65 
66 	/// Rendering style of the owning line, needed for sorting
67 	AudioRenderingStyle type;
68 
69 	/// The line which owns this marker
70 	TimeableLine *line;
71 
72 public:
GetPosition() const73 	int       GetPosition() const override { return position; }
GetStyle() const74 	wxPen     GetStyle()    const override { return *style; }
GetFeet() const75 	FeetStyle GetFeet()     const override { return feet; }
76 
77 	/// Move the marker to a new position
78 	/// @param new_position The position to move the marker to, in milliseconds
79 	///
80 	/// This notifies the owning line of the change, so that it can ensure that
81 	/// this marker has the appropriate rendering style.
82 	void SetPosition(int new_position);
83 
84 	/// Constructor
85 	/// @param position Initial position of this marker
86 	/// @param style Rendering style of this marker
87 	/// @param feet Foot style of this marker
88 	/// @param type Type of this marker, used only for sorting
89 	/// @param line Line which this is a marker for
DialogueTimingMarker(int position,const Pen * style,FeetStyle feet,AudioRenderingStyle type,TimeableLine * line)90 	DialogueTimingMarker(int position, const Pen *style, FeetStyle feet, AudioRenderingStyle type, TimeableLine *line)
91 	: position(position)
92 	, style(style)
93 	, feet(feet)
94 	, type(type)
95 	, line(line)
96 	{
97 	}
98 
DialogueTimingMarker(DialogueTimingMarker const & other,TimeableLine * line)99 	DialogueTimingMarker(DialogueTimingMarker const& other, TimeableLine *line)
100 	: position(other.position)
101 	, style(other.style)
102 	, feet(other.feet)
103 	, type(other.type)
104 	, line(line)
105 	{
106 	}
107 
108 	/// Get the line which this is a marker for
GetLine() const109 	TimeableLine *GetLine() const { return line; }
110 
111 	/// Implicit decay to the position of the marker
operator int() const112 	operator int() const { return position; }
113 
114 	/// Comparison operator
115 	///
116 	/// Compares first on position, then on audio rendering style so that the
117 	/// markers for the active line end up after those for the inactive lines.
operator <(DialogueTimingMarker const & other) const118 	bool operator<(DialogueTimingMarker const& other) const
119 	{
120 		if (position < other.position) return true;
121 		if (position > other.position) return false;
122 		return type < other.type;
123 	}
124 
125 	/// Swap the rendering style of this marker with that of the passed marker
SwapStyles(DialogueTimingMarker & other)126 	void SwapStyles(DialogueTimingMarker &other)
127 	{
128 		std::swap(style, other.style);
129 		std::swap(feet, other.feet);
130 	}
131 };
132 
133 /// A comparison predicate for pointers to dialogue markers and millisecond positions
134 struct marker_ptr_cmp
135 {
operator ()__anon0a76d66c0111::marker_ptr_cmp136 	bool operator()(const DialogueTimingMarker *lft, const DialogueTimingMarker *rgt) const
137 	{
138 		return *lft < *rgt;
139 	}
140 
operator ()__anon0a76d66c0111::marker_ptr_cmp141 	bool operator()(const DialogueTimingMarker *lft, int rgt) const
142 	{
143 		return *lft < rgt;
144 	}
145 
operator ()__anon0a76d66c0111::marker_ptr_cmp146 	bool operator()(int lft, const DialogueTimingMarker *rgt) const
147 	{
148 		return lft < *rgt;
149 	}
150 };
151 
152 /// @class TimeableLine
153 /// @brief A single dialogue line which can be timed via AudioTimingControllerDialogue
154 ///
155 /// This class provides markers and styling ranges for a single dialogue line,
156 /// both active and inactive. In addition, it can apply changes made via those
157 /// markers to the tracked dialogue line.
158 class TimeableLine {
159 	/// The current tracked dialogue line
160 	AssDialogue *line = nullptr;
161 	/// The rendering style of this line
162 	AudioRenderingStyle style;
163 
164 	/// One of the markers. Initially the left marker, but the user may change this.
165 	DialogueTimingMarker marker1;
166 	/// One of the markers. Initially the right marker, but the user may change this.
167 	DialogueTimingMarker marker2;
168 
169 	/// Pointer to whichever marker happens to be on the left
170 	DialogueTimingMarker *left_marker;
171 	/// Pointer to whichever marker happens to be on the right
172 	DialogueTimingMarker *right_marker;
173 
174 public:
175 	/// Constructor
176 	/// @param style Rendering style to use for this line's time range
177 	/// @param style_left The rendering style for the start marker
178 	/// @param style_right The rendering style for the end marker
TimeableLine(AudioRenderingStyle style,const Pen * style_left,const Pen * style_right)179 	TimeableLine(AudioRenderingStyle style, const Pen *style_left, const Pen *style_right)
180 	: style(style)
181 	, marker1(0, style_left, AudioMarker::Feet_Right, style, this)
182 	, marker2(0, style_right, AudioMarker::Feet_Left, style, this)
183 	, left_marker(&marker1)
184 	, right_marker(&marker2)
185 	{
186 	}
187 
188 	/// Explicit copy constructor needed due to that the markers have a pointer to this
TimeableLine(TimeableLine const & other)189 	TimeableLine(TimeableLine const& other)
190 	: line(other.line)
191 	, style(other.style)
192 	, marker1(*other.left_marker, this)
193 	, marker2(*other.right_marker, this)
194 	, left_marker(&marker1)
195 	, right_marker(&marker2)
196 	{
197 	}
198 
199 	/// Get the tracked dialogue line
GetLine() const200 	AssDialogue *GetLine() const { return line; }
201 
202 	/// Get the time range for this line
operator TimeRange() const203 	operator TimeRange() const { return TimeRange(*left_marker, *right_marker); }
204 
205 	/// Add this line's style to the style ranges
GetStyleRange(AudioRenderingStyleRanges * ranges) const206 	void GetStyleRange(AudioRenderingStyleRanges *ranges) const
207 	{
208 		ranges->AddRange(*left_marker, *right_marker, style);
209 	}
210 
211 	/// Get this line's markers
212 	/// @param c Vector to add the markers to
213 	template<typename Container>
GetMarkers(Container * c) const214 	void GetMarkers(Container *c) const
215 	{
216 		c->push_back(left_marker);
217 		c->push_back(right_marker);
218 	}
219 
220 	/// Get the leftmost of the markers
GetLeftMarker()221 	DialogueTimingMarker *GetLeftMarker() { return left_marker; }
GetLeftMarker() const222 	const DialogueTimingMarker *GetLeftMarker() const { return left_marker; }
223 
224 	/// Get the rightmost of the markers
GetRightMarker()225 	DialogueTimingMarker *GetRightMarker() { return right_marker; }
GetRightMarker() const226 	const DialogueTimingMarker *GetRightMarker() const { return right_marker; }
227 
228 	/// Does this line have a marker in the given range?
ContainsMarker(TimeRange const & range) const229 	bool ContainsMarker(TimeRange const& range) const
230 	{
231 		return range.contains(marker1) || range.contains(marker2);
232 	}
233 
234 	/// Check if the markers have the correct styles, and correct them if needed
CheckMarkers()235 	void CheckMarkers()
236 	{
237 		if (*right_marker < *left_marker)
238 		{
239 			marker1.SwapStyles(marker2);
240 			std::swap(left_marker, right_marker);
241 		}
242 	}
243 
244 	/// Apply any changes made here to the tracked dialogue line
Apply()245 	void Apply()
246 	{
247 		if (line)
248 		{
249 			line->Start = left_marker->GetPosition();
250 			line->End = right_marker->GetPosition();
251 		}
252 	}
253 
254 	/// Set the dialogue line which this is tracking and reset the markers to
255 	/// the line's time range
256 	/// @return Were the markers actually set to the line's time?
SetLine(AssDialogue * new_line)257 	bool SetLine(AssDialogue *new_line)
258 	{
259 		if (!line || new_line->End > 0)
260 		{
261 			line = new_line;
262 			marker1.SetPosition(new_line->Start);
263 			marker2.SetPosition(new_line->End);
264 			return true;
265 		}
266 		else
267 		{
268 			line = new_line;
269 			return false;
270 		}
271 	}
272 };
273 
SetPosition(int new_position)274 void DialogueTimingMarker::SetPosition(int new_position) {
275 	position = new_position;
276 	line->CheckMarkers();
277 }
278 
279 /// @class AudioTimingControllerDialogue
280 /// @brief Default timing mode for dialogue subtitles
281 ///
282 /// Displays a start and end marker for an active subtitle line, and possibly
283 /// some of the inactive lines. The markers for the active line can be dragged,
284 /// updating the audio selection and the start/end time of that line. In
285 /// addition, any markers for inactive lines that start/end at the same time
286 /// as the active line starts/ends can optionally be dragged along with the
287 /// active line's markers, updating those lines as well.
288 class AudioTimingControllerDialogue final : public AudioTimingController {
289 	/// The rendering style for the active line's start marker
290 	Pen style_left{"Colour/Audio Display/Line boundary Start", "Audio/Line Boundaries Thickness"};
291 	/// The rendering style for the active line's end marker
292 	Pen style_right{"Colour/Audio Display/Line boundary End", "Audio/Line Boundaries Thickness"};
293 	/// The rendering style for the start and end markers of inactive lines
294 	Pen style_inactive{"Colour/Audio Display/Line Boundary Inactive Line", "Audio/Line Boundaries Thickness"};
295 
296 	/// The currently active line
297 	TimeableLine active_line;
298 
299 	/// Inactive lines which are currently modifiable
300 	std::list<TimeableLine> inactive_lines;
301 
302 	/// Selected lines which are currently modifiable
303 	std::list<TimeableLine> selected_lines;
304 
305 	/// All audio markers for active and inactive lines, sorted by position
306 	std::vector<DialogueTimingMarker*> markers;
307 
308 	/// Marker provider for video keyframes
309 	AudioMarkerProviderKeyframes keyframes_provider;
310 
311 	/// Marker provider for video playback position
312 	VideoPositionMarkerProvider video_position_provider;
313 
314 	/// Marker provider for seconds lines
315 	SecondsMarkerProvider seconds_provider;
316 
317 	/// The set of lines which have been modified and need to have their
318 	/// changes applied on commit
319 	std::set<TimeableLine*> modified_lines;
320 
321 	/// Commit id for coalescing purposes when in auto commit mode
322 	int commit_id =-1;
323 
324 	/// The owning project context
325 	agi::Context *context;
326 
327 	/// The time which was clicked on for alt-dragging mode
328 	int clicked_ms;
329 
330 	/// Autocommit option
331 	const agi::OptionValue *auto_commit = OPT_GET("Audio/Auto/Commit");
332 	const agi::OptionValue *inactive_line_mode = OPT_GET("Audio/Inactive Lines Display Mode");
333 	const agi::OptionValue *inactive_line_comments = OPT_GET("Audio/Display/Draw/Inactive Comments");
334 	const agi::OptionValue *drag_timing = OPT_GET("Audio/Drag Timing");
335 
336 	agi::signal::Connection commit_connection;
337 	agi::signal::Connection audio_open_connection;
338 	agi::signal::Connection inactive_line_mode_connection;
339 	agi::signal::Connection inactive_line_comment_connection;
340 	agi::signal::Connection active_line_connection;
341 	agi::signal::Connection selection_connection;
342 
343 	/// Update the audio controller's selection
344 	void UpdateSelection();
345 
346 	/// Regenerate the list of timeable inactive lines
347 	void RegenerateInactiveLines();
348 
349 	/// Regenerate the list of timeable selected lines
350 	void RegenerateSelectedLines();
351 
352 	/// Add a line to the list of timeable inactive lines
353 	void AddInactiveLine(Selection const& sel, AssDialogue *diag);
354 
355 	/// Regenerate the list of active and inactive line markers
356 	void RegenerateMarkers();
357 
358 	/// Get the start markers for the active line and all selected lines
359 	std::vector<AudioMarker*> GetLeftMarkers();
360 
361 	/// Get the end markers for the active line and all selected lines
362 	std::vector<AudioMarker*> GetRightMarkers();
363 
364 	/// @brief Set the position of markers and announce the change to the world
365 	/// @param upd_markers Markers to move
366 	/// @param ms New position of the markers
367 	void SetMarkers(std::vector<AudioMarker*> const& upd_markers, int ms, int snap_range);
368 
369 	/// Try to snap all of the active markers to any inactive markers
370 	/// @param snap_range Maximum distance to snap in milliseconds
371 	/// @param active     Markers which should be snapped
372 	/// @return The distance the markers were shifted by
373 	int SnapMarkers(int snap_range, std::vector<AudioMarker*> const& markers) const;
374 
375 	/// Commit all pending changes to the file
376 	/// @param user_triggered Is this a user-initiated commit or an autocommit
377 	void DoCommit(bool user_triggered);
378 
379 	void OnSelectedSetChanged();
380 
381 	// AssFile events
382 	void OnFileChanged(int type);
383 
384 public:
385 	// AudioMarkerProvider interface
386 	void GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const override;
387 
388 	// AudioTimingController interface
389 	void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const override;
GetLabels(TimeRange const & range,std::vector<AudioLabel> & out) const390 	void GetLabels(TimeRange const& range, std::vector<AudioLabel> &out) const override { }
391 	void Next(NextMode mode) override;
392 	void Prev() override;
393 	void Revert() override;
394 	void AddLeadIn() override;
395 	void AddLeadOut() override;
396 	void ModifyLength(int delta, bool shift_following) override;
397 	void ModifyStart(int delta) override;
398 	bool IsNearbyMarker(int ms, int sensitivity, bool alt_down) const override;
399 	std::vector<AudioMarker*> OnLeftClick(int ms, bool ctrl_down, bool alt_down, int sensitivity, int snap_range) override;
400 	std::vector<AudioMarker*> OnRightClick(int ms, bool, int sensitivity, int snap_range) override;
401 	void OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range) override;
402 
403 	// We have no warning messages currently, maybe add the old "Modified" message back later?
GetWarningMessage() const404 	wxString GetWarningMessage() const override { return wxString(); }
GetIdealVisibleTimeRange() const405 	TimeRange GetIdealVisibleTimeRange() const override { return active_line; }
GetPrimaryPlaybackRange() const406 	TimeRange GetPrimaryPlaybackRange() const override { return active_line; }
GetActiveLineRange() const407 	TimeRange GetActiveLineRange() const override { return active_line; }
Commit()408 	void Commit() override { DoCommit(true); }
409 
410 	/// Constructor
411 	/// @param c Project context
412 	AudioTimingControllerDialogue(agi::Context *c);
413 };
414 
AudioTimingControllerDialogue(agi::Context * c)415 AudioTimingControllerDialogue::AudioTimingControllerDialogue(agi::Context *c)
416 : active_line(AudioStyle_Primary, &style_left, &style_right)
417 , keyframes_provider(c, "Audio/Display/Draw/Keyframes in Dialogue Mode")
418 , video_position_provider(c)
419 , context(c)
420 , commit_connection(c->ass->AddCommitListener(&AudioTimingControllerDialogue::OnFileChanged, this))
421 , inactive_line_mode_connection(OPT_SUB("Audio/Inactive Lines Display Mode", &AudioTimingControllerDialogue::RegenerateInactiveLines, this))
422 , inactive_line_comment_connection(OPT_SUB("Audio/Display/Draw/Inactive Comments", &AudioTimingControllerDialogue::RegenerateInactiveLines, this))
423 , active_line_connection(c->selectionController->AddActiveLineListener(&AudioTimingControllerDialogue::Revert, this))
424 , selection_connection(c->selectionController->AddSelectionListener(&AudioTimingControllerDialogue::OnSelectedSetChanged, this))
425 {
426 	keyframes_provider.AddMarkerMovedListener([=]{ AnnounceMarkerMoved(); });
427 	video_position_provider.AddMarkerMovedListener([=]{ AnnounceMarkerMoved(); });
428 	seconds_provider.AddMarkerMovedListener([=]{ AnnounceMarkerMoved(); });
429 
430 	Revert();
431 }
432 
GetMarkers(const TimeRange & range,AudioMarkerVector & out_markers) const433 void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const
434 {
435 	// The order matters here; later markers are painted on top of earlier
436 	// markers, so the markers that we want to end up on top need to appear last
437 
438 	seconds_provider.GetMarkers(range, out_markers);
439 
440 	// Copy inactive line markers in the range
441 	copy(
442 		boost::lower_bound(markers, range.begin(), marker_ptr_cmp()),
443 		boost::upper_bound(markers, range.end(), marker_ptr_cmp()),
444 		back_inserter(out_markers));
445 
446 	keyframes_provider.GetMarkers(range, out_markers);
447 	video_position_provider.GetMarkers(range, out_markers);
448 }
449 
OnSelectedSetChanged()450 void AudioTimingControllerDialogue::OnSelectedSetChanged()
451 {
452 	RegenerateSelectedLines();
453 	RegenerateInactiveLines();
454 }
455 
OnFileChanged(int type)456 void AudioTimingControllerDialogue::OnFileChanged(int type) {
457 	if (type & AssFile::COMMIT_DIAG_TIME)
458 		Revert();
459 	else if (type & AssFile::COMMIT_DIAG_ADDREM)
460 		RegenerateInactiveLines();
461 }
462 
GetRenderingStyles(AudioRenderingStyleRanges & ranges) const463 void AudioTimingControllerDialogue::GetRenderingStyles(AudioRenderingStyleRanges &ranges) const
464 {
465 	active_line.GetStyleRange(&ranges);
466 	for (auto const& line : selected_lines)
467 		line.GetStyleRange(&ranges);
468 	for (auto const& line : inactive_lines)
469 		line.GetStyleRange(&ranges);
470 }
471 
Next(NextMode mode)472 void AudioTimingControllerDialogue::Next(NextMode mode)
473 {
474 	if (mode == TIMING_UNIT)
475 	{
476 		context->selectionController->NextLine();
477 		return;
478 	}
479 
480 	int new_end_ms = *active_line.GetRightMarker();
481 
482 	cmd::call("grid/line/next/create", context);
483 
484 	if (mode == LINE_RESET_DEFAULT || active_line.GetLine()->End == 0) {
485 		const int default_duration = OPT_GET("Timing/Default Duration")->GetInt();
486 		// Setting right first here so that they don't get switched and the
487 		// same marker gets set twice
488 		active_line.GetRightMarker()->SetPosition(new_end_ms + default_duration);
489 		active_line.GetLeftMarker()->SetPosition(new_end_ms);
490 		boost::sort(markers, marker_ptr_cmp());
491 		modified_lines.insert(&active_line);
492 		UpdateSelection();
493 	}
494 }
495 
Prev()496 void AudioTimingControllerDialogue::Prev()
497 {
498 	context->selectionController->PrevLine();
499 }
500 
DoCommit(bool user_triggered)501 void AudioTimingControllerDialogue::DoCommit(bool user_triggered)
502 {
503 	// Store back new times
504 	if (modified_lines.size())
505 	{
506 		for (auto line : modified_lines)
507 			line->Apply();
508 
509 		commit_connection.Block();
510 		if (user_triggered)
511 		{
512 			context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME);
513 			commit_id = -1; // never coalesce with a manually triggered commit
514 		}
515 		else
516 		{
517 			AssDialogue *amend = modified_lines.size() == 1 ? (*modified_lines.begin())->GetLine() : nullptr;
518 			commit_id = context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME, commit_id, amend);
519 		}
520 
521 		commit_connection.Unblock();
522 		modified_lines.clear();
523 	}
524 }
525 
Revert()526 void AudioTimingControllerDialogue::Revert()
527 {
528 	commit_id = -1;
529 
530 	if (AssDialogue *line = context->selectionController->GetActiveLine())
531 	{
532 		modified_lines.clear();
533 		if (active_line.SetLine(line))
534 		{
535 			AnnounceUpdatedPrimaryRange();
536 			if (inactive_line_mode->GetInt() == 0)
537 				AnnounceUpdatedStyleRanges();
538 		}
539 		else
540 		{
541 			modified_lines.insert(&active_line);
542 		}
543 	}
544 
545 	RegenerateInactiveLines();
546 	RegenerateSelectedLines();
547 }
548 
AddLeadIn()549 void AudioTimingControllerDialogue::AddLeadIn()
550 {
551 	DialogueTimingMarker *m = active_line.GetLeftMarker();
552 	SetMarkers({ m }, *m - OPT_GET("Audio/Lead/IN")->GetInt(), 0);
553 }
554 
AddLeadOut()555 void AudioTimingControllerDialogue::AddLeadOut()
556 {
557 	DialogueTimingMarker *m = active_line.GetRightMarker();
558 	SetMarkers({ m }, *m + OPT_GET("Audio/Lead/OUT")->GetInt(), 0);
559 }
560 
ModifyLength(int delta,bool)561 void AudioTimingControllerDialogue::ModifyLength(int delta, bool) {
562 	DialogueTimingMarker *m = active_line.GetRightMarker();
563 	SetMarkers({ m },
564 		std::max<int>(*m + delta * 10, *active_line.GetLeftMarker()), 0);
565 }
566 
ModifyStart(int delta)567 void AudioTimingControllerDialogue::ModifyStart(int delta) {
568 	DialogueTimingMarker *m = active_line.GetLeftMarker();
569 	SetMarkers({ m },
570 		std::min<int>(*m + delta * 10, *active_line.GetRightMarker()), 0);
571 }
572 
IsNearbyMarker(int ms,int sensitivity,bool alt_down) const573 bool AudioTimingControllerDialogue::IsNearbyMarker(int ms, int sensitivity, bool alt_down) const
574 {
575 	assert(sensitivity >= 0);
576 	return alt_down || active_line.ContainsMarker(TimeRange(ms-sensitivity, ms+sensitivity));
577 }
578 
OnLeftClick(int ms,bool ctrl_down,bool alt_down,int sensitivity,int snap_range)579 std::vector<AudioMarker*> AudioTimingControllerDialogue::OnLeftClick(int ms, bool ctrl_down, bool alt_down, int sensitivity, int snap_range)
580 {
581 	assert(sensitivity >= 0);
582 	assert(snap_range >= 0);
583 
584 	std::vector<AudioMarker*> ret;
585 
586 	clicked_ms = INT_MIN;
587 	if (alt_down)
588 	{
589 		clicked_ms = ms;
590 		active_line.GetMarkers(&ret);
591 		for (auto const& line : selected_lines)
592 			line.GetMarkers(&ret);
593 		return ret;
594 	}
595 
596 	DialogueTimingMarker *left = active_line.GetLeftMarker();
597 	DialogueTimingMarker *right = active_line.GetRightMarker();
598 
599 	int dist_l = tabs(*left - ms);
600 	int dist_r = tabs(*right - ms);
601 
602 	if (dist_l > sensitivity && dist_r > sensitivity)
603 	{
604 		// Clicked far from either marker:
605 		// Insta-set the left marker to the clicked position and return the
606 		// right as the dragged one, such that if the user does start dragging,
607 		// he will create a new selection from scratch
608 		std::vector<AudioMarker*> jump = GetLeftMarkers();
609 		ret = drag_timing->GetBool() ? GetRightMarkers() : jump;
610 		// Get ret before setting as setting may swap left/right
611 		SetMarkers(jump, ms, snap_range);
612 		return ret;
613 	}
614 
615 	DialogueTimingMarker *clicked = dist_l <= dist_r ? left : right;
616 
617 	if (ctrl_down)
618 	{
619 		// The use of GetPosition here is important, as otherwise it'll start
620 		// after lines ending at the same time as the active line begins
621 		auto it = boost::lower_bound(markers, clicked->GetPosition(), marker_ptr_cmp());
622 		for (; it != markers.end() && !(*clicked < **it); ++it)
623 			ret.push_back(*it);
624 	}
625 	else
626 		ret.push_back(clicked);
627 
628 	// Left-click within drag range should still move the left marker to the
629 	// clicked position, but not the right marker
630 	if (clicked == left)
631 		SetMarkers(ret, ms, snap_range);
632 
633 	return ret;
634 }
635 
OnRightClick(int ms,bool,int sensitivity,int snap_range)636 std::vector<AudioMarker*> AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range)
637 {
638 	clicked_ms = INT_MIN;
639 	std::vector<AudioMarker*> ret = GetRightMarkers();
640 	SetMarkers(ret, ms, snap_range);
641 	return ret;
642 }
643 
OnMarkerDrag(std::vector<AudioMarker * > const & markers,int new_position,int snap_range)644 void AudioTimingControllerDialogue::OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range)
645 {
646 	SetMarkers(markers, new_position, snap_range);
647 }
648 
UpdateSelection()649 void AudioTimingControllerDialogue::UpdateSelection()
650 {
651 	AnnounceUpdatedPrimaryRange();
652 	AnnounceUpdatedStyleRanges();
653 }
654 
SetMarkers(std::vector<AudioMarker * > const & upd_markers,int ms,int snap_range)655 void AudioTimingControllerDialogue::SetMarkers(std::vector<AudioMarker*> const& upd_markers, int ms, int snap_range)
656 {
657 	if (upd_markers.empty()) return;
658 
659 	int shift = clicked_ms != INT_MIN ? ms - clicked_ms : 0;
660 	if (shift) clicked_ms = ms;
661 
662 	// Since we're moving markers, the sorted list of markers will need to be
663 	// resorted. To avoid resorting the entire thing, find the subrange that
664 	// is effected.
665 	int min_ms = ms;
666 	int max_ms = ms;
667 	for (AudioMarker *upd_marker : upd_markers)
668 	{
669 		auto marker = static_cast<DialogueTimingMarker*>(upd_marker);
670 		if (shift < 0) {
671 			min_ms = std::min<int>(*marker + shift, min_ms);
672 			max_ms = std::max<int>(*marker, max_ms);
673 		}
674 		else {
675 			min_ms = std::min<int>(*marker, min_ms);
676 			max_ms = std::max<int>(*marker + shift, max_ms);
677 		}
678 	}
679 
680 	auto begin = boost::lower_bound(markers, min_ms, marker_ptr_cmp());
681 	auto end = upper_bound(begin, markers.end(), max_ms, marker_ptr_cmp());
682 
683 	// Update the markers
684 	for (auto upd_marker : upd_markers)
685 	{
686 		auto marker = static_cast<DialogueTimingMarker*>(upd_marker);
687 		marker->SetPosition(clicked_ms != INT_MIN ? *marker + shift : ms);
688 		modified_lines.insert(marker->GetLine());
689 	}
690 
691 	int snap = SnapMarkers(snap_range, upd_markers);
692 	if (clicked_ms != INT_MIN)
693 		clicked_ms += snap;
694 
695 	// Resort the range
696 	sort(begin, end, marker_ptr_cmp());
697 
698 	if (auto_commit->GetBool()) DoCommit(false);
699 	UpdateSelection();
700 
701 	AnnounceMarkerMoved();
702 }
703 
RegenerateInactiveLines()704 void AudioTimingControllerDialogue::RegenerateInactiveLines()
705 {
706 	using pred = bool(*)(AssDialogue const&);
707 	auto predicate = inactive_line_comments->GetBool()
708 		? static_cast<pred>([](AssDialogue const&) { return true; })
709 		: static_cast<pred>([](AssDialogue const& d) { return !d.Comment; });
710 
711 	bool was_empty = inactive_lines.empty();
712 	inactive_lines.clear();
713 
714 	auto const& sel = context->selectionController->GetSelectedSet();
715 
716 	switch (int mode = inactive_line_mode->GetInt())
717 	{
718 	case 1: // Previous line only
719 	case 2: // Previous and next lines
720 		if (AssDialogue *line = context->selectionController->GetActiveLine())
721 		{
722 			auto current_line = context->ass->iterator_to(*line);
723 			if (current_line == context->ass->Events.end())
724 				break;
725 
726 			if (current_line != context->ass->Events.begin())
727 			{
728 				auto prev = current_line;
729 				while (--prev != context->ass->Events.begin() && !predicate(*prev)) ;
730 				if (predicate(*prev))
731 					AddInactiveLine(sel, &*prev);
732 			}
733 
734 			if (mode == 2)
735 			{
736 				auto next = std::find_if(++current_line, context->ass->Events.end(), predicate);
737 				if (next != context->ass->Events.end())
738 					AddInactiveLine(sel, &*next);
739 			}
740 		}
741 		break;
742 	case 3: // All inactive lines
743 	{
744 		AssDialogue *active_line = context->selectionController->GetActiveLine();
745 		for (auto& line : context->ass->Events)
746 		{
747 			if (&line != active_line && predicate(line))
748 				AddInactiveLine(sel, &line);
749 		}
750 		break;
751 	}
752 	default:
753 		if (was_empty)
754 		{
755 			RegenerateMarkers();
756 			return;
757 		}
758 	}
759 
760 	AnnounceUpdatedStyleRanges();
761 
762 	RegenerateMarkers();
763 }
764 
AddInactiveLine(Selection const & sel,AssDialogue * diag)765 void AudioTimingControllerDialogue::AddInactiveLine(Selection const& sel, AssDialogue *diag)
766 {
767 	if (sel.count(diag)) return;
768 
769 	inactive_lines.emplace_back(AudioStyle_Inactive, &style_inactive, &style_inactive);
770 	inactive_lines.back().SetLine(diag);
771 }
772 
RegenerateSelectedLines()773 void AudioTimingControllerDialogue::RegenerateSelectedLines()
774 {
775 	bool was_empty = selected_lines.empty();
776 	selected_lines.clear();
777 
778 	AssDialogue *active = context->selectionController->GetActiveLine();
779 	for (auto line : context->selectionController->GetSelectedSet())
780 	{
781 		if (line == active) continue;
782 
783 		selected_lines.emplace_back(AudioStyle_Selected, &style_inactive, &style_inactive);
784 		selected_lines.back().SetLine(line);
785 	}
786 
787 	if (!selected_lines.empty() || !was_empty)
788 	{
789 		AnnounceUpdatedStyleRanges();
790 		RegenerateMarkers();
791 	}
792 }
793 
RegenerateMarkers()794 void AudioTimingControllerDialogue::RegenerateMarkers()
795 {
796 	markers.clear();
797 
798 	active_line.GetMarkers(&markers);
799 	for (auto const& line : selected_lines)
800 		line.GetMarkers(&markers);
801 	for (auto const& line : inactive_lines)
802 		line.GetMarkers(&markers);
803 	boost::sort(markers, marker_ptr_cmp());
804 
805 	AnnounceMarkerMoved();
806 }
807 
GetLeftMarkers()808 std::vector<AudioMarker*> AudioTimingControllerDialogue::GetLeftMarkers()
809 {
810 	std::vector<AudioMarker*> ret;
811 	ret.reserve(selected_lines.size() + 1);
812 	ret.push_back(active_line.GetLeftMarker());
813 	for (auto& line : selected_lines)
814 		ret.push_back(line.GetLeftMarker());
815 	return ret;
816 }
817 
GetRightMarkers()818 std::vector<AudioMarker*> AudioTimingControllerDialogue::GetRightMarkers()
819 {
820 	std::vector<AudioMarker*> ret;
821 	ret.reserve(selected_lines.size() + 1);
822 	ret.push_back(active_line.GetRightMarker());
823 	for (auto& line : selected_lines)
824 		ret.push_back(line.GetRightMarker());
825 	return ret;
826 }
827 
SnapMarkers(int snap_range,std::vector<AudioMarker * > const & active) const828 int AudioTimingControllerDialogue::SnapMarkers(int snap_range, std::vector<AudioMarker*> const& active) const
829 {
830 	if (snap_range <= 0 || active.empty()) return 0;
831 
832 	auto marker_range = [&] {
833 		int front = active.front()->GetPosition();
834 		int min = front;
835 		int max = front;
836 		for (auto m : active)
837 		{
838 			auto pos = m->GetPosition();
839 			if (pos < min) min = pos;
840 			if (pos > max) max = pos;
841 		}
842 		return TimeRange{min - snap_range, max + snap_range};
843 	}();
844 
845 	std::vector<int> inactive_markers;
846 	inactive_markers.reserve(inactive_lines.size() * 2 + selected_lines.size() * 2 + 2 - active.size());
847 
848 	auto add_inactive = [&](const DialogueTimingMarker *m, bool check)
849 	{
850 		if (!marker_range.contains(*m)) return;
851 		if (!inactive_markers.empty() && inactive_markers.back() == *m) return;
852 		if (check && boost::find(active, m) != end(active)) return;
853 		inactive_markers.push_back(*m);
854 	};
855 
856 	for (auto const& line : inactive_lines)
857 	{
858 		add_inactive(line.GetLeftMarker(), false);
859 		add_inactive(line.GetRightMarker(), false);
860 	}
861 
862 	if (active.size() != selected_lines.size() * 2 + 2)
863 	{
864 		for (auto const& line : selected_lines)
865 		{
866 			add_inactive(line.GetLeftMarker(), true);
867 			add_inactive(line.GetRightMarker(), true);
868 		}
869 		add_inactive(active_line.GetLeftMarker(), true);
870 		add_inactive(active_line.GetRightMarker(), true);
871 	}
872 
873 	int snap_distance = 0;
874 	bool has_snapped = false;
875 	auto check = [&](int marker, int pos)
876 	{
877 		auto dist = marker - pos;
878 		if (!has_snapped)
879 			snap_distance = dist;
880 		else if (tabs(dist) < tabs(snap_distance))
881 			snap_distance = dist;
882 		has_snapped = true;
883 	};
884 
885 	int prev = -1;
886 	AudioMarkerVector snap_markers;
887 	for (const auto active_marker : active)
888 	{
889 		auto pos = active_marker->GetPosition();
890 		if (pos == prev) continue;
891 
892 		snap_markers.clear();
893 		TimeRange range(pos - snap_range, pos + snap_range);
894 		keyframes_provider.GetMarkers(range, snap_markers);
895 		video_position_provider.GetMarkers(range, snap_markers);
896 
897 		for (const auto marker : snap_markers)
898 		{
899 			check(marker->GetPosition(), pos);
900 			if (snap_distance == 0) return 0;
901 		}
902 
903 		for (auto it = boost::lower_bound(inactive_markers, range.begin()); it != end(inactive_markers); ++it)
904 		{
905 			check(*it, pos);
906 			if (snap_distance == 0) return 0;
907 			if (*it > pos) break;
908 		}
909 	}
910 
911 	if (!has_snapped || tabs(snap_distance) > snap_range)
912 		return 0;
913 
914 	for (auto m : active)
915 		static_cast<DialogueTimingMarker *>(m)->SetPosition(m->GetPosition() + snap_distance);
916 	return snap_distance;
917 }
918 
919 } // namespace {
920 
CreateDialogueTimingController(agi::Context * c)921 std::unique_ptr<AudioTimingController> CreateDialogueTimingController(agi::Context *c)
922 {
923 	return agi::make_unique<AudioTimingControllerDialogue>(c);
924 }
925