mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-18 12:46:32 +01:00
the start (only the start) of MIDI diff commands
git-svn-id: svn://localhost/ardour2/branches/3.0@5637 d708f5d6-7413-0410-9779-e7cbd77b26cf
This commit is contained in:
parent
c3c5c9a559
commit
837bfc9af4
9 changed files with 536 additions and 65 deletions
|
|
@ -1636,5 +1636,5 @@ widget "*RegionListWholeFile" style:highest "treeview_parent_node"
|
||||||
widget "*EditorHScrollbar" style:highest "editor_hscrollbar"
|
widget "*EditorHScrollbar" style:highest "editor_hscrollbar"
|
||||||
widget "*OddPortGroups" style:highest "odd_port_groups"
|
widget "*OddPortGroups" style:highest "odd_port_groups"
|
||||||
widget "*EvenPortGroups" style:highest "even_port_groups"
|
widget "*EvenPortGroups" style:highest "even_port_groups"
|
||||||
Widget "*MidiListView" style:highest "white_tree_view"
|
widget "*MidiListView" style:highest "white_tree_view"
|
||||||
Widget "*MidiListView*" style:highest "white_tree_view"
|
Widget "*MidiListView*" style:highest "white_tree_view"
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,6 @@ MidiRegionView::canvas_event(GdkEvent* ev)
|
||||||
} else if (ev->key.keyval == GDK_Delete) {
|
} else if (ev->key.keyval == GDK_Delete) {
|
||||||
|
|
||||||
delete_selection();
|
delete_selection();
|
||||||
apply_command();
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} else if (ev->key.keyval == GDK_Tab) {
|
} else if (ev->key.keyval == GDK_Tab) {
|
||||||
|
|
@ -309,8 +308,8 @@ MidiRegionView::canvas_event(GdkEvent* ev)
|
||||||
|
|
||||||
} else if (ev->key.keyval == GDK_Control_L) {
|
} else if (ev->key.keyval == GDK_Control_L) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case GDK_KEY_RELEASE:
|
case GDK_KEY_RELEASE:
|
||||||
|
|
@ -584,7 +583,15 @@ MidiRegionView::start_delta_command(string name)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::command_add_note(const boost::shared_ptr<NoteType> note, bool selected, bool show_velocity)
|
MidiRegionView::start_diff_command(string name)
|
||||||
|
{
|
||||||
|
if (!_diff_command) {
|
||||||
|
_diff_command = _model->new_diff_command(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiRegionView::delta_add_note(const boost::shared_ptr<NoteType> note, bool selected, bool show_velocity)
|
||||||
{
|
{
|
||||||
if (_delta_command) {
|
if (_delta_command) {
|
||||||
_delta_command->add(note);
|
_delta_command->add(note);
|
||||||
|
|
@ -598,7 +605,7 @@ MidiRegionView::command_add_note(const boost::shared_ptr<NoteType> note, bool se
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::command_remove_note(ArdourCanvas::CanvasNoteEvent* ev)
|
MidiRegionView::delta_remove_note(ArdourCanvas::CanvasNoteEvent* ev)
|
||||||
{
|
{
|
||||||
if (_delta_command && ev->note()) {
|
if (_delta_command && ev->note()) {
|
||||||
_delta_command->remove(ev->note());
|
_delta_command->remove(ev->note());
|
||||||
|
|
@ -606,7 +613,17 @@ MidiRegionView::command_remove_note(ArdourCanvas::CanvasNoteEvent* ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::apply_command()
|
MidiRegionView::diff_add_change (ArdourCanvas::CanvasNoteEvent* ev,
|
||||||
|
MidiModel::DiffCommand::Property property,
|
||||||
|
uint8_t val)
|
||||||
|
{
|
||||||
|
if (_diff_command) {
|
||||||
|
_diff_command->change (ev->note(), property, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiRegionView::apply_delta()
|
||||||
{
|
{
|
||||||
if (!_delta_command) {
|
if (!_delta_command) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -626,7 +643,27 @@ MidiRegionView::apply_command()
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::apply_command_as_subcommand()
|
MidiRegionView::apply_diff ()
|
||||||
|
{
|
||||||
|
if (!_diff_command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all selected notes for selection when model reloads
|
||||||
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
||||||
|
_marked_for_selection.insert((*i)->note());
|
||||||
|
}
|
||||||
|
|
||||||
|
_model->apply_command(trackview.session(), _diff_command);
|
||||||
|
_diff_command = NULL;
|
||||||
|
midi_view()->midi_track()->diskstream()->playlist_modified();
|
||||||
|
|
||||||
|
_marked_for_selection.clear();
|
||||||
|
_marked_for_velocity.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiRegionView::apply_delta_as_subcommand()
|
||||||
{
|
{
|
||||||
if (!_delta_command) {
|
if (!_delta_command) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -645,15 +682,36 @@ MidiRegionView::apply_command_as_subcommand()
|
||||||
_marked_for_velocity.clear();
|
_marked_for_velocity.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiRegionView::apply_diff_as_subcommand()
|
||||||
|
{
|
||||||
|
if (!_diff_command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all selected notes for selection when model reloads
|
||||||
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
||||||
|
_marked_for_selection.insert((*i)->note());
|
||||||
|
}
|
||||||
|
|
||||||
|
_model->apply_command_as_subcommand(trackview.session(), _diff_command);
|
||||||
|
_diff_command = NULL;
|
||||||
|
midi_view()->midi_track()->diskstream()->playlist_modified();
|
||||||
|
|
||||||
|
_marked_for_selection.clear();
|
||||||
|
_marked_for_velocity.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::abort_command()
|
MidiRegionView::abort_command()
|
||||||
{
|
{
|
||||||
delete _delta_command;
|
delete _delta_command;
|
||||||
_delta_command = NULL;
|
_delta_command = 0;
|
||||||
|
delete _diff_command;
|
||||||
|
_diff_command = 0;
|
||||||
clear_selection();
|
clear_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::redisplay_model()
|
MidiRegionView::redisplay_model()
|
||||||
{
|
{
|
||||||
|
|
@ -1277,7 +1335,7 @@ MidiRegionView::delete_selection()
|
||||||
|
|
||||||
_selection.clear();
|
_selection.clear();
|
||||||
|
|
||||||
apply_command ();
|
apply_delta ();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1558,14 +1616,14 @@ MidiRegionView::note_dropped(CanvasNoteEvent* ev, double dt, uint8_t dnote)
|
||||||
|
|
||||||
copy->set_note(new_pitch);
|
copy->set_note(new_pitch);
|
||||||
|
|
||||||
command_remove_note(*i);
|
delta_remove_note(*i);
|
||||||
cerr << "Adding note: " << *copy << endl;
|
cerr << "Adding note: " << *copy << endl;
|
||||||
command_add_note(copy, (*i)->selected());
|
delta_add_note(copy, (*i)->selected());
|
||||||
|
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command();
|
apply_delta();
|
||||||
|
|
||||||
// care about notes being moved beyond the upper/lower bounds on the canvas
|
// care about notes being moved beyond the upper/lower bounds on the canvas
|
||||||
if (lowest_note_in_selection < midi_stream_view()->lowest_note() ||
|
if (lowest_note_in_selection < midi_stream_view()->lowest_note() ||
|
||||||
|
|
@ -1720,15 +1778,15 @@ MidiRegionView::commit_resizing(CanvasNote::NoteEnd note_end, double event_x, bo
|
||||||
|
|
||||||
// resize beginning of note
|
// resize beginning of note
|
||||||
if (note_end == CanvasNote::NOTE_ON && current_frame < copy->end_time()) {
|
if (note_end == CanvasNote::NOTE_ON && current_frame < copy->end_time()) {
|
||||||
command_remove_note(canvas_note);
|
delta_remove_note(canvas_note);
|
||||||
copy->on_event().time() = current_frame;
|
copy->on_event().time() = current_frame;
|
||||||
command_add_note(copy, _selection.find(canvas_note) != _selection.end());
|
delta_add_note(copy, _selection.find(canvas_note) != _selection.end());
|
||||||
}
|
}
|
||||||
// resize end of note
|
// resize end of note
|
||||||
if (note_end == CanvasNote::NOTE_OFF && current_frame > copy->time()) {
|
if (note_end == CanvasNote::NOTE_OFF && current_frame > copy->time()) {
|
||||||
command_remove_note(canvas_note);
|
delta_remove_note(canvas_note);
|
||||||
copy->off_event().time() = current_frame;
|
copy->off_event().time() = current_frame;
|
||||||
command_add_note(copy, _selection.find(canvas_note) != _selection.end());
|
delta_add_note(copy, _selection.find(canvas_note) != _selection.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
delete resize_rect;
|
delete resize_rect;
|
||||||
|
|
@ -1736,7 +1794,7 @@ MidiRegionView::commit_resizing(CanvasNote::NoteEnd note_end, double event_x, bo
|
||||||
}
|
}
|
||||||
|
|
||||||
_resize_data.clear();
|
_resize_data.clear();
|
||||||
apply_command();
|
apply_delta();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1752,25 +1810,23 @@ MidiRegionView::change_note_velocity(CanvasNoteEvent* event, int8_t velocity, bo
|
||||||
copy->set_velocity(velocity);
|
copy->set_velocity(velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
command_remove_note(event);
|
delta_remove_note(event);
|
||||||
command_add_note(copy, event->selected(), true);
|
delta_add_note(copy, event->selected(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MidiRegionView::change_note_note (CanvasNoteEvent* event, int8_t note, bool relative)
|
MidiRegionView::change_note_note (CanvasNoteEvent* event, int8_t note, bool relative)
|
||||||
{
|
{
|
||||||
const boost::shared_ptr<NoteType> copy(new NoteType(*(event->note().get())));
|
uint8_t new_note;
|
||||||
|
|
||||||
if (relative) {
|
if (relative) {
|
||||||
uint8_t new_note = copy->note() + note;
|
new_note = event->note()->note() + note;
|
||||||
clamp_to_0_127(new_note);
|
|
||||||
copy->set_note(new_note);
|
|
||||||
} else {
|
} else {
|
||||||
copy->set_note(note);
|
new_note = note;
|
||||||
}
|
}
|
||||||
|
|
||||||
command_remove_note(event);
|
clamp_to_0_127 (new_note);
|
||||||
command_add_note(copy, event->selected(), false);
|
diff_add_change (event, MidiModel::DiffCommand::NoteNumber, new_note);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1825,8 +1881,8 @@ MidiRegionView::trim_note (CanvasNoteEvent* event, Evoral::MusicalTime front_del
|
||||||
copy->set_length (copy->length() + end_delta);
|
copy->set_length (copy->length() + end_delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
command_remove_note(event);
|
delta_remove_note(event);
|
||||||
command_add_note(copy, event->selected(), false);
|
delta_add_note(copy, event->selected(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1848,8 +1904,8 @@ MidiRegionView::change_note_time (CanvasNoteEvent* event, Evoral::MusicalTime de
|
||||||
copy->set_time (delta);
|
copy->set_time (delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
command_remove_note(event);
|
delta_remove_note(event);
|
||||||
command_add_note(copy, event->selected(), false);
|
delta_add_note(copy, event->selected(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1888,7 +1944,7 @@ MidiRegionView::change_velocities (bool up, bool fine, bool allow_smush)
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command();
|
apply_delta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1925,7 +1981,7 @@ MidiRegionView::transpose (bool up, bool fine, bool allow_smush)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start_delta_command (_("transpose"));
|
start_diff_command (_("transpose"));
|
||||||
|
|
||||||
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
||||||
Selection::iterator next = i;
|
Selection::iterator next = i;
|
||||||
|
|
@ -1934,7 +1990,7 @@ MidiRegionView::transpose (bool up, bool fine, bool allow_smush)
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command ();
|
apply_diff ();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -1971,7 +2027,7 @@ MidiRegionView::change_note_lengths (bool fine, bool shorter, bool start, bool e
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command ();
|
apply_delta ();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2032,7 +2088,7 @@ MidiRegionView::nudge_notes (bool forward)
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command ();
|
apply_delta ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2049,13 +2105,13 @@ MidiRegionView::change_channel(uint8_t channel)
|
||||||
|
|
||||||
copy->set_channel(channel);
|
copy->set_channel(channel);
|
||||||
|
|
||||||
command_remove_note(event);
|
delta_remove_note(event);
|
||||||
command_add_note(copy, event->selected());
|
delta_add_note(copy, event->selected());
|
||||||
|
|
||||||
i = next;
|
i = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command();
|
apply_delta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2152,14 +2208,14 @@ MidiRegionView::cut_copy_clear (Editing::CutCopyOp op)
|
||||||
case Copy:
|
case Copy:
|
||||||
break;
|
break;
|
||||||
case Cut:
|
case Cut:
|
||||||
command_remove_note (*i);
|
delta_remove_note (*i);
|
||||||
break;
|
break;
|
||||||
case Clear:
|
case Clear:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command();
|
apply_delta();
|
||||||
}
|
}
|
||||||
|
|
||||||
MidiCutBuffer*
|
MidiCutBuffer*
|
||||||
|
|
@ -2212,7 +2268,7 @@ MidiRegionView::paste (nframes64_t pos, float times, const MidiCutBuffer& mcb)
|
||||||
|
|
||||||
/* make all newly added notes selected */
|
/* make all newly added notes selected */
|
||||||
|
|
||||||
command_add_note (copied_note, true);
|
delta_add_note (copied_note, true);
|
||||||
end_point = copied_note->end_time();
|
end_point = copied_note->end_time();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2233,7 +2289,7 @@ MidiRegionView::paste (nframes64_t pos, float times, const MidiCutBuffer& mcb)
|
||||||
trackview.session().add_command (new MementoCommand<Region>(*_region, &before, &_region->get_state()));
|
trackview.session().add_command (new MementoCommand<Region>(*_region, &before, &_region->get_state()));
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command ();
|
apply_delta ();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -2243,8 +2299,8 @@ MidiRegionView::add_note (uint8_t channel, uint8_t number, uint8_t velocity,
|
||||||
boost::shared_ptr<NoteType> new_note (new NoteType (channel, pos, len, number, velocity));
|
boost::shared_ptr<NoteType> new_note (new NoteType (channel, pos, len, number, velocity));
|
||||||
|
|
||||||
start_delta_command (_("step add"));
|
start_delta_command (_("step add"));
|
||||||
command_add_note (new_note, true, false);
|
delta_add_note (new_note, true, false);
|
||||||
apply_command ();
|
apply_delta();
|
||||||
|
|
||||||
/* potentially extend region to hold new note */
|
/* potentially extend region to hold new note */
|
||||||
|
|
||||||
|
|
@ -2333,7 +2389,7 @@ MidiRegionView::replace_selected (NoteList& replacements)
|
||||||
tmp = i;
|
tmp = i;
|
||||||
++tmp;
|
++tmp;
|
||||||
|
|
||||||
command_remove_note (*i);
|
delta_remove_note (*i);
|
||||||
remove_from_selection (*i);
|
remove_from_selection (*i);
|
||||||
|
|
||||||
i = tmp;
|
i = tmp;
|
||||||
|
|
@ -2342,9 +2398,9 @@ MidiRegionView::replace_selected (NoteList& replacements)
|
||||||
_selection.clear ();
|
_selection.clear ();
|
||||||
|
|
||||||
for (NoteList::iterator i = replacements.begin(); i != replacements.end(); ++i) {
|
for (NoteList::iterator i = replacements.begin(); i != replacements.end(); ++i) {
|
||||||
command_add_note (*i, true, false);
|
delta_add_note (*i, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_command_as_subcommand ();
|
apply_delta_as_subcommand ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,11 +169,16 @@ class MidiRegionView : public RegionView
|
||||||
void display_model(boost::shared_ptr<ARDOUR::MidiModel> model);
|
void display_model(boost::shared_ptr<ARDOUR::MidiModel> model);
|
||||||
|
|
||||||
void start_delta_command(std::string name = "midi edit");
|
void start_delta_command(std::string name = "midi edit");
|
||||||
void command_add_note(const boost::shared_ptr<NoteType> note, bool selected, bool show_velocity=false);
|
void delta_add_note(const boost::shared_ptr<NoteType> note, bool selected, bool show_velocity=false);
|
||||||
void command_remove_note(ArdourCanvas::CanvasNoteEvent* ev);
|
void delta_remove_note(ArdourCanvas::CanvasNoteEvent* ev);
|
||||||
|
|
||||||
void apply_command();
|
void start_diff_command(std::string name = "midi edit");
|
||||||
void apply_command_as_subcommand();
|
void diff_add_change(ArdourCanvas::CanvasNoteEvent* ev, ARDOUR::MidiModel::DiffCommand::Property, uint8_t val);
|
||||||
|
|
||||||
|
void apply_delta();
|
||||||
|
void apply_diff();
|
||||||
|
void apply_delta_as_subcommand();
|
||||||
|
void apply_diff_as_subcommand();
|
||||||
void abort_command();
|
void abort_command();
|
||||||
|
|
||||||
void note_entered(ArdourCanvas::CanvasNoteEvent* ev);
|
void note_entered(ArdourCanvas::CanvasNoteEvent* ev);
|
||||||
|
|
@ -354,6 +359,7 @@ class MidiRegionView : public RegionView
|
||||||
ArdourCanvas::CanvasNote** _active_notes;
|
ArdourCanvas::CanvasNote** _active_notes;
|
||||||
ArdourCanvas::Group* _note_group;
|
ArdourCanvas::Group* _note_group;
|
||||||
ARDOUR::MidiModel::DeltaCommand* _delta_command;
|
ARDOUR::MidiModel::DeltaCommand* _delta_command;
|
||||||
|
ARDOUR::MidiModel::DiffCommand* _diff_command;
|
||||||
|
|
||||||
MouseState _mouse_state;
|
MouseState _mouse_state;
|
||||||
int _pressed_button;
|
int _pressed_button;
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,62 @@ public:
|
||||||
NoteList _removed_notes;
|
NoteList _removed_notes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** Change note properties.
|
||||||
|
* More efficient than DeltaCommand and has the important property that
|
||||||
|
* it leaves the objects in the MidiModel (Notes) the same, thus
|
||||||
|
* enabling selection and other state to persist across command
|
||||||
|
* do/undo/redo.
|
||||||
|
*/
|
||||||
|
class DiffCommand : public Command {
|
||||||
|
public:
|
||||||
|
enum Property {
|
||||||
|
NoteNumber,
|
||||||
|
Velocity,
|
||||||
|
StartTime,
|
||||||
|
Length,
|
||||||
|
Channel
|
||||||
|
};
|
||||||
|
|
||||||
|
DiffCommand (boost::shared_ptr<MidiModel> m, const std::string& name);
|
||||||
|
DiffCommand (boost::shared_ptr<MidiModel> m, const XMLNode& node);
|
||||||
|
|
||||||
|
const std::string& name() const { return _name; }
|
||||||
|
|
||||||
|
void operator()();
|
||||||
|
void undo();
|
||||||
|
|
||||||
|
int set_state (const XMLNode&);
|
||||||
|
XMLNode& get_state ();
|
||||||
|
|
||||||
|
void change (const boost::shared_ptr<Evoral::Note<TimeType> > note,
|
||||||
|
Property prop, uint8_t new_value);
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::shared_ptr<MidiModel> _model;
|
||||||
|
const std::string _name;
|
||||||
|
|
||||||
|
struct NotePropertyChange {
|
||||||
|
DiffCommand::Property property;
|
||||||
|
boost::shared_ptr<Evoral::Note<TimeType> > note;
|
||||||
|
uint8_t old_value;
|
||||||
|
uint8_t new_value;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef std::list<NotePropertyChange> ChangeList;
|
||||||
|
ChangeList _changes;
|
||||||
|
|
||||||
|
XMLNode &marshal_change(const NotePropertyChange&);
|
||||||
|
NotePropertyChange unmarshal_change(XMLNode *xml_note);
|
||||||
|
};
|
||||||
|
|
||||||
MidiModel::DeltaCommand* new_delta_command(const std::string name="midi edit");
|
MidiModel::DeltaCommand* new_delta_command(const std::string name="midi edit");
|
||||||
|
MidiModel::DiffCommand* new_diff_command(const std::string name="midi edit");
|
||||||
void apply_command(Session& session, Command* cmd);
|
void apply_command(Session& session, Command* cmd);
|
||||||
void apply_command_as_subcommand(Session& session, Command* cmd);
|
void apply_command_as_subcommand(Session& session, Command* cmd);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool write_to(boost::shared_ptr<MidiSource> source);
|
bool write_to(boost::shared_ptr<MidiSource> source);
|
||||||
|
|
||||||
// MidiModel doesn't use the normal AutomationList serialisation code
|
// MidiModel doesn't use the normal AutomationList serialisation code
|
||||||
|
|
@ -105,6 +157,8 @@ public:
|
||||||
const MidiSource* midi_source() const { return _midi_source; }
|
const MidiSource* midi_source() const { return _midi_source; }
|
||||||
void set_midi_source(MidiSource* source) { _midi_source = source; }
|
void set_midi_source(MidiSource* source) { _midi_source = source; }
|
||||||
|
|
||||||
|
boost::shared_ptr<Evoral::Note<TimeType> > find_note (boost::shared_ptr<Evoral::Note<TimeType> >);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class DeltaCommand;
|
friend class DeltaCommand;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
#include "ardour/export_profile_manager.h"
|
#include "ardour/export_profile_manager.h"
|
||||||
#include "ardour/io.h"
|
#include "ardour/io.h"
|
||||||
#include "ardour/location.h"
|
#include "ardour/location.h"
|
||||||
|
#include "ardour/midi_model.h"
|
||||||
#include "ardour/midi_track.h"
|
#include "ardour/midi_track.h"
|
||||||
#include "ardour/mute_master.h"
|
#include "ardour/mute_master.h"
|
||||||
#include "ardour/panner.h"
|
#include "ardour/panner.h"
|
||||||
|
|
@ -109,6 +110,7 @@ setup_enum_writer ()
|
||||||
Delivery::Role _Delivery_Role;
|
Delivery::Role _Delivery_Role;
|
||||||
IO::Direction _IO_Direction;
|
IO::Direction _IO_Direction;
|
||||||
MuteMaster::MutePoint _MuteMaster_MutePoint;
|
MuteMaster::MutePoint _MuteMaster_MutePoint;
|
||||||
|
MidiModel::DiffCommand::Property _MidiModel_DiffCommand_Property;
|
||||||
|
|
||||||
#define REGISTER(e) enum_writer->register_distinct (typeid(e).name(), i, s); i.clear(); s.clear()
|
#define REGISTER(e) enum_writer->register_distinct (typeid(e).name(), i, s); i.clear(); s.clear()
|
||||||
#define REGISTER_BITS(e) enum_writer->register_bits (typeid(e).name(), i, s); i.clear(); s.clear()
|
#define REGISTER_BITS(e) enum_writer->register_bits (typeid(e).name(), i, s); i.clear(); s.clear()
|
||||||
|
|
@ -522,4 +524,11 @@ setup_enum_writer ()
|
||||||
REGISTER_CLASS_ENUM (IO, Input);
|
REGISTER_CLASS_ENUM (IO, Input);
|
||||||
REGISTER_CLASS_ENUM (IO, Output);
|
REGISTER_CLASS_ENUM (IO, Output);
|
||||||
REGISTER (_IO_Direction);
|
REGISTER (_IO_Direction);
|
||||||
|
|
||||||
|
REGISTER_CLASS_ENUM (MidiModel::DiffCommand, NoteNumber);
|
||||||
|
REGISTER_CLASS_ENUM (MidiModel::DiffCommand, Channel);
|
||||||
|
REGISTER_CLASS_ENUM (MidiModel::DiffCommand, Velocity);
|
||||||
|
REGISTER_CLASS_ENUM (MidiModel::DiffCommand, StartTime);
|
||||||
|
REGISTER_CLASS_ENUM (MidiModel::DiffCommand, Length);
|
||||||
|
REGISTER (_MidiModel_DiffCommand_Property);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ MidiModel::MidiModel(MidiSource* s, size_t size)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Start a new command.
|
/** Start a new Delta command.
|
||||||
*
|
*
|
||||||
* This has no side-effects on the model or Session, the returned command
|
* This has no side-effects on the model or Session, the returned command
|
||||||
* can be held on to for as long as the caller wishes, or discarded without
|
* can be held on to for as long as the caller wishes, or discarded without
|
||||||
|
|
@ -56,6 +56,19 @@ MidiModel::new_delta_command(const string name)
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Start a new Diff command.
|
||||||
|
*
|
||||||
|
* This has no side-effects on the model or Session, the returned command
|
||||||
|
* can be held on to for as long as the caller wishes, or discarded without
|
||||||
|
* formality, until apply_command is called and ownership is taken.
|
||||||
|
*/
|
||||||
|
MidiModel::DiffCommand*
|
||||||
|
MidiModel::new_diff_command(const string name)
|
||||||
|
{
|
||||||
|
DiffCommand* cmd = new DiffCommand(_midi_source->model(), name);
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
/** Apply a command.
|
/** Apply a command.
|
||||||
*
|
*
|
||||||
* Ownership of cmd is taken, it must not be deleted by the caller.
|
* Ownership of cmd is taken, it must not be deleted by the caller.
|
||||||
|
|
@ -225,7 +238,7 @@ MidiModel::DeltaCommand::unmarshal_note(XMLNode *xml_note)
|
||||||
length_str >> length;
|
length_str >> length;
|
||||||
} else {
|
} else {
|
||||||
warning << "note information missing length" << endmsg;
|
warning << "note information missing length" << endmsg;
|
||||||
note = 1;
|
length = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((prop = xml_note->property("velocity")) != 0) {
|
if ((prop = xml_note->property("velocity")) != 0) {
|
||||||
|
|
@ -286,6 +299,320 @@ MidiModel::DeltaCommand::get_state()
|
||||||
return *delta_command;
|
return *delta_command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************** DIFF COMMAND ********************/
|
||||||
|
|
||||||
|
#define DIFF_NOTES_ELEMENT "changed_notes"
|
||||||
|
#define DIFF_COMMAND_ELEMENT "DiffCommand"
|
||||||
|
|
||||||
|
MidiModel::DiffCommand::DiffCommand(boost::shared_ptr<MidiModel> m, const std::string& name)
|
||||||
|
: Command(name)
|
||||||
|
, _model(m)
|
||||||
|
, _name(name)
|
||||||
|
{
|
||||||
|
assert(_model);
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiModel::DiffCommand::DiffCommand(boost::shared_ptr<MidiModel> m, const XMLNode& node)
|
||||||
|
: _model(m)
|
||||||
|
{
|
||||||
|
assert(_model);
|
||||||
|
set_state(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiModel::DiffCommand::change(const boost::shared_ptr< Evoral::Note<TimeType> > note, Property prop,
|
||||||
|
uint8_t new_value)
|
||||||
|
{
|
||||||
|
NotePropertyChange change;
|
||||||
|
|
||||||
|
change.note = note;
|
||||||
|
change.property = prop;
|
||||||
|
change.new_value = new_value;
|
||||||
|
|
||||||
|
switch (prop) {
|
||||||
|
case NoteNumber:
|
||||||
|
change.old_value = note->note();
|
||||||
|
break;
|
||||||
|
case Velocity:
|
||||||
|
change.old_value = note->velocity();
|
||||||
|
break;
|
||||||
|
case StartTime:
|
||||||
|
change.old_value = note->time();
|
||||||
|
break;
|
||||||
|
case Length:
|
||||||
|
change.old_value = note->length();
|
||||||
|
break;
|
||||||
|
case Channel:
|
||||||
|
change.old_value = note->channel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_changes.push_back (change);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiModel::DiffCommand::operator()()
|
||||||
|
{
|
||||||
|
Glib::Mutex::Lock lm (_model->_midi_source->mutex());
|
||||||
|
_model->_midi_source->invalidate(); // release model read lock
|
||||||
|
_model->write_lock();
|
||||||
|
|
||||||
|
for (ChangeList::iterator i = _changes.begin(); i != _changes.end(); ++i) {
|
||||||
|
Property prop = i->property;
|
||||||
|
switch (prop) {
|
||||||
|
case NoteNumber:
|
||||||
|
i->note->set_note (i->new_value);
|
||||||
|
break;
|
||||||
|
case Velocity:
|
||||||
|
i->note->set_velocity (i->new_value);
|
||||||
|
break;
|
||||||
|
case StartTime:
|
||||||
|
i->note->set_time (i->new_value);
|
||||||
|
break;
|
||||||
|
case Length:
|
||||||
|
i->note->set_length (i->new_value);
|
||||||
|
break;
|
||||||
|
case Channel:
|
||||||
|
i->note->set_channel (i->new_value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_model->write_unlock();
|
||||||
|
_model->ContentsChanged(); /* EMIT SIGNAL */
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MidiModel::DiffCommand::undo()
|
||||||
|
{
|
||||||
|
Glib::Mutex::Lock lm (_model->_midi_source->mutex());
|
||||||
|
_model->_midi_source->invalidate(); // release model read lock
|
||||||
|
_model->write_lock();
|
||||||
|
|
||||||
|
for (ChangeList::iterator i = _changes.begin(); i != _changes.end(); ++i) {
|
||||||
|
Property prop = i->property;
|
||||||
|
switch (prop) {
|
||||||
|
case NoteNumber:
|
||||||
|
i->note->set_note (i->old_value);
|
||||||
|
break;
|
||||||
|
case Velocity:
|
||||||
|
i->note->set_velocity (i->old_value);
|
||||||
|
break;
|
||||||
|
case StartTime:
|
||||||
|
i->note->set_time (i->old_value);
|
||||||
|
break;
|
||||||
|
case Length:
|
||||||
|
i->note->set_length (i->old_value);
|
||||||
|
break;
|
||||||
|
case Channel:
|
||||||
|
i->note->set_channel (i->old_value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_model->write_unlock();
|
||||||
|
_model->ContentsChanged(); /* EMIT SIGNAL */
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLNode&
|
||||||
|
MidiModel::DiffCommand::marshal_change(const NotePropertyChange& change)
|
||||||
|
{
|
||||||
|
XMLNode* xml_change = new XMLNode("change");
|
||||||
|
|
||||||
|
/* first, the change itself */
|
||||||
|
|
||||||
|
xml_change->add_property ("property", enum_2_string (change.property));
|
||||||
|
|
||||||
|
{
|
||||||
|
ostringstream old_value_str (ios::ate);
|
||||||
|
old_value_str << (unsigned int) change.old_value;
|
||||||
|
xml_change->add_property ("old", old_value_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ostringstream new_value_str (ios::ate);
|
||||||
|
new_value_str << (unsigned int) change.old_value;
|
||||||
|
xml_change->add_property ("new", new_value_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* now the rest of the note */
|
||||||
|
|
||||||
|
if (change.property != NoteNumber) {
|
||||||
|
ostringstream note_str(ios::ate);
|
||||||
|
note_str << int(change.note->note());
|
||||||
|
xml_change->add_property("note", note_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Channel) {
|
||||||
|
ostringstream channel_str(ios::ate);
|
||||||
|
channel_str << int(change.note->channel());
|
||||||
|
xml_change->add_property("channel", channel_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != StartTime) {
|
||||||
|
ostringstream time_str(ios::ate);
|
||||||
|
time_str << int(change.note->time());
|
||||||
|
xml_change->add_property("time", time_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Length) {
|
||||||
|
ostringstream length_str(ios::ate);
|
||||||
|
length_str <<(unsigned int) change.note->length();
|
||||||
|
xml_change->add_property("length", length_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Velocity) {
|
||||||
|
ostringstream velocity_str(ios::ate);
|
||||||
|
velocity_str << (unsigned int) change.note->velocity();
|
||||||
|
xml_change->add_property("velocity", velocity_str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return *xml_change;
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiModel::DiffCommand::NotePropertyChange
|
||||||
|
MidiModel::DiffCommand::unmarshal_change(XMLNode *xml_change)
|
||||||
|
{
|
||||||
|
XMLProperty* prop;
|
||||||
|
NotePropertyChange change;
|
||||||
|
unsigned int note;
|
||||||
|
unsigned int channel;
|
||||||
|
unsigned int time;
|
||||||
|
unsigned int length;
|
||||||
|
unsigned int velocity;
|
||||||
|
|
||||||
|
if ((prop = xml_change->property("property")) != 0) {
|
||||||
|
change.property = (Property) string_2_enum (prop->value(), change.property);
|
||||||
|
} else {
|
||||||
|
fatal << "!!!" << endmsg;
|
||||||
|
/*NOTREACHED*/
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((prop = xml_change->property ("old")) != 0) {
|
||||||
|
istringstream old_str (prop->value());
|
||||||
|
old_str >> change.old_value;
|
||||||
|
} else {
|
||||||
|
fatal << "!!!" << endmsg;
|
||||||
|
/*NOTREACHED*/
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((prop = xml_change->property ("new")) == 0) {
|
||||||
|
istringstream new_str (prop->value());
|
||||||
|
new_str >> change.new_value;
|
||||||
|
} else {
|
||||||
|
fatal << "!!!" << endmsg;
|
||||||
|
/*NOTREACHED*/
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != NoteNumber) {
|
||||||
|
if ((prop = xml_change->property("note")) != 0) {
|
||||||
|
istringstream note_str(prop->value());
|
||||||
|
note_str >> note;
|
||||||
|
} else {
|
||||||
|
warning << "note information missing note value" << endmsg;
|
||||||
|
note = 127;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note = change.new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Channel) {
|
||||||
|
if ((prop = xml_change->property("channel")) != 0) {
|
||||||
|
istringstream channel_str(prop->value());
|
||||||
|
channel_str >> channel;
|
||||||
|
} else {
|
||||||
|
warning << "note information missing channel" << endmsg;
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel = change.new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != StartTime) {
|
||||||
|
if ((prop = xml_change->property("time")) != 0) {
|
||||||
|
istringstream time_str(prop->value());
|
||||||
|
time_str >> time;
|
||||||
|
} else {
|
||||||
|
warning << "note information missing time" << endmsg;
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
time = change.new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Length) {
|
||||||
|
if ((prop = xml_change->property("length")) != 0) {
|
||||||
|
istringstream length_str(prop->value());
|
||||||
|
length_str >> length;
|
||||||
|
} else {
|
||||||
|
warning << "note information missing length" << endmsg;
|
||||||
|
length = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
length = change.new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.property != Velocity) {
|
||||||
|
if ((prop = xml_change->property("velocity")) != 0) {
|
||||||
|
istringstream velocity_str(prop->value());
|
||||||
|
velocity_str >> velocity;
|
||||||
|
} else {
|
||||||
|
warning << "note information missing velocity" << endmsg;
|
||||||
|
velocity = 127;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
velocity = change.new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* we must point at the instance of the note that is actually in the model.
|
||||||
|
so go look for it ...
|
||||||
|
*/
|
||||||
|
|
||||||
|
boost::shared_ptr<Evoral::Note<TimeType> > new_note (new Evoral::Note<TimeType> (channel, time, length, note, velocity));
|
||||||
|
|
||||||
|
change.note = _model->find_note (new_note);
|
||||||
|
|
||||||
|
if (!change.note) {
|
||||||
|
warning << "MIDI note not found in model - programmers should investigate this" << endmsg;
|
||||||
|
/* use the actual new note */
|
||||||
|
change.note = new_note;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
MidiModel::DiffCommand::set_state(const XMLNode& diff_command)
|
||||||
|
{
|
||||||
|
if (diff_command.name() != string(DIFF_COMMAND_ELEMENT)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_changes.clear();
|
||||||
|
|
||||||
|
XMLNode* changed_notes = diff_command.child(DIFF_NOTES_ELEMENT);
|
||||||
|
XMLNodeList notes = changed_notes->children();
|
||||||
|
|
||||||
|
transform (notes.begin(), notes.end(), back_inserter(_changes),
|
||||||
|
sigc::mem_fun(*this, &DiffCommand::unmarshal_change));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLNode&
|
||||||
|
MidiModel::DiffCommand::get_state ()
|
||||||
|
{
|
||||||
|
XMLNode* diff_command = new XMLNode(DIFF_COMMAND_ELEMENT);
|
||||||
|
diff_command->add_property("midi-source", _model->midi_source()->id().to_s());
|
||||||
|
|
||||||
|
XMLNode* changes = diff_command->add_child(DIFF_NOTES_ELEMENT);
|
||||||
|
for_each(_changes.begin(), _changes.end(), sigc::compose(
|
||||||
|
sigc::mem_fun(*changes, &XMLNode::add_child_nocopy),
|
||||||
|
sigc::mem_fun(*this, &DiffCommand::marshal_change)));
|
||||||
|
|
||||||
|
return *diff_command;
|
||||||
|
}
|
||||||
|
|
||||||
/** Write the model to a MidiSource (i.e. save the model).
|
/** Write the model to a MidiSource (i.e. save the model).
|
||||||
* This is different from manually using read to write to a source in that
|
* This is different from manually using read to write to a source in that
|
||||||
* note off events are written regardless of the track mode. This is so the
|
* note off events are written regardless of the track mode. This is so the
|
||||||
|
|
@ -322,3 +649,14 @@ MidiModel::get_state()
|
||||||
return *node;
|
return *node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boost::shared_ptr<Evoral::Note<MidiModel::TimeType> >
|
||||||
|
MidiModel::find_note (boost::shared_ptr<Evoral::Note<TimeType> > other)
|
||||||
|
{
|
||||||
|
Notes::iterator i = find (notes().begin(), notes().end(), other);
|
||||||
|
|
||||||
|
if (i == notes().end()) {
|
||||||
|
return boost::shared_ptr<Evoral::Note<TimeType> > ();
|
||||||
|
}
|
||||||
|
|
||||||
|
return *i;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2932,6 +2932,16 @@ Session::restore_history (string snapshot_name)
|
||||||
} else {
|
} else {
|
||||||
error << "FIXME: Failed to downcast MidiSource for DeltaCommand" << endmsg;
|
error << "FIXME: Failed to downcast MidiSource for DeltaCommand" << endmsg;
|
||||||
}
|
}
|
||||||
|
} else if (n->name() == "DiffCommand") {
|
||||||
|
PBD::ID id(n->property("midi-source")->value());
|
||||||
|
boost::shared_ptr<MidiSource> midi_source =
|
||||||
|
boost::dynamic_pointer_cast<MidiSource, Source>(source_by_id(id));
|
||||||
|
if(midi_source) {
|
||||||
|
ut->add_command(new MidiModel::DiffCommand(midi_source->model(), *n));
|
||||||
|
} else {
|
||||||
|
error << "FIXME: Failed to downcast MidiSource for DeltaCommand" << endmsg;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
error << string_compose(_("Couldn't figure out how to make a Command out of a %1 XMLNode."), n->name()) << endmsg;
|
error << string_compose(_("Couldn't figure out how to make a Command out of a %1 XMLNode."), n->name()) << endmsg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,6 @@ private:
|
||||||
MIDIEvent<Time> _off_event;
|
MIDIEvent<Time> _off_event;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
} // namespace Evoral
|
} // namespace Evoral
|
||||||
|
|
||||||
template<typename Time>
|
template<typename Time>
|
||||||
|
|
|
||||||
|
|
@ -78,21 +78,20 @@ Note<Time>::~Note()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
template<typename Time>
|
template<typename Time>
|
||||||
const Note<Time>&
|
const Note<Time>&
|
||||||
Note<Time>::operator=(const Note<Time>& copy)
|
Note<Time>::operator=(const Note<Time>& other)
|
||||||
{
|
{
|
||||||
_on_event = copy._on_event;
|
_on_event = other._on_event;
|
||||||
_off_event = copy._off_event;
|
_off_event = other._off_event;
|
||||||
|
|
||||||
assert(time() == copy.time());
|
assert(time() == other.time());
|
||||||
assert(end_time() == copy.end_time());
|
assert(end_time() == other.end_time());
|
||||||
assert(note() == copy.note());
|
assert(note() == other.note());
|
||||||
assert(velocity() == copy.velocity());
|
assert(velocity() == other.velocity());
|
||||||
assert(length() == copy.length());
|
assert(length() == other.length());
|
||||||
assert(_on_event.channel() == _off_event.channel());
|
assert(_on_event.channel() == _off_event.channel());
|
||||||
assert(channel() == copy.channel());
|
assert(channel() == other.channel());
|
||||||
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue