Support dragging multiple markers at once in the dialogue timing controller

When ctrl is held down and the user clicks on one of the active line's
markers, all markers at the same position as the clicked marker (for
both active and inactive lines) are now moved along with the clicked
marker.

Closes #20.

Originally committed to SVN as r6461.
This commit is contained in:
Thomas Goyne 2012-02-10 00:04:24 +00:00
parent 32c8cc0974
commit 47cf5c8629

View file

@ -50,109 +50,252 @@
#include "selection_controller.h" #include "selection_controller.h"
#include "utils.h" #include "utils.h"
/// @class AudioMarkerDialogueTiming class TimeableLine;
/// @class DialogueTimingMarker
/// @brief AudioMarker implementation for AudioTimingControllerDialogue /// @brief AudioMarker implementation for AudioTimingControllerDialogue
/// ///
/// Audio marker intended to live in pairs of two, taking styles depending /// Audio marker intended to live in pairs of two, taking styles depending
/// on which marker in the pair is to the left and which is to the right. /// on which marker in the pair is to the left and which is to the right.
class AudioMarkerDialogueTiming : public AudioMarker { class DialogueTimingMarker : public AudioMarker {
/// The other marker for the dialogue line's pair
AudioMarkerDialogueTiming *other;
/// Current ms position of this marker /// Current ms position of this marker
int position; int position;
/// Draw style for the marker /// Draw style for the marker
wxPen style; const Pen *style;
/// Foot style for the marker
/// Feet style for the marker
FeetStyle feet; FeetStyle feet;
/// Draw style for the left marker /// Rendering style of the owning line, needed for sorting
Pen style_left; AudioRenderingStyle type;
/// Draw style for the right marker
Pen style_right;
/// The line which owns this marker
public: TimeableLine *line;
// AudioMarker interface
int GetPosition() const { return position; }
wxPen GetStyle() const { return style; }
FeetStyle GetFeet() const { return feet; }
bool CanSnap() const { return false; }
public:
// Specific interface
/// @brief Move the marker to a new position
/// @param new_position The position to move the marker to, in milliseconds
///
/// If the marker moves to the opposite side of the other marker in the pair,
/// the styles of the two markers will be changed to match the new start/end
/// relationship of them.
void SetPosition(int new_position);
/// @brief Constructor
///
/// Initialises the fields to default values.
AudioMarkerDialogueTiming();
/// @brief Initialise a pair of dialogue markers to be a pair
/// @param marker1 The first marker in the pair to make
/// @param marker2 The second marker in the pair to make
///
/// This checks that the markers aren't already part of a pair, and then
/// sets their "other" field. Positions and styles aren't affected.
static void InitPair(AudioMarkerDialogueTiming *marker1, AudioMarkerDialogueTiming *marker2);
/// Implicit decay to the position of the marker
operator int() const { return position; }
};
/// @class InactiveLineMarker
/// @brief Markers for the beginning and ends of inactive lines
class InactiveLineMarker : public AudioMarker {
int position;
Pen style;
FeetStyle feet;
public: public:
int GetPosition() const { return position; } int GetPosition() const { return position; }
wxPen GetStyle() const { return style; } wxPen GetStyle() const { return *style; }
FeetStyle GetFeet() const { return feet; } FeetStyle GetFeet() const { return feet; }
bool CanSnap() const { return true; } bool CanSnap() const { return true; }
InactiveLineMarker(int position, bool start) /// Move the marker to a new position
/// @param new_position The position to move the marker to, in milliseconds
///
/// This notifies the owning line of the change, so that it can ensure that
/// this marker has the appropriate rendering style.
void SetPosition(int new_position);
/// Constructor
/// @param position Initial position of this marker
/// @param style Rendering style of this marker
/// @param feet Foot style of this marker
/// @param type Type of this marker, used only for sorting
/// @param line Line which this is a marker for
DialogueTimingMarker(int position, const Pen *style, FeetStyle feet, AudioRenderingStyle type, TimeableLine *line)
: position(position) : position(position)
, style("Colour/Audio Display/Line Boundary Inactive Line", "Audio/Line Boundaries Thickness") , style(style)
, feet(start ? Feet_Right : Feet_Left) , feet(feet)
, type(type)
, line(line)
{ {
} }
DialogueTimingMarker(DialogueTimingMarker const& other, TimeableLine *line)
: position(other.position)
, style(other.style)
, feet(other.feet)
, type(other.type)
, line(line)
{
}
/// Get the line which this is a marker for
TimeableLine *GetLine() const { return line; }
/// Implicit decay to the position of the marker /// Implicit decay to the position of the marker
operator int() const { return position; } operator int() const { return position; }
/// Comparison operator
///
/// Compares first on position, then on audio rendering style so that the
/// markers for the active line end up after those for the inactive lines.
bool operator<(DialogueTimingMarker const& other) const
{
if (position < other.position) return true;
if (position > other.position) return false;
return type < other.type;
}
/// Swap the rendering style of this marker with that of the passed marker
void SwapStyles(DialogueTimingMarker &other)
{
std::swap(style, other.style);
std::swap(feet, other.feet);
}
}; };
/// A comparison predicate for pointers to dialogue markers and millisecond positions
struct marker_ptr_cmp
{
bool operator()(const DialogueTimingMarker *lft, const DialogueTimingMarker *rgt) const
{
return *lft < *rgt;
}
bool operator()(const DialogueTimingMarker *lft, int rgt) const
{
return *lft < rgt;
}
bool operator()(int lft, const DialogueTimingMarker *rgt) const
{
return lft < *rgt;
}
};
/// @class TimeableLine
/// @brief A single dialogue line which can be timed via AudioTimingControllerDialogue
///
/// This class provides markers and styling ranges for a single dialogue line,
/// both active and inactive. In addition, it can apply changes made via those
/// markers to the tracked dialogue line.
class TimeableLine {
/// The current tracked dialogue line
AssDialogue *line;
/// The rendering style of this line
AudioRenderingStyle style;
/// One of the markers. Initially the left marker, but the user may change this.
DialogueTimingMarker marker1;
/// One of the markers. Initially the right marker, but the user may change this.
DialogueTimingMarker marker2;
/// Pointer to whichever marker happens to be on the left
DialogueTimingMarker *left_marker;
/// Pointer to whichever marker happens to be on the right
DialogueTimingMarker *right_marker;
public:
/// Constructor
/// @param style Rendering style to use for this line's time range
/// @param style_left The rendering style for the start marker
/// @param style_right The rendering style for the end marker
TimeableLine(AudioRenderingStyle style, const Pen *style_left, const Pen *style_right)
: line(0)
, style(style)
, marker1(0, style_left, AudioMarker::Feet_Right, style, this)
, marker2(0, style_right, AudioMarker::Feet_Left, style, this)
, left_marker(&marker1)
, right_marker(&marker2)
{
}
/// Explicit copy constructor needed due to that the markers have a pointer to this
TimeableLine(TimeableLine const& other)
: line(other.line)
, style(other.style)
, marker1(*other.left_marker, this)
, marker2(*other.right_marker, this)
, left_marker(&marker1)
, right_marker(&marker2)
{
}
/// Get the tracked dialogue line
AssDialogue *GetLine() const { return line; }
/// Get the time range for this line
operator TimeRange() const { return TimeRange(*left_marker, *right_marker); }
/// Add this line's style to the style ranges
void GetStyleRange(AudioRenderingStyleRanges *ranges) const
{
ranges->AddRange(*left_marker, *right_marker, style);
}
/// Get this line's markers
/// @param c Vector to add the markers to
void GetMarkers(std::vector<DialogueTimingMarker*> *c) const
{
c->push_back(left_marker);
c->push_back(right_marker);
}
/// Get the leftmost of the markers
DialogueTimingMarker *GetLeftMarker() { return left_marker; }
const DialogueTimingMarker *GetLeftMarker() const { return left_marker; }
/// Get the rightmost of the markers
DialogueTimingMarker *GetRightMarker() { return right_marker; }
const DialogueTimingMarker *GetRightMarker() const { return right_marker; }
/// Does this line have a marker in the given range?
bool ContainsMarker(TimeRange const& range) const
{
return range.contains(marker1) || range.contains(marker2);
}
/// Check if the markers have the correct styles, and correct them if needed
void CheckMarkers()
{
if (*right_marker < *left_marker)
{
marker1.SwapStyles(marker2);
std::swap(left_marker, right_marker);
}
}
/// Apply any changes made here to the tracked dialogue line
void Apply()
{
if (line)
{
line->Start = left_marker->GetPosition();
line->End = right_marker->GetPosition();
}
}
/// Set the dialogue line which this is tracking and reset the markers to
/// the line's time range
void SetLine(AssDialogue *new_line)
{
line = new_line;
marker1.SetPosition(new_line->Start);
marker2.SetPosition(new_line->End);
}
};
void DialogueTimingMarker::SetPosition(int new_position) {
position = new_position;
line->CheckMarkers();
}
/// @class AudioTimingControllerDialogue /// @class AudioTimingControllerDialogue
/// @brief Default timing mode for dialogue subtitles /// @brief Default timing mode for dialogue subtitles
/// ///
/// Displays a start and end marker for an active subtitle line, and allows /// Displays a start and end marker for an active subtitle line, and possibly
/// for those markers to be dragged. Dragging the start/end markers changes /// some of the inactive lines. The markers for the active line can be dragged,
/// the audio selection. /// updating the audio selection and the start/end time of that line. In
/// /// addition, any markers for inactive lines that start/end at the same time
/// Another later expansion will be to affect the timing of multiple selected /// as the active line starts/ends can optionally be dragged along with the
/// lines at the same time, if they e.g. have end1==start2. /// active line's markers, updating those lines as well.
class AudioTimingControllerDialogue : public AudioTimingController, private SelectionListener<AssDialogue> { class AudioTimingControllerDialogue : public AudioTimingController, private SelectionListener<AssDialogue> {
/// Start and end markers for the active line /// The rendering style for the active line's start marker
AudioMarkerDialogueTiming active_markers[2]; Pen style_left;
/// The rendering style for the active line's end marker
Pen style_right;
/// The rendering style for the start and end markers of inactive lines
Pen style_inactive;
/// Markers for inactive lines /// The currently active line
std::vector<InactiveLineMarker> inactive_markers; TimeableLine active_line;
/// Time ranges with inactive lines /// Inactive lines which are currently modifiable
std::vector<std::pair<int, int> > inactive_ranges; std::list<TimeableLine> inactive_lines;
/// All audio markers for active and inactive lines, sorted by position
std::vector<DialogueTimingMarker*> markers;
/// Marker provider for video keyframes /// Marker provider for video keyframes
AudioMarkerProviderKeyframes keyframes_provider; AudioMarkerProviderKeyframes keyframes_provider;
@ -160,10 +303,9 @@ class AudioTimingControllerDialogue : public AudioTimingController, private Sele
/// Marker provider for video playback position /// Marker provider for video playback position
VideoPositionMarkerProvider video_position_provider; VideoPositionMarkerProvider video_position_provider;
/// Has the timing been modified by the user? /// The set of lines which have been modified and need to have their
/// If auto commit is enabled this will only be true very briefly following /// changes applied on commit
/// changes std::set<TimeableLine*> modified_lines;
bool timing_modified;
/// Commit id for coalescing purposes when in auto commit mode /// Commit id for coalescing purposes when in auto commit mode
int commit_id; int commit_id;
@ -181,33 +323,32 @@ class AudioTimingControllerDialogue : public AudioTimingController, private Sele
agi::signal::Connection inactive_line_mode_connection; agi::signal::Connection inactive_line_mode_connection;
agi::signal::Connection inactive_line_comment_connection; agi::signal::Connection inactive_line_comment_connection;
/// Get the leftmost of the markers
AudioMarkerDialogueTiming *GetLeftMarker();
const AudioMarkerDialogueTiming *GetLeftMarker() const;
/// Get the rightmost of the markers
AudioMarkerDialogueTiming *GetRightMarker();
const AudioMarkerDialogueTiming *GetRightMarker() const;
/// Update the audio controller's selection /// Update the audio controller's selection
void UpdateSelection(); void UpdateSelection();
/// Regenerate markers for inactive lines /// Regenerate the list of timeable inactive lines
void RegenerateInactiveLines(); void RegenerateInactiveLines();
/// Add the inactive line markers for a single line /// Add a line to the list of timeable inactive lines
/// @param line Line to add markers for. May be NULL. void AddInactiveLine(AssDialogue *diag);
void AddInactiveMarkers(AssDialogue *line);
/// @brief Set the position of a marker and announce the change to the world /// Regenerate the list of active and inactive line markers
/// @param marker Marker to move void RegenerateMarkers();
/// @param ms New position of the marker
void SetMarker(AudioMarkerDialogueTiming *marker, int ms); /// @brief Set the position of markers and announce the change to the world
/// @param upd_markers Markers to move
/// @param ms New position of the markers
void SetMarkers(std::vector<AudioMarker*> const& upd_markers, int ms);
/// Snap a position to a nearby marker, if any /// Snap a position to a nearby marker, if any
/// @param position Position to snap /// @param position Position to snap
/// @param snap_range Maximum distance to snap in milliseconds /// @param snap_range Maximum distance to snap in milliseconds
int SnapPosition(int position, int snap_range) const; /// @param exclude Markers which should be excluded from the potential snaps
int SnapPosition(int position, int snap_range, std::vector<AudioMarker*> const& exclude) const;
/// Commit all pending changes to the file
/// @param user_triggered Is this a user-initiated commit or an autocommit
void DoCommit(bool user_triggered);
// SubtitleSelectionListener interface // SubtitleSelectionListener interface
void OnActiveLineChanged(AssDialogue *new_line); void OnActiveLineChanged(AssDialogue *new_line);
@ -235,11 +376,10 @@ public:
std::vector<AudioMarker*> OnRightClick(int ms, bool, int sensitivity, int snap_range); std::vector<AudioMarker*> OnRightClick(int ms, bool, int sensitivity, int snap_range);
void OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range); void OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range);
public: /// Constructor
// Specific interface /// @param c Project context
/// @brief Constructor
AudioTimingControllerDialogue(agi::Context *c); AudioTimingControllerDialogue(agi::Context *c);
/// Destructor
~AudioTimingControllerDialogue(); ~AudioTimingControllerDialogue();
}; };
@ -248,56 +388,13 @@ AudioTimingController *CreateDialogueTimingController(agi::Context *c)
return new AudioTimingControllerDialogue(c); return new AudioTimingControllerDialogue(c);
} }
// AudioMarkerDialogueTiming
void AudioMarkerDialogueTiming::SetPosition(int new_position)
{
position = new_position;
if (other)
{
if (position < other->position)
{
feet = Feet_Right;
other->feet = Feet_Left;
style = style_left;
other->style = style_right;
}
else if (position > other->position)
{
feet = Feet_Left;
other->feet = Feet_Right;
style = style_right;
other->style = style_left;
}
}
}
AudioMarkerDialogueTiming::AudioMarkerDialogueTiming()
: other(0)
, position(0)
, style(*wxTRANSPARENT_PEN)
, feet(Feet_None)
, style_left("Colour/Audio Display/Line boundary Start", "Audio/Line Boundaries Thickness")
, style_right("Colour/Audio Display/Line boundary End", "Audio/Line Boundaries Thickness")
{
// Nothing more to do
}
void AudioMarkerDialogueTiming::InitPair(AudioMarkerDialogueTiming *marker1, AudioMarkerDialogueTiming *marker2)
{
assert(marker1->other == 0);
assert(marker2->other == 0);
marker1->other = marker2;
marker2->other = marker1;
}
// AudioTimingControllerDialogue
AudioTimingControllerDialogue::AudioTimingControllerDialogue(agi::Context *c) AudioTimingControllerDialogue::AudioTimingControllerDialogue(agi::Context *c)
: keyframes_provider(c, "Audio/Display/Draw/Keyframes in Dialogue Mode") : style_left("Colour/Audio Display/Line boundary Start", "Audio/Line Boundaries Thickness")
, style_right("Colour/Audio Display/Line boundary End", "Audio/Line Boundaries Thickness")
, style_inactive("Colour/Audio Display/Line Boundary Inactive Line", "Audio/Line Boundaries Thickness")
, active_line(AudioStyle_Selected, &style_left, &style_right)
, keyframes_provider(c, "Audio/Display/Draw/Keyframes in Dialogue Mode")
, video_position_provider(c) , video_position_provider(c)
, timing_modified(false)
, commit_id(-1) , commit_id(-1)
, context(c) , context(c)
, auto_commit(OPT_GET("Audio/Auto/Commit")) , auto_commit(OPT_GET("Audio/Auto/Commit"))
@ -307,8 +404,6 @@ AudioTimingControllerDialogue::AudioTimingControllerDialogue(agi::Context *c)
, inactive_line_mode_connection(OPT_SUB("Audio/Inactive Lines Display Mode", &AudioTimingControllerDialogue::RegenerateInactiveLines, this)) , inactive_line_mode_connection(OPT_SUB("Audio/Inactive Lines Display Mode", &AudioTimingControllerDialogue::RegenerateInactiveLines, this))
, inactive_line_comment_connection(OPT_SUB("Audio/Display/Draw/Inactive Comments", &AudioTimingControllerDialogue::RegenerateInactiveLines, this)) , inactive_line_comment_connection(OPT_SUB("Audio/Display/Draw/Inactive Comments", &AudioTimingControllerDialogue::RegenerateInactiveLines, this))
{ {
AudioMarkerDialogueTiming::InitPair(&active_markers[0], &active_markers[1]);
c->selectionController->AddSelectionListener(this); c->selectionController->AddSelectionListener(this);
keyframes_provider.AddMarkerMovedListener(std::tr1::bind(std::tr1::ref(AnnounceMarkerMoved))); keyframes_provider.AddMarkerMovedListener(std::tr1::bind(std::tr1::ref(AnnounceMarkerMoved)));
video_position_provider.AddMarkerMovedListener(std::tr1::bind(std::tr1::ref(AnnounceMarkerMoved))); video_position_provider.AddMarkerMovedListener(std::tr1::bind(std::tr1::ref(AnnounceMarkerMoved)));
@ -322,43 +417,16 @@ AudioTimingControllerDialogue::~AudioTimingControllerDialogue()
context->selectionController->RemoveSelectionListener(this); context->selectionController->RemoveSelectionListener(this);
} }
AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetLeftMarker()
{
return active_markers[0] < active_markers[1] ? &active_markers[0] : &active_markers[1];
}
const AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetLeftMarker() const
{
return &std::min(active_markers[0], active_markers[1]);
}
AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetRightMarker()
{
return active_markers[0] < active_markers[1] ? &active_markers[1] : &active_markers[0];
}
const AudioMarkerDialogueTiming *AudioTimingControllerDialogue::GetRightMarker() const
{
return &std::max(active_markers[0], active_markers[1]);
}
void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const
{ {
// The order matters here; later markers are painted on top of earlier // The order matters here; later markers are painted on top of earlier
// markers, so the markers that we want to end up on top need to appear last // markers, so the markers that we want to end up on top need to appear last
// Copy inactive line markers in the range // Copy inactive line markers in the range
std::vector<InactiveLineMarker>::const_iterator copy(
a = lower_bound(inactive_markers.begin(), inactive_markers.end(), range.begin()), lower_bound(markers.begin(), markers.end(), range.begin(), marker_ptr_cmp()),
b = upper_bound(inactive_markers.begin(), inactive_markers.end(), range.end()); upper_bound(markers.begin(), markers.end(), range.end(), marker_ptr_cmp()),
back_inserter(out_markers));
for (; a != b; ++a)
out_markers.push_back(&*a);
if (range.contains(active_markers[0]))
out_markers.push_back(&active_markers[0]);
if (range.contains(active_markers[1]))
out_markers.push_back(&active_markers[1]);
keyframes_provider.GetMarkers(range, out_markers); keyframes_provider.GetMarkers(range, out_markers);
video_position_provider.GetMarkers(range, out_markers); video_position_provider.GetMarkers(range, out_markers);
@ -376,14 +444,10 @@ void AudioTimingControllerDialogue::OnSelectedSetChanged(const Selection &lines_
void AudioTimingControllerDialogue::OnFileChanged(int type) { void AudioTimingControllerDialogue::OnFileChanged(int type) {
if (type & AssFile::COMMIT_DIAG_TIME) if (type & AssFile::COMMIT_DIAG_TIME)
{
Revert(); Revert();
}
else if (type & AssFile::COMMIT_DIAG_ADDREM) else if (type & AssFile::COMMIT_DIAG_ADDREM)
{
RegenerateInactiveLines(); RegenerateInactiveLines();
} }
}
wxString AudioTimingControllerDialogue::GetWarningMessage() const wxString AudioTimingControllerDialogue::GetWarningMessage() const
{ {
@ -398,16 +462,14 @@ TimeRange AudioTimingControllerDialogue::GetIdealVisibleTimeRange() const
TimeRange AudioTimingControllerDialogue::GetPrimaryPlaybackRange() const TimeRange AudioTimingControllerDialogue::GetPrimaryPlaybackRange() const
{ {
return TimeRange(*GetLeftMarker(), *GetRightMarker()); return active_line;
} }
void AudioTimingControllerDialogue::GetRenderingStyles(AudioRenderingStyleRanges &ranges) const void AudioTimingControllerDialogue::GetRenderingStyles(AudioRenderingStyleRanges &ranges) const
{ {
ranges.AddRange(*GetLeftMarker(), *GetRightMarker(), AudioStyle_Selected); active_line.GetStyleRange(&ranges);
for (size_t i = 0; i < inactive_ranges.size(); ++i) for_each(inactive_lines.begin(), inactive_lines.end(),
{ bind(&TimeableLine::GetStyleRange, std::tr1::placeholders::_1, &ranges));
ranges.AddRange(inactive_ranges[i].first, inactive_ranges[i].second, AudioStyle_Inactive);
}
} }
void AudioTimingControllerDialogue::Next() void AudioTimingControllerDialogue::Next()
@ -422,25 +484,17 @@ void AudioTimingControllerDialogue::Prev()
void AudioTimingControllerDialogue::Commit() void AudioTimingControllerDialogue::Commit()
{ {
int new_start_ms = *GetLeftMarker(); DoCommit(true);
int new_end_ms = *GetRightMarker();
// If auto committing is enabled, timing_modified will be true iif it is an
// auto commit, as there is never pending changes to commit when the button
// is clicked
bool user_triggered = !(timing_modified && auto_commit->GetBool());
// Store back new times
if (timing_modified)
{
Selection sel;
context->selectionController->GetSelectedSet(sel);
for (Selection::iterator sub = sel.begin(); sub != sel.end(); ++sub)
{
(*sub)->Start = new_start_ms;
(*sub)->End = new_end_ms;
} }
void AudioTimingControllerDialogue::DoCommit(bool user_triggered)
{
// Store back new times
if (modified_lines.size())
{
for_each(modified_lines.begin(), modified_lines.end(),
std::tr1::mem_fn(&TimeableLine::Apply));
commit_connection.Block(); commit_connection.Block();
if (user_triggered) if (user_triggered)
{ {
@ -448,23 +502,32 @@ void AudioTimingControllerDialogue::Commit()
commit_id = -1; // never coalesce with a manually triggered commit commit_id = -1; // never coalesce with a manually triggered commit
} }
else else
commit_id = context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME, commit_id, context->selectionController->GetActiveLine()); {
AssDialogue *amend = modified_lines.size() == 1 ? (*modified_lines.begin())->GetLine() : 0;
commit_id = context->ass->Commit(_("timing"), AssFile::COMMIT_DIAG_TIME, commit_id, amend);
}
commit_connection.Unblock(); commit_connection.Unblock();
timing_modified = false; modified_lines.clear();
} }
if (user_triggered && OPT_GET("Audio/Next Line on Commit")->GetBool()) if (user_triggered && OPT_GET("Audio/Next Line on Commit")->GetBool())
{ {
int new_end_ms = *active_line.GetRightMarker();
/// @todo Old audio display created a new line if there was no next, /// @todo Old audio display created a new line if there was no next,
/// like the edit box, so maybe add a way to do that which both /// like the edit box, so maybe add a way to do that which both
/// this and the edit box can use /// this and the edit box can use
Next(); Next();
if (context->selectionController->GetActiveLine()->End == 0) {
if (*active_line.GetRightMarker() == 0) {
const int default_duration = OPT_GET("Timing/Default Duration")->GetInt(); const int default_duration = OPT_GET("Timing/Default Duration")->GetInt();
active_markers[0].SetPosition(new_end_ms); // Setting right first here so that they don't get switched and the
active_markers[1].SetPosition(new_end_ms + default_duration); // same marker gets set twice
timing_modified = true; active_line.GetRightMarker()->SetPosition(new_end_ms + default_duration);
active_line.GetLeftMarker()->SetPosition(new_end_ms);
sort(markers.begin(), markers.end(), marker_ptr_cmp());
modified_lines.insert(&active_line);
UpdateSelection(); UpdateSelection();
} }
} }
@ -474,74 +537,78 @@ void AudioTimingControllerDialogue::Revert()
{ {
if (AssDialogue *line = context->selectionController->GetActiveLine()) if (AssDialogue *line = context->selectionController->GetActiveLine())
{ {
if (line->Start != 0 || line->End != 0) active_line.SetLine(line);
{ modified_lines.clear();
active_markers[0].SetPosition(line->Start);
active_markers[1].SetPosition(line->End);
timing_modified = false;
AnnounceUpdatedPrimaryRange(); AnnounceUpdatedPrimaryRange();
if (inactive_line_mode->GetInt() == 0) if (inactive_line_mode->GetInt() == 0)
AnnounceUpdatedStyleRanges(); AnnounceUpdatedStyleRanges();
} }
}
RegenerateInactiveLines(); RegenerateInactiveLines();
} }
bool AudioTimingControllerDialogue::IsNearbyMarker(int ms, int sensitivity) const bool AudioTimingControllerDialogue::IsNearbyMarker(int ms, int sensitivity) const
{ {
TimeRange range(ms-sensitivity, ms+sensitivity); assert(sensitivity >= 0);
return active_line.ContainsMarker(TimeRange(ms-sensitivity, ms+sensitivity));
return range.contains(active_markers[0]) || range.contains(active_markers[1]);
} }
std::vector<AudioMarker*> AudioTimingControllerDialogue::OnLeftClick(int ms, bool ctrl_down, int sensitivity, int snap_range) std::vector<AudioMarker*> AudioTimingControllerDialogue::OnLeftClick(int ms, bool ctrl_down, int sensitivity, int snap_range)
{ {
assert(sensitivity >= 0); assert(sensitivity >= 0);
assert(snap_range >= 0);
int dist_l, dist_r; DialogueTimingMarker *left = active_line.GetLeftMarker();
DialogueTimingMarker *right = active_line.GetRightMarker();
AudioMarkerDialogueTiming *left = GetLeftMarker(); int dist_l = tabs(*left - ms);
AudioMarkerDialogueTiming *right = GetRightMarker(); int dist_r = tabs(*right - ms);
dist_l = tabs(*left - ms); if (dist_l > sensitivity && dist_r > sensitivity)
dist_r = tabs(*right - ms);
if (dist_l < dist_r && dist_l <= sensitivity)
{ {
// Clicked near the left marker:
// Insta-move it and start dragging it
SetMarker(left, SnapPosition(ms, snap_range));
return std::vector<AudioMarker*>(1, left);
}
if (dist_r < dist_l && dist_r <= sensitivity)
{
// Clicked near the right marker:
// Only drag it. For insta-move, the user must right-click.
return std::vector<AudioMarker*>(1, right);
}
// Clicked far from either marker: // Clicked far from either marker:
// Insta-set the left marker to the clicked position and return the right as the dragged one, // Insta-set the left marker to the clicked position and return the
// such that if the user does start dragging, he will create a new selection from scratch // right as the dragged one, such that if the user does start dragging,
SetMarker(left, SnapPosition(ms, snap_range)); // he will create a new selection from scratch
return std::vector<AudioMarker*>(1, right); std::vector<AudioMarker*> ret(1, left);
SetMarkers(ret, SnapPosition(ms, snap_range, ret));
ret[0] = right;
return ret;
}
DialogueTimingMarker *clicked = dist_l <= dist_r ? left : right;
std::vector<AudioMarker*> ret;
if (ctrl_down)
{
// The use of GetPosition here is important, as otherwise it'll start
// after lines ending at the same time as the active line begins
std::vector<DialogueTimingMarker*>::iterator it =
lower_bound(markers.begin(), markers.end(), clicked->GetPosition(), marker_ptr_cmp());
for(; it != markers.end() && !(*clicked < **it); ++it)
ret.push_back(*it);
}
else
ret.push_back(clicked);
// Left-click within drag range should still move the left marker to the
// clicked position, but not the right marker
if (clicked == left)
SetMarkers(ret, SnapPosition(ms, snap_range, ret));
return ret;
} }
std::vector<AudioMarker*> AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range) std::vector<AudioMarker*> AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range)
{ {
AudioMarkerDialogueTiming *right = GetRightMarker(); std::vector<AudioMarker*> ret(1, active_line.GetRightMarker());
SetMarker(right, SnapPosition(ms, snap_range)); SetMarkers(ret, SnapPosition(ms, snap_range, ret));
return std::vector<AudioMarker*>(1, right); return ret;
} }
void AudioTimingControllerDialogue::OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range) void AudioTimingControllerDialogue::OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range)
{ {
assert(markers.size() == 1); SetMarkers(markers, SnapPosition(new_position, snap_range, markers));
AudioMarker *marker = markers[0];
assert(marker == &active_markers[0] || marker == &active_markers[1]);
SetMarker(static_cast<AudioMarkerDialogueTiming*>(marker), SnapPosition(new_position, snap_range));
} }
void AudioTimingControllerDialogue::UpdateSelection() void AudioTimingControllerDialogue::UpdateSelection()
@ -550,12 +617,38 @@ void AudioTimingControllerDialogue::UpdateSelection()
AnnounceUpdatedStyleRanges(); AnnounceUpdatedStyleRanges();
} }
void AudioTimingControllerDialogue::SetMarker(AudioMarkerDialogueTiming *marker, int ms) void AudioTimingControllerDialogue::SetMarkers(std::vector<AudioMarker*> const& upd_markers, int ms)
{ {
// Since we're moving markers, the sorted list of markers will need to be
// resorted. To avoid resorting the entire thing, find the subrange that
// is effected.
int min_ms = ms;
int max_ms = ms;
for (size_t i = 0; i < upd_markers.size(); ++i)
{
DialogueTimingMarker *marker = static_cast<DialogueTimingMarker*>(upd_markers[i]);
min_ms = std::min<int>(*marker, min_ms);
max_ms = std::max<int>(*marker, max_ms);
}
std::vector<DialogueTimingMarker*>::iterator
begin = lower_bound(markers.begin(), markers.end(), min_ms, marker_ptr_cmp()),
end = upper_bound(begin, markers.end(), max_ms, marker_ptr_cmp());
// Update the markers
for (size_t i = 0; i < upd_markers.size(); ++i)
{
DialogueTimingMarker *marker = static_cast<DialogueTimingMarker*>(upd_markers[i]);
marker->SetPosition(ms); marker->SetPosition(ms);
timing_modified = true; modified_lines.insert(marker->GetLine());
if (auto_commit->GetBool()) Commit(); }
// Resort the range
sort(begin, end, marker_ptr_cmp());
if (auto_commit->GetBool()) DoCommit(false);
UpdateSelection(); UpdateSelection();
AnnounceMarkerMoved(); AnnounceMarkerMoved();
} }
@ -575,9 +668,8 @@ void AudioTimingControllerDialogue::RegenerateInactiveLines()
{ {
bool (*predicate)(AssEntry*) = inactive_line_comments->GetBool() ? dialogue : noncomment_dialogue; bool (*predicate)(AssEntry*) = inactive_line_comments->GetBool() ? dialogue : noncomment_dialogue;
bool was_empty = inactive_markers.empty(); bool was_empty = inactive_lines.empty();
inactive_markers.clear(); inactive_lines.clear();
inactive_ranges.clear();
switch (int mode = inactive_line_mode->GetInt()) switch (int mode = inactive_line_mode->GetInt())
{ {
@ -591,14 +683,14 @@ void AudioTimingControllerDialogue::RegenerateInactiveLines()
std::list<AssEntry*>::iterator prev = current_line; std::list<AssEntry*>::iterator prev = current_line;
while (--prev != context->ass->Line.begin() && !predicate(*prev)) ; while (--prev != context->ass->Line.begin() && !predicate(*prev)) ;
if (prev != context->ass->Line.begin()) if (prev != context->ass->Line.begin())
AddInactiveMarkers(static_cast<AssDialogue*>(*prev)); AddInactiveLine(static_cast<AssDialogue*>(*prev));
if (mode == 2) if (mode == 2)
{ {
std::list<AssEntry*>::iterator next = std::list<AssEntry*>::iterator next =
find_if(++current_line, context->ass->Line.end(), predicate); find_if(++current_line, context->ass->Line.end(), predicate);
if (next != context->ass->Line.end()) if (next != context->ass->Line.end())
AddInactiveMarkers(static_cast<AssDialogue*>(*next)); AddInactiveLine(static_cast<AssDialogue*>(*next));
} }
} }
break; break;
@ -608,21 +700,42 @@ void AudioTimingControllerDialogue::RegenerateInactiveLines()
for (std::list<AssEntry*>::const_iterator it = context->ass->Line.begin(); it != context->ass->Line.end(); ++it) for (std::list<AssEntry*>::const_iterator it = context->ass->Line.begin(); it != context->ass->Line.end(); ++it)
{ {
if (*it != active_line && predicate(*it)) if (*it != active_line && predicate(*it))
AddInactiveMarkers(static_cast<AssDialogue*>(*it)); AddInactiveLine(static_cast<AssDialogue*>(*it));
} }
break; break;
} }
default: default:
if (was_empty) if (was_empty)
{
RegenerateMarkers();
return; return;
} }
}
sort(inactive_markers.begin(), inactive_markers.end());
AnnounceUpdatedStyleRanges(); AnnounceUpdatedStyleRanges();
RegenerateMarkers();
}
void AudioTimingControllerDialogue::AddInactiveLine(AssDialogue *diag)
{
inactive_lines.push_back(TimeableLine(AudioStyle_Inactive, &style_inactive, &style_inactive));
inactive_lines.back().SetLine(diag);
}
void AudioTimingControllerDialogue::RegenerateMarkers()
{
markers.clear();
active_line.GetMarkers(&markers);
for_each(inactive_lines.begin(), inactive_lines.end(),
bind(&TimeableLine::GetMarkers, std::tr1::placeholders::_1, &markers));
sort(markers.begin(), markers.end(), marker_ptr_cmp());
AnnounceMarkerMoved(); AnnounceMarkerMoved();
} }
int AudioTimingControllerDialogue::SnapPosition(int position, int snap_range) const int AudioTimingControllerDialogue::SnapPosition(int position, int snap_range, std::vector<AudioMarker*> const& exclude) const
{ {
if (snap_range <= 0) if (snap_range <= 0)
return position; return position;
@ -633,7 +746,7 @@ int AudioTimingControllerDialogue::SnapPosition(int position, int snap_range) co
GetMarkers(snap_time_range, potential_snaps); GetMarkers(snap_time_range, potential_snaps);
for (AudioMarkerVector::iterator mi = potential_snaps.begin(); mi != potential_snaps.end(); ++mi) for (AudioMarkerVector::iterator mi = potential_snaps.begin(); mi != potential_snaps.end(); ++mi)
{ {
if ((*mi)->CanSnap()) if ((*mi)->CanSnap() && find(exclude.begin(), exclude.end(), *mi) == exclude.end())
{ {
if (!snap_marker) if (!snap_marker)
snap_marker = *mi; snap_marker = *mi;
@ -646,10 +759,3 @@ int AudioTimingControllerDialogue::SnapPosition(int position, int snap_range) co
return snap_marker->GetPosition(); return snap_marker->GetPosition();
return position; return position;
} }
void AudioTimingControllerDialogue::AddInactiveMarkers(AssDialogue *line)
{
inactive_markers.push_back(InactiveLineMarker(line->Start, true));
inactive_markers.push_back(InactiveLineMarker(line->End, false));
inactive_ranges.push_back(std::pair<int, int>(line->Start, line->End));
}