, _optimization_iterator (_events.end())
, _list_editor (0)
, _no_sound_notes (false)
+ , _last_display_zoom (0)
, _last_event_x (0)
, _last_event_y (0)
, _grabbed_keyboard (false)
, _entered (false)
- , pre_enter_cursor (0)
- , pre_press_cursor (0)
- , pre_note_enter_cursor (0)
- , _note_player (0)
+ , _mouse_changed_selection (false)
{
CANVAS_DEBUG_NAME (_note_group, string_compose ("note group for %1", get_item_name()));
_note_group->raise_to_top();
, _region_relative_time_converter(r->session().tempo_map(), r->position())
, _source_relative_time_converter(r->session().tempo_map(), r->position() - r->start())
, _active_notes(0)
- , _note_group (new ArdourCanvas::Container (parent))
+ , _note_group (new ArdourCanvas::Container (group))
, _note_diff_command (0)
, _ghost_note(0)
, _step_edit_cursor (0)
, _optimization_iterator (_events.end())
, _list_editor (0)
, _no_sound_notes (false)
+ , _last_display_zoom (0)
, _last_event_x (0)
, _last_event_y (0)
, _grabbed_keyboard (false)
, _entered (false)
- , pre_enter_cursor (0)
- , pre_press_cursor (0)
- , pre_note_enter_cursor (0)
- , _note_player (0)
+ , _mouse_changed_selection (false)
{
CANVAS_DEBUG_NAME (_note_group, string_compose ("note group for %1", get_item_name()));
_note_group->raise_to_top();
, _optimization_iterator (_events.end())
, _list_editor (0)
, _no_sound_notes (false)
+ , _last_display_zoom (0)
, _last_event_x (0)
, _last_event_y (0)
, _grabbed_keyboard (false)
, _entered (false)
- , pre_enter_cursor (0)
- , pre_press_cursor (0)
- , pre_note_enter_cursor (0)
- , _note_player (0)
+ , _mouse_changed_selection (false)
{
init (false);
}
, _optimization_iterator (_events.end())
, _list_editor (0)
, _no_sound_notes (false)
+ , _last_display_zoom (0)
, _last_event_x (0)
, _last_event_y (0)
, _grabbed_keyboard (false)
, _entered (false)
- , pre_enter_cursor (0)
- , pre_press_cursor (0)
- , pre_note_enter_cursor (0)
- , _note_player (0)
+ , _mouse_changed_selection (false)
{
init (true);
}
_model = midi_region()->midi_source(0)->model();
_enable_display = false;
- _fill_color_name = "midi frame base";
+ fill_color_name = "midi frame base";
RegionView::init (false);
case GDK_BUTTON_RELEASE:
r = button_release (&ev->button);
- delete _note_player;
- _note_player = 0;
+ _note_player.reset();
return r;
case GDK_MOTION_NOTIFY:
bool
MidiRegionView::leave_notify (GdkEventCrossing*)
{
- _mouse_mode_connection.disconnect ();
-
leave_internal();
_entered = false;
Keyboard::magic_widget_grab_focus();
_grabbed_keyboard = true;
}
+
+ // Lower frame handles below notes so they don't steal events
+ if (frame_handle_start) {
+ frame_handle_start->lower_to_bottom();
+ }
+ if (frame_handle_end) {
+ frame_handle_end->lower_to_bottom();
+ }
}
void
Keyboard::magic_widget_drop_focus();
_grabbed_keyboard = false;
}
+
+ // Raise frame handles above notes so they catch events
+ if (frame_handle_start) {
+ frame_handle_start->raise_to_top();
+ }
+ if (frame_handle_end) {
+ frame_handle_end->raise_to_top();
+ }
}
bool
MouseMode m = editor->current_mouse_mode();
if (m == MouseContent && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier())) {
- pre_press_cursor = editor->get_canvas_cursor ();
- editor->set_canvas_cursor (editor->cursors()->midi_pencil);
+ _press_cursor_ctx = CursorContext::create(*editor, editor->cursors()->midi_pencil);
}
if (_mouse_state != SelectTouchDragging) {
}
_pressed_button = ev->button;
+ _mouse_changed_selection = false;
return true;
}
PublicEditor& editor = trackview.editor ();
- if (pre_press_cursor) {
- dynamic_cast<Editor*>(&editor)->set_canvas_cursor (pre_press_cursor, false);
- pre_press_cursor = 0;
- }
+ _press_cursor_ctx.reset();
switch (_mouse_state) {
case Pressed: // Clicked
case MouseRange:
/* no motion occured - simple click */
clear_selection ();
+ _mouse_changed_selection = true;
break;
case MouseContent:
case MouseTimeFX:
{
clear_selection();
+ _mouse_changed_selection = true;
if (Keyboard::is_insert_note_event(ev)) {
event_y = ev->y;
group->canvas_to_item (event_x, event_y);
- Evoral::MusicalTime beats = get_grid_beats(editor.pixel_to_sample(event_x));
+ Evoral::Beats beats = get_grid_beats(editor.pixel_to_sample(event_x));
/* Shorten the length by 1 tick so that we can add a new note at the next
grid snap without it overlapping this one.
*/
- beats -= Evoral::MusicalTime::tick();
+ beats -= Evoral::Beats::tick();
create_note_at (editor.pixel_to_sample (event_x), event_y, beats, true);
}
}
case MouseDraw:
{
- Evoral::MusicalTime beats = get_grid_beats(editor.pixel_to_sample(event_x));
+ Evoral::Beats beats = get_grid_beats(editor.pixel_to_sample(event_x));
/* Shorten the length by 1 tick so that we can add a new note at the next
grid snap without it overlapping this one.
*/
- beats -= Evoral::MusicalTime::tick();
+ beats -= Evoral::Beats::tick();
create_note_at (editor.pixel_to_sample (event_x), event_y, beats, true);
break;
}
+ if (_mouse_changed_selection) {
+ trackview.editor().begin_reversible_selection_op (_("Mouse Selection Change"));
+ trackview.editor().commit_reversible_selection_op ();
+ }
+
return false;
}
editor.drags()->set (new MidiRubberbandSelectDrag (dynamic_cast<Editor *> (&editor), this), (GdkEvent *) ev);
if (!Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
clear_selection ();
+ _mouse_changed_selection = true;
}
_mouse_state = SelectRectDragging;
return true;
clear_selection();
_mouse_state = None;
- } else if (unmodified && (ev->keyval == GDK_comma || ev->keyval == GDK_period)) {
+ } else if (ev->keyval == GDK_comma || ev->keyval == GDK_period) {
bool start = (ev->keyval == GDK_comma);
bool end = (ev->keyval == GDK_period);
bool shorter = Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier);
bool fine = Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier);
- change_note_lengths (fine, shorter, Evoral::MusicalTime(), start, end);
+ change_note_lengths (fine, shorter, Evoral::Beats(), start, end);
return true;
delete_selection();
return true;
- } else if (ev->keyval == GDK_Tab) {
-
- if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
- goto_previous_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
- } else {
- goto_next_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
- }
- return true;
-
- } else if (ev->keyval == GDK_ISO_Left_Tab) {
+ } else if (ev->keyval == GDK_Tab || ev->keyval == GDK_ISO_Left_Tab) {
- /* Shift-TAB generates ISO Left Tab, for some reason */
+ trackview.editor().begin_reversible_selection_op (_("Select Adjacent Note"));
if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
goto_previous_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
} else {
goto_next_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
}
- return true;
+ trackview.editor().commit_reversible_selection_op();
+ return true;
} else if (ev->keyval == GDK_Up) {
* \param snap_t true to snap t to the grid, otherwise false.
*/
void
-MidiRegionView::create_note_at (framepos_t t, double y, Evoral::MusicalTime length, bool snap_t)
+MidiRegionView::create_note_at (framepos_t t, double y, Evoral::Beats length, bool snap_t)
{
if (length < 2 * DBL_EPSILON) {
return;
}
- MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
- MidiStreamView* const view = mtv->midi_view();
-
- const double note = view->y_to_note(y);
+ MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
+ MidiStreamView* const view = mtv->midi_view();
// Start of note in frames relative to region start
if (snap_t) {
t = snap_frame_to_grid_underneath (t, grid_frames);
}
- const boost::shared_ptr<NoteType> new_note (
- new NoteType (mtv->get_channel_for_add (),
- region_frames_to_region_beats(t + _region->start()),
- length,
- (uint8_t)note, 0x40));
+ const MidiModel::TimeType beat_time = region_frames_to_region_beats(
+ t + _region->start());
+
+ const double note = view->y_to_note(y);
+ const uint8_t chan = mtv->get_channel_for_add();
+ const uint8_t velocity = get_velocity_for_add(beat_time);
+
+ const boost::shared_ptr<NoteType> new_note(
+ new NoteType (chan, beat_time, length, (uint8_t)note, velocity));
if (_model->contains (new_note)) {
return;
view->update_note_range(new_note->note());
- MidiModel::NoteDiffCommand* cmd = _model->new_note_diff_command(_("add note"));
- cmd->add (new_note);
- _model->apply_command(*trackview.session(), cmd);
+ start_note_diff_command(_("add note"));
+
+ clear_selection ();
+ note_diff_add_note (new_note, true, false);
+
+ apply_diff();
play_midi_note (new_note);
}
content_connection.disconnect ();
_model->ContentsChanged.connect (content_connection, invalidator (*this), boost::bind (&MidiRegionView::redisplay_model, this), gui_context());
-
- clear_events ();
+ /* Don't signal as nobody else needs to know until selection has been altered. */
+ clear_events (false);
if (_enable_display) {
redisplay_model();
MidiRegionView::start_note_diff_command (string name)
{
if (!_note_diff_command) {
+ trackview.editor().begin_reversible_command (name);
_note_diff_command = _model->new_note_diff_command (name);
}
}
void
MidiRegionView::note_diff_add_change (NoteBase* ev,
MidiModel::NoteDiffCommand::Property property,
- Evoral::MusicalTime val)
+ Evoral::Beats val)
{
if (_note_diff_command) {
_note_diff_command->change (ev->note(), property, val);
MidiRegionView::apply_diff (bool as_subcommand)
{
bool add_or_remove;
+ bool commit = false;
if (!_note_diff_command) {
return;
_model->apply_command_as_subcommand (*trackview.session(), _note_diff_command);
} else {
_model->apply_command (*trackview.session(), _note_diff_command);
+ commit = true;
}
_note_diff_command = 0;
}
_marked_for_velocity.clear();
+ if (commit) {
+ trackview.editor().commit_reversible_command ();
+ }
}
void
return 0;
}
+/** This version finds any canvas note matching the supplied note. */
+NoteBase*
+MidiRegionView::find_canvas_note (NoteType note)
+{
+ Events::iterator it;
+
+ for (it = _events.begin(); it != _events.end(); ++it) {
+ if (*((*it)->note()) == note) {
+ return *it;
+ }
+ }
+
+ return 0;
+}
+
void
-MidiRegionView::get_events (Events& e, Evoral::Sequence<Evoral::MusicalTime>::NoteOperator op, uint8_t val, int chan_mask)
+MidiRegionView::get_events (Events& e, Evoral::Sequence<Evoral::Beats>::NoteOperator op, uint8_t val, int chan_mask)
{
MidiModel::Notes notes;
_model->get_notes (notes, op, val, chan_mask);
void
MidiRegionView::redisplay_model()
{
- // Don't redisplay the model if we're currently recording and displaying that
if (_active_notes) {
+ // Currently recording
+ const framecnt_t zoom = trackview.editor().get_current_zoom();
+ if (zoom != _last_display_zoom) {
+ /* Update resolved canvas notes to reflect changes in zoom without
+ touching model. Leave active notes (with length 0) alone since
+ they are being extended. */
+ for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
+ if ((*i)->note()->length() > 0) {
+ update_note(*i);
+ }
+ }
+ _last_display_zoom = zoom;
+ }
return;
}
if (!empty_when_starting && (cne = find_canvas_note (note)) != 0) {
cne->validate ();
-
- Note* cn;
- Hit* ch;
-
- if ((cn = dynamic_cast<Note*>(cne)) != 0) {
- update_note (cn);
- } else if ((ch = dynamic_cast<Hit*>(cne)) != 0) {
- update_hit (ch);
- }
+ update_note (cne);
if (visible) {
cne->show ();
} else {
- add_note (note, visible);
+ cne = add_note (note, visible);
+ }
+
+ set<boost::shared_ptr<NoteType> >::iterator it;
+ for (it = _pending_note_selection.begin(); it != _pending_note_selection.end(); ++it) {
+ if (*(*it) == *note) {
+ add_to_selection (cne);
+ }
}
} else {
}
}
-
/* remove note items that are no longer valid */
if (!empty_when_starting) {
_marked_for_selection.clear ();
_marked_for_velocity.clear ();
+ _pending_note_selection.clear ();
/* we may have caused _events to contain things out of order (e.g. if a note
moved earlier or later). we don't generally need them in time order, but
bool have_periodic_system_messages = false;
bool display_periodic_messages = true;
- if (!Config->get_never_display_periodic_midi()) {
+ if (!ARDOUR_UI::config()->get_never_display_periodic_midi()) {
for (MidiModel::SysExes::const_iterator i = _model->sysexes().begin(); i != _model->sysexes().end(); ++i) {
- const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::MusicalTime> > mev =
- boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::MusicalTime> > (*i);
+ const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::Beats> > mev =
+ boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::Beats> > (*i);
if (mev) {
if (mev->is_spp() || mev->is_mtc_quarter() || mev->is_mtc_full()) {
for (MidiModel::SysExes::const_iterator i = _model->sysexes().begin(); i != _model->sysexes().end(); ++i) {
- const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::MusicalTime> > mev =
- boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::MusicalTime> > (*i);
+ const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::Beats> > mev =
+ boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::Beats> > (*i);
- Evoral::MusicalTime time = (*i)->time();
+ Evoral::Beats time = (*i)->time();
if (mev) {
if (mev->is_spp() || mev->is_mtc_quarter() || mev->is_mtc_full()) {
GhostRegion*
MidiRegionView::add_ghost (TimeAxisView& tv)
{
- Note* note;
-
double unit_position = _region->position () / samples_per_pixel;
MidiTimeAxisView* mtv = dynamic_cast<MidiTimeAxisView*>(&tv);
MidiGhostRegion* ghost;
}
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
- if ((note = dynamic_cast<Note*>(*i)) != 0) {
- ghost->add_note(note);
- }
+ ghost->add_note(*i);
}
ghost->set_height ();
/** Resolve an active MIDI note (while recording).
*/
void
-MidiRegionView::resolve_note(uint8_t note, Evoral::MusicalTime end_time)
+MidiRegionView::resolve_note(uint8_t note, Evoral::Beats end_time)
{
if (midi_view()->note_mode() != Sustained) {
return;
}
if (_active_notes && _active_notes[note]) {
+ /* Set note length so update_note() works. Note this is a local note
+ for recording, not from a model, so we can safely mess with it. */
+ _active_notes[note]->note()->set_length(
+ end_time - _active_notes[note]->note()->time());
- /* XXX is end_time really region-centric? I think so, because
- this is a new region that we're recording, so source zero is
- the same as region zero
- */
+ /* End time is relative to the region being recorded. */
const framepos_t end_time_frames = region_beats_to_region_frames(end_time);
_active_notes[note]->set_x1 (trackview.editor().sample_to_pixel(end_time_frames));
_active_notes[note]->set_outline_all ();
_active_notes[note] = 0;
-
}
}
return;
}
- for (unsigned i=0; i < 128; ++i) {
+ for (unsigned i = 0; i < 128; ++i) {
if (_active_notes[i]) {
_active_notes[i]->set_x1(
- trackview.editor().sample_to_pixel(_region->position() + _region->length()));
+ trackview.editor().sample_to_pixel(_region->length()));
}
}
}
-
void
MidiRegionView::play_midi_note(boost::shared_ptr<NoteType> note)
{
- if (_no_sound_notes || !Config->get_sound_midi_notes()) {
+ if (_no_sound_notes || !ARDOUR_UI::config()->get_sound_midi_notes()) {
return;
}
void
MidiRegionView::start_playing_midi_note(boost::shared_ptr<NoteType> note)
{
- if (_no_sound_notes || !Config->get_sound_midi_notes()) {
- return;
- }
-
- RouteUI* route_ui = dynamic_cast<RouteUI*> (&trackview);
-
- if (!route_ui || !route_ui->midi_track()) {
- return;
- }
-
- delete _note_player;
- _note_player = new NotePlayer (route_ui->midi_track ());
- _note_player->add (note);
- _note_player->on ();
+ const std::vector< boost::shared_ptr<NoteType> > notes(1, note);
+ start_playing_midi_chord(notes);
}
void
MidiRegionView::start_playing_midi_chord (vector<boost::shared_ptr<NoteType> > notes)
{
- if (_no_sound_notes || !Config->get_sound_midi_notes()) {
+ if (_no_sound_notes || !ARDOUR_UI::config()->get_sound_midi_notes()) {
return;
}
return;
}
- delete _note_player;
- _note_player = new NotePlayer (route_ui->midi_track());
+ _note_player = boost::shared_ptr<NotePlayer>(new NotePlayer(route_ui->midi_track()));
for (vector<boost::shared_ptr<NoteType> >::iterator n = notes.begin(); n != notes.end(); ++n) {
_note_player->add (*n);
bool
MidiRegionView::note_in_region_range (const boost::shared_ptr<NoteType> note, bool& visible) const
{
+ /* This is imprecise due to all the conversion conversion involved, so only
+ hide notes if they seem to start more than one tick before the start. */
+ const framecnt_t tick_frames = Evoral::Beats::tick().to_ticks(trackview.session()->frame_rate());
const framepos_t note_start_frames = source_beats_to_region_frames (note->time());
- bool outside = (note_start_frames < 0) || (note_start_frames > _region->last_frame());
+ const bool outside = ((note_start_frames <= -tick_frames) ||
+ (note_start_frames > _region->last_frame()));
visible = (note->note() >= midi_stream_view()->lowest_note()) &&
(note->note() <= midi_stream_view()->highest_note());
return !outside;
}
+void
+MidiRegionView::update_note (NoteBase* note, bool update_ghost_regions)
+{
+ Note* sus = NULL;
+ Hit* hit = NULL;
+ if ((sus = dynamic_cast<Note*>(note))) {
+ update_sustained(sus, update_ghost_regions);
+ } else if ((hit = dynamic_cast<Hit*>(note))) {
+ update_hit(hit, update_ghost_regions);
+ }
+}
+
/** Update a canvas note's size from its model note.
* @param ev Canvas note to update.
* @param update_ghost_regions true to update the note in any ghost regions that we have, otherwise false.
*/
void
-MidiRegionView::update_note (Note* ev, bool update_ghost_regions)
+MidiRegionView::update_sustained (Note* ev, bool update_ghost_regions)
{
boost::shared_ptr<NoteType> note = ev->note();
const double x = trackview.editor().sample_to_pixel (source_beats_to_region_frames (note->time()));
if (!note->length()) {
if (_active_notes && note->note() < 128) {
- // If this note is already active there's a stuck note,
- // finish the old note rectangle
- if (_active_notes[note->note()]) {
- Note* const old_rect = _active_notes[note->note()];
- boost::shared_ptr<NoteType> old_note = old_rect->note();
+ Note* const old_rect = _active_notes[note->note()];
+ if (old_rect) {
+ /* There is an active note on this key, so we have a stuck
+ note. Finish the old rectangle here. */
old_rect->set_x1 (x);
old_rect->set_outline_all ();
}
/* outline all edges */
ev->set_outline_all ();
}
-
+
+ // Update color in case velocity has changed
+ ev->set_fill_color(ev->base_color());
+ ev->set_outline_color(ev->calculate_outline(ev->base_color(), ev->selected()));
+
if (update_ghost_regions) {
for (std::vector<GhostRegion*>::iterator i = ghosts.begin(); i != ghosts.end(); ++i) {
MidiGhostRegion* gr = dynamic_cast<MidiGhostRegion*> (*i);
}
void
-MidiRegionView::update_hit (Hit* ev)
+MidiRegionView::update_hit (Hit* ev, bool update_ghost_regions)
{
boost::shared_ptr<NoteType> note = ev->note();
ev->set_position (ArdourCanvas::Duple (x, y));
ev->set_height (diamond_size);
+
+ // Update color in case velocity has changed
+ ev->set_fill_color(ev->base_color());
+ ev->set_outline_color(ev->calculate_outline(ev->base_color(), ev->selected()));
+
+ if (update_ghost_regions) {
+ for (std::vector<GhostRegion*>::iterator i = ghosts.begin(); i != ghosts.end(); ++i) {
+ MidiGhostRegion* gr = dynamic_cast<MidiGhostRegion*> (*i);
+ if (gr) {
+ gr->update_note (ev);
+ }
+ }
+ }
}
/** Add a MIDI note to the view (with length).
* notes, and resolve_note should be called when the corresponding note off
* event arrives, to properly display the note.
*/
-void
+NoteBase*
MidiRegionView::add_note(const boost::shared_ptr<NoteType> note, bool visible)
{
NoteBase* event = 0;
Note* ev_rect = new Note (*this, _note_group, note);
- update_note (ev_rect);
+ update_sustained (ev_rect);
event = ev_rect;
- MidiGhostRegion* gr;
-
- for (std::vector<GhostRegion*>::iterator g = ghosts.begin(); g != ghosts.end(); ++g) {
- if ((gr = dynamic_cast<MidiGhostRegion*>(*g)) != 0) {
- gr->add_note(ev_rect);
- }
- }
-
} else if (midi_view()->note_mode() == Percussive) {
const double diamond_size = std::max(1., floor(midi_stream_view()->note_height()) - 2.);
}
if (event) {
+ MidiGhostRegion* gr;
+
+ for (std::vector<GhostRegion*>::iterator g = ghosts.begin(); g != ghosts.end(); ++g) {
+ if ((gr = dynamic_cast<MidiGhostRegion*>(*g)) != 0) {
+ gr->add_note(event);
+ }
+ }
+
if (_marked_for_selection.find(note) != _marked_for_selection.end()) {
note_selected(event, true);
}
MidiStreamView* const view = mtv->midi_view();
view->update_note_range (note->note());
+ return event;
}
void
MidiRegionView::step_add_note (uint8_t channel, uint8_t number, uint8_t velocity,
- Evoral::MusicalTime pos, Evoral::MusicalTime len)
+ Evoral::Beats pos, Evoral::Beats len)
{
boost::shared_ptr<NoteType> new_note (new NoteType (channel, pos, len, number, velocity));
view->update_note_range(new_note->note());
_marked_for_selection.clear ();
- clear_selection ();
start_note_diff_command (_("step add"));
+
+ clear_selection ();
note_diff_add_note (new_note, true, false);
+
apply_diff();
// last_step_edit_note = new_note;
}
void
-MidiRegionView::step_sustain (Evoral::MusicalTime beats)
+MidiRegionView::step_sustain (Evoral::Beats beats)
{
change_note_lengths (false, false, beats, false, true);
}
/// Return true iff @p pc applies to the given time on the given channel.
static bool
-patch_applies (const ARDOUR::MidiModel::constPatchChangePtr pc, Evoral::MusicalTime time, uint8_t channel)
+patch_applies (const ARDOUR::MidiModel::constPatchChangePtr pc, Evoral::Beats time, uint8_t channel)
{
return pc->time() <= time && pc->channel() == channel;
}
void
-MidiRegionView::get_patch_key_at (Evoral::MusicalTime time, uint8_t channel, MIDI::Name::PatchPrimaryKey& key) const
+MidiRegionView::get_patch_key_at (Evoral::Beats time, uint8_t channel, MIDI::Name::PatchPrimaryKey& key) const
{
// The earliest event not before time
MidiModel::PatchChanges::iterator i = _model->patch_change_lower_bound (time);
void
MidiRegionView::change_patch_change (PatchChange& pc, const MIDI::Name::PatchPrimaryKey& new_patch)
{
- MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("alter patch change"));
+ string name = _("alter patch change");
+ trackview.editor().begin_reversible_command (name);
+ MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (name);
if (pc.patch()->program() != new_patch.program()) {
c->change_program (pc.patch (), new_patch.program());
}
_model->apply_command (*trackview.session(), c);
+ trackview.editor().commit_reversible_command ();
_patch_changes.clear ();
display_patch_changes ();
}
void
-MidiRegionView::change_patch_change (MidiModel::PatchChangePtr old_change, const Evoral::PatchChange<Evoral::MusicalTime> & new_change)
+MidiRegionView::change_patch_change (MidiModel::PatchChangePtr old_change, const Evoral::PatchChange<Evoral::Beats> & new_change)
{
- MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("alter patch change"));
+ string name = _("alter patch change");
+ trackview.editor().begin_reversible_command (name);
+ MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (name);
if (old_change->time() != new_change.time()) {
c->change_time (old_change, new_change.time());
}
_model->apply_command (*trackview.session(), c);
+ trackview.editor().commit_reversible_command ();
_patch_changes.clear ();
display_patch_changes ();
* MidiTimeAxisView::get_channel_for_add())
*/
void
-MidiRegionView::add_patch_change (framecnt_t t, Evoral::PatchChange<Evoral::MusicalTime> const & patch)
+MidiRegionView::add_patch_change (framecnt_t t, Evoral::PatchChange<Evoral::Beats> const & patch)
{
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
+ string name = _("add patch change");
- MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("add patch change"));
+ trackview.editor().begin_reversible_command (name);
+ MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (name);
c->add (MidiModel::PatchChangePtr (
- new Evoral::PatchChange<Evoral::MusicalTime> (
+ new Evoral::PatchChange<Evoral::Beats> (
absolute_frames_to_source_beats (_region->position() + t),
mtv->get_channel_for_add(), patch.program(), patch.bank()
)
);
_model->apply_command (*trackview.session(), c);
+ trackview.editor().commit_reversible_command ();
_patch_changes.clear ();
display_patch_changes ();
}
void
-MidiRegionView::move_patch_change (PatchChange& pc, Evoral::MusicalTime t)
+MidiRegionView::move_patch_change (PatchChange& pc, Evoral::Beats t)
{
+ trackview.editor().begin_reversible_command (_("move patch change"));
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("move patch change"));
c->change_time (pc.patch (), t);
_model->apply_command (*trackview.session(), c);
+ trackview.editor().commit_reversible_command ();
_patch_changes.clear ();
display_patch_changes ();
void
MidiRegionView::delete_patch_change (PatchChange* pc)
{
+ trackview.editor().begin_reversible_command (_("delete patch change"));
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("delete patch change"));
c->remove (pc->patch ());
_model->apply_command (*trackview.session(), c);
+ trackview.editor().commit_reversible_command ();
_patch_changes.clear ();
display_patch_changes ();
}
}
+/** Used for selection undo/redo.
+ The requested notes most likely won't exist in the view until the next model redisplay.
+*/
+void
+MidiRegionView::select_notes (list<boost::shared_ptr<NoteType> > notes)
+{
+ NoteBase* cne;
+ list<boost::shared_ptr<NoteType> >::iterator n;
+
+ for (n = notes.begin(); n != notes.end(); ++n) {
+ if ((cne = find_canvas_note(*(*n))) != 0) {
+ add_to_selection (cne);
+ } else {
+ _pending_note_selection.insert(*n);
+ }
+ }
+}
+
void
MidiRegionView::select_matching_notes (uint8_t notenum, uint16_t channel_mask, bool add, bool extend)
{
} else {
/* find end of latest note selected, select all between that and the start of "ev" */
- Evoral::MusicalTime earliest = Evoral::MaxMusicalTime;
- Evoral::MusicalTime latest = Evoral::MusicalTime();
+ Evoral::Beats earliest = Evoral::MaxBeats;
+ Evoral::Beats latest = Evoral::Beats();
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
if ((*i)->note()->end_time() > latest) {
{
typedef vector<boost::shared_ptr<NoteType> > PossibleChord;
PossibleChord to_play;
- Evoral::MusicalTime earliest = Evoral::MaxMusicalTime;
+ Evoral::Beats earliest = Evoral::MaxBeats;
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
if ((*i)->note()->time() < earliest) {
(*i)->move_event(dx, dy);
}
- if (dy && !_selection.empty() && !_no_sound_notes && Config->get_sound_midi_notes()) {
+ if (dy && !_selection.empty() && !_no_sound_notes && ARDOUR_UI::config()->get_sound_midi_notes()) {
if (to_play.size() > 1) {
for (Selection::iterator i = _selection.begin(); i != _selection.end() ; ++i) {
framepos_t new_frames = source_beats_to_absolute_frames ((*i)->note()->time()) + dt;
- Evoral::MusicalTime new_time = absolute_frames_to_source_beats (new_frames);
+ Evoral::Beats new_time = absolute_frames_to_source_beats (new_frames);
if (new_time < 0) {
continue;
}
framepos_t
-MidiRegionView::source_beats_to_absolute_frames(Evoral::MusicalTime beats) const
+MidiRegionView::source_beats_to_absolute_frames(Evoral::Beats beats) const
{
/* the time converter will return the frame corresponding to `beats'
relative to the start of the source. The start of the source
return source_start + _source_relative_time_converter.to (beats);
}
-Evoral::MusicalTime
+Evoral::Beats
MidiRegionView::absolute_frames_to_source_beats(framepos_t frames) const
{
/* the `frames' argument needs to be converted into a frame count
}
framepos_t
-MidiRegionView::region_beats_to_region_frames(Evoral::MusicalTime beats) const
+MidiRegionView::region_beats_to_region_frames(Evoral::Beats beats) const
{
return _region_relative_time_converter.to(beats);
}
-Evoral::MusicalTime
+Evoral::Beats
MidiRegionView::region_frames_to_region_beats(framepos_t frames) const
{
return _region_relative_time_converter.from(frames);
}
if (!cursor_set) {
- const double snapped_x = snap_pixel_to_sample (current_x);
- Evoral::MusicalTime beats = region_frames_to_region_beats (snapped_x);
- Evoral::MusicalTime len = Evoral::MusicalTime();
+ const double snapped_x = snap_pixel_to_sample (current_x);
+ Evoral::Beats beats = region_frames_to_region_beats (snapped_x);
+ Evoral::Beats len = Evoral::Beats();
if (at_front) {
if (beats < canvas_note->note()->end_time()) {
}
}
+ len = std::max(Evoral::Beats(1 / 512.0), len);
+
char buf[16];
snprintf (buf, sizeof (buf), "%.3g beats", len.to_double());
show_verbose_cursor (buf, 0, 0);
void
MidiRegionView::commit_resizing (NoteBase* primary, bool at_front, double delta_x, bool relative)
{
- start_note_diff_command (_("resize notes"));
+ _note_diff_command = _model->new_note_diff_command (_("resize notes"));
for (std::vector<NoteResizeData *>::iterator i = _resize_data.begin(); i != _resize_data.end(); ++i) {
Note* canvas_note = (*i)->note;
current_x = snap_pixel_to_sample (current_x) + _region->start ();
/* and then to beats */
- const Evoral::MusicalTime x_beats = region_frames_to_region_beats (current_x);
+ const Evoral::Beats x_beats = region_frames_to_region_beats (current_x);
if (at_front && x_beats < canvas_note->note()->end_time()) {
note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::StartTime, x_beats);
- Evoral::MusicalTime len = canvas_note->note()->time() - x_beats;
+ Evoral::Beats len = canvas_note->note()->time() - x_beats;
len += canvas_note->note()->length();
if (!!len) {
}
if (!at_front) {
- const Evoral::MusicalTime len = x_beats - canvas_note->note()->time();
-
- if (!!len) {
- /* XXX convert to beats */
- note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::Length, len);
- }
+ const Evoral::Beats len = std::max(Evoral::Beats(1 / 512.0),
+ x_beats - canvas_note->note()->time());
+ note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::Length, len);
}
delete resize_rect;
}
_resize_data.clear();
- apply_diff();
+ apply_diff(true);
}
void
}
void
-MidiRegionView::trim_note (NoteBase* event, Evoral::MusicalTime front_delta, Evoral::MusicalTime end_delta)
+MidiRegionView::trim_note (NoteBase* event, Evoral::Beats front_delta, Evoral::Beats end_delta)
{
bool change_start = false;
bool change_length = false;
- Evoral::MusicalTime new_start;
- Evoral::MusicalTime new_length;
+ Evoral::Beats new_start;
+ Evoral::Beats new_length;
/* NOTE: the semantics of the two delta arguments are slightly subtle:
if (front_delta < 0) {
if (event->note()->time() < -front_delta) {
- new_start = Evoral::MusicalTime();
+ new_start = Evoral::Beats();
} else {
new_start = event->note()->time() + front_delta; // moves earlier
}
} else {
- Evoral::MusicalTime new_pos = event->note()->time() + front_delta;
+ Evoral::Beats new_pos = event->note()->time() + front_delta;
if (new_pos < event->note()->end_time()) {
new_start = event->note()->time() + front_delta;
}
void
-MidiRegionView::change_note_time (NoteBase* event, Evoral::MusicalTime delta, bool relative)
+MidiRegionView::change_note_time (NoteBase* event, Evoral::Beats delta, bool relative)
{
- Evoral::MusicalTime new_time;
+ Evoral::Beats new_time;
if (relative) {
if (delta < 0.0) {
if (event->note()->time() < -delta) {
- new_time = Evoral::MusicalTime();
+ new_time = Evoral::Beats();
} else {
new_time = event->note()->time() + delta;
}
}
void
-MidiRegionView::change_note_length (NoteBase* event, Evoral::MusicalTime t)
+MidiRegionView::change_note_length (NoteBase* event, Evoral::Beats t)
{
note_diff_add_change (event, MidiModel::NoteDiffCommand::Length, t);
}
}
void
-MidiRegionView::change_note_lengths (bool fine, bool shorter, Evoral::MusicalTime delta, bool start, bool end)
+MidiRegionView::change_note_lengths (bool fine, bool shorter, Evoral::Beats delta, bool start, bool end)
{
if (!delta) {
if (fine) {
- delta = Evoral::MusicalTime(1.0/128.0);
+ delta = Evoral::Beats(1.0/128.0);
} else {
/* grab the current grid distance */
delta = get_grid_beats(_region->position());
/* note the negation of the delta for start */
trim_note (*i,
- (start ? -delta : Evoral::MusicalTime()),
- (end ? delta : Evoral::MusicalTime()));
+ (start ? -delta : Evoral::Beats()),
+ (end ? delta : Evoral::Beats()));
i = next;
}
into a vector and sort before using the first one.
*/
- const framepos_t ref_point = source_beats_to_absolute_frames ((*(_selection.begin()))->note()->time());
- Evoral::MusicalTime delta;
+ const framepos_t ref_point = source_beats_to_absolute_frames ((*(_selection.begin()))->note()->time());
+ Evoral::Beats delta;
if (!fine) {
/* non-fine, move by 1 bar regardless of snap */
- delta = Evoral::MusicalTime(trackview.session()->tempo_map().meter_at(ref_point).divisions_per_bar());
+ delta = Evoral::Beats(trackview.session()->tempo_map().meter_at(ref_point).divisions_per_bar());
} else if (trackview.editor().snap_mode() == Editing::SnapOff) {
{
Editor* editor = dynamic_cast<Editor*>(&trackview.editor());
- pre_note_enter_cursor = editor->get_canvas_cursor ();
-
if (_mouse_state == SelectTouchDragging) {
note_selected (ev, true);
+ } else if (editor->current_mouse_mode() == MouseContent) {
+ show_verbose_cursor (ev->note ());
+ } else if (editor->current_mouse_mode() == MouseDraw) {
+ show_verbose_cursor (ev->note ());
}
-
- show_verbose_cursor (ev->note ());
}
void
}
editor->verbose_cursor()->hide ();
-
- if (pre_note_enter_cursor) {
- editor->set_canvas_cursor (pre_note_enter_cursor);
- pre_note_enter_cursor = 0;
- }
}
void
Editing::MouseMode mm = editor->current_mouse_mode();
bool trimmable = (mm == MouseContent || mm == MouseTimeFX || mm == MouseDraw);
- if (can_set_cursor) {
+ Editor::EnterContext* ctx = editor->get_enter_context(NoteItem);
+ if (can_set_cursor && ctx) {
if (trimmable && x_fraction > 0.0 && x_fraction < 0.2) {
- editor->set_canvas_cursor (editor->cursors()->left_side_trim);
+ ctx->cursor_ctx->change(editor->cursors()->left_side_trim);
} else if (trimmable && x_fraction >= 0.8 && x_fraction < 1.0) {
- editor->set_canvas_cursor (editor->cursors()->right_side_trim);
- } else if (pre_note_enter_cursor) {
- editor->set_canvas_cursor (pre_note_enter_cursor);
+ ctx->cursor_ctx->change(editor->cursors()->right_side_trim);
+ } else {
+ ctx->cursor_ctx->change(editor->cursors()->grabber_note);
}
}
}
uint32_t
-MidiRegionView::fill_opacity() const
+MidiRegionView::get_fill_color() const
{
- uint32_t a = RegionView::fill_opacity();
- if (trackview.editor().current_mouse_mode() == MouseDraw ||
- trackview.editor().current_mouse_mode() == MouseContent) {
- /* Make rect more transparent when in an internal mode. This should
- probably be configurable somehow. */
- a /= 2;
- }
- return a;
-}
-
-void
-MidiRegionView::set_frame_color()
-{
- uint32_t f;
-
- TimeAxisViewItem::set_frame_color ();
-
- if (!frame) {
- return;
- }
-
+ const std::string mod_name = (_dragging ? "dragging region" :
+ trackview.editor().internal_editing() ? "editable region" :
+ "midi frame base");
if (_selected) {
- f = ARDOUR_UI::config()->color ("selected region base");
- } else if (high_enough_for_name || !ARDOUR_UI::config()->get_color_regions_using_track_color()) {
- f = ARDOUR_UI::config()->color_mod ("midi frame base", "midi frame base");
- } else {
- f = fill_color;
+ return ARDOUR_UI::config()->color_mod ("selected region base", mod_name);
+ } else if ((!ARDOUR_UI::config()->get_show_name_highlight() || high_enough_for_name) &&
+ !ARDOUR_UI::config()->get_color_regions_using_track_color()) {
+ return ARDOUR_UI::config()->color_mod ("midi frame base", mod_name);
}
-
- f = UINT_RGBA_CHANGE_A (f, fill_opacity());
-
- frame->set_fill_color (f);
+ return ARDOUR_UI::config()->color_mod (fill_color, mod_name);
}
void
bool
MidiRegionView::paste (framepos_t pos, const ::Selection& selection, PasteContext& ctx)
{
+ bool commit = false;
// Paste notes, if available
MidiNoteSelection::const_iterator m = selection.midi_notes.get_nth(ctx.counts.n_notes());
if (m != selection.midi_notes.end()) {
ctx.counts.increase_n_notes();
+ if (!(*m)->empty()) { commit = true; }
paste_internal(pos, ctx.count, ctx.times, **m);
}
typedef RouteTimeAxisView::AutomationTracks ATracks;
const ATracks& atracks = midi_view()->automation_tracks();
for (ATracks::const_iterator a = atracks.begin(); a != atracks.end(); ++a) {
- a->second->paste(pos, selection, ctx);
+ if (a->second->paste(pos, selection, ctx)) {
+ commit = true;
+ }
}
+ if (commit) {
+ trackview.editor().commit_reversible_command ();
+ }
return true;
}
start_note_diff_command (_("paste"));
- const Evoral::MusicalTime snap_beats = get_grid_beats(pos);
- const Evoral::MusicalTime first_time = (*mcb.notes().begin())->time();
- const Evoral::MusicalTime last_time = (*mcb.notes().rbegin())->end_time();
- const Evoral::MusicalTime duration = last_time - first_time;
- const Evoral::MusicalTime snap_duration = duration.snap_to(snap_beats);
- const Evoral::MusicalTime paste_offset = snap_duration * paste_count;
- const Evoral::MusicalTime pos_beats = absolute_frames_to_source_beats(pos) + paste_offset;
- Evoral::MusicalTime end_point = Evoral::MusicalTime();
+ const Evoral::Beats snap_beats = get_grid_beats(pos);
+ const Evoral::Beats first_time = (*mcb.notes().begin())->time();
+ const Evoral::Beats last_time = (*mcb.notes().rbegin())->end_time();
+ const Evoral::Beats duration = last_time - first_time;
+ const Evoral::Beats snap_duration = duration.snap_to(snap_beats);
+ const Evoral::Beats paste_offset = snap_duration * paste_count;
+ const Evoral::Beats pos_beats = absolute_frames_to_source_beats(pos) + paste_offset;
+ Evoral::Beats end_point = Evoral::Beats();
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("Paste data spans from %1 to %2 (%3) ; paste pos beats = %4 (based on %5 - %6)\n",
first_time,
framecnt_t grid_frames;
framepos_t const f = snap_frame_to_grid_underneath (unsnapped_frame, grid_frames);
- /* use region_frames... because we are converting a delta within the region
- */
-
- const Evoral::MusicalTime length = get_grid_beats(unsnapped_frame);
+ /* calculate time in beats relative to start of source */
+ const Evoral::Beats length = get_grid_beats(unsnapped_frame);
+ const Evoral::Beats time = std::max(
+ Evoral::Beats(),
+ absolute_frames_to_source_beats (f + _region->position ()));
- /* note that this sets the time of the ghost note in beats relative to
- the start of the source; that is how all note times are stored.
- */
- _ghost_note->note()->set_time (
- std::max(Evoral::MusicalTime(),
- absolute_frames_to_source_beats (f + _region->position ())));
+ _ghost_note->note()->set_time (time);
_ghost_note->note()->set_length (length);
_ghost_note->note()->set_note (midi_stream_view()->y_to_note (y));
_ghost_note->note()->set_channel (mtv->get_channel_for_add ());
+ _ghost_note->note()->set_velocity (get_velocity_for_add (time));
/* the ghost note does not appear in ghost regions, so pass false in here */
update_note (_ghost_note, false);
remove_ghost_note ();
boost::shared_ptr<NoteType> g (new NoteType);
- _ghost_note = new Note (*this, _note_group, g);
+ if (midi_view()->note_mode() == Sustained) {
+ _ghost_note = new Note (*this, _note_group, g);
+ } else {
+ _ghost_note = new Hit (*this, _note_group, 10, g);
+ }
_ghost_note->set_ignore_events (true);
_ghost_note->set_outline_color (0x000000aa);
update_ghost_note (x, y);
uint16_t chn_mask = mtv->midi_track()->get_playback_channel_mask();
if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
- get_events (e, Evoral::Sequence<Evoral::MusicalTime>::PitchGreaterThanOrEqual, (uint8_t) floor (note), chn_mask);
+ get_events (e, Evoral::Sequence<Evoral::Beats>::PitchGreaterThanOrEqual, (uint8_t) floor (note), chn_mask);
} else if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
- get_events (e, Evoral::Sequence<Evoral::MusicalTime>::PitchLessThanOrEqual, (uint8_t) floor (note), chn_mask);
+ get_events (e, Evoral::Sequence<Evoral::Beats>::PitchLessThanOrEqual, (uint8_t) floor (note), chn_mask);
} else {
return;
}
}
void
-MidiRegionView::show_step_edit_cursor (Evoral::MusicalTime pos)
+MidiRegionView::show_step_edit_cursor (Evoral::Beats pos)
{
if (_step_edit_cursor == 0) {
ArdourCanvas::Item* const group = get_canvas_group();
}
void
-MidiRegionView::move_step_edit_cursor (Evoral::MusicalTime pos)
+MidiRegionView::move_step_edit_cursor (Evoral::Beats pos)
{
_step_edit_cursor_position = pos;
}
void
-MidiRegionView::set_step_edit_cursor_width (Evoral::MusicalTime beats)
+MidiRegionView::set_step_edit_cursor_width (Evoral::Beats beats)
{
_step_edit_cursor_width = beats;
}
/* convert from session frames to source beats */
- Evoral::MusicalTime const time_beats = _source_relative_time_converter.from(ev.time());
+ Evoral::Beats const time_beats = _source_relative_time_converter.from(
+ ev.time() - src->timeline_position() + _region->start());
if (ev.type() == MIDI_CMD_NOTE_ON) {
boost::shared_ptr<NoteType> note (
- new NoteType (ev.channel(), time_beats, Evoral::MusicalTime(), ev.note(), ev.velocity()));
+ new NoteType (ev.channel(), time_beats, Evoral::Beats(), ev.note(), ev.velocity()));
add_note (note, true);
trackview.editor().verbose_cursor()->set_offset (ArdourCanvas::Duple (xoffset, yoffset));
}
+uint8_t
+MidiRegionView::get_velocity_for_add (MidiModel::TimeType time) const
+{
+ if (_model->notes().empty()) {
+ return 0x40; // No notes, use default
+ }
+
+ MidiModel::Notes::const_iterator m = _model->note_lower_bound(time);
+ if (m == _model->notes().begin()) {
+ // Before the start, use the velocity of the first note
+ return (*m)->velocity();
+ } else if (m == _model->notes().end()) {
+ // Past the end, use the velocity of the last note
+ --m;
+ return (*m)->velocity();
+ }
+
+ // Interpolate velocity of surrounding notes
+ MidiModel::Notes::const_iterator n = m;
+ --n;
+
+ const double frac = ((time - (*n)->time()).to_double() /
+ ((*m)->time() - (*n)->time()).to_double());
+
+ return (*n)->velocity() + (frac * ((*m)->velocity() - (*n)->velocity()));
+}
+
/** @param p A session framepos.
* @param grid_frames Filled in with the number of frames that a grid interval is at p.
* @return p snapped to the grid subdivision underneath it.
{
PublicEditor& editor = trackview.editor ();
- const Evoral::MusicalTime grid_beats = get_grid_beats(p);
+ const Evoral::Beats grid_beats = get_grid_beats(p);
grid_frames = region_beats_to_region_frames (grid_beats);
void
MidiRegionView::note_button_release ()
{
- delete _note_player;
- _note_player = 0;
+ _note_player.reset();
}
ChannelMode
}
-Evoral::MusicalTime
+Evoral::Beats
MidiRegionView::get_grid_beats(framepos_t pos) const
{
- PublicEditor& editor = trackview.editor();
- bool success = false;
- Evoral::MusicalTime beats = editor.get_grid_type_as_beats(success, pos);
+ PublicEditor& editor = trackview.editor();
+ bool success = false;
+ Evoral::Beats beats = editor.get_grid_type_as_beats(success, pos);
if (!success) {
- beats = Evoral::MusicalTime(1);
+ beats = Evoral::Beats(1);
}
return beats;
}