1 /* 2 * Copyright (C) 2018-2021 Alexandros Theodotou <alex at zrythm dot org> 3 * 4 * This file is part of Zrythm 5 * 6 * Zrythm is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU Affero General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * Zrythm is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU Affero General Public License for more details. 15 * 16 * You should have received a copy of the GNU Affero General Public License 17 * along with Zrythm. If not, see <https://www.gnu.org/licenses/>. 18 */ 19 20 #include "actions/arranger_selections.h" 21 #include "audio/channel.h" 22 #include "audio/instrument_track.h" 23 #include "audio/midi_region.h" 24 #include "audio/track.h" 25 #include "audio/tracklist.h" 26 #include "audio/transport.h" 27 #include "audio/velocity.h" 28 #include "gui/backend/event.h" 29 #include "gui/backend/event_manager.h" 30 #include "gui/widgets/arranger.h" 31 #include "gui/widgets/bot_dock_edge.h" 32 #include "gui/widgets/center_dock.h" 33 #include "gui/widgets/clip_editor.h" 34 #include "gui/widgets/clip_editor_inner.h" 35 #include "gui/widgets/color_area.h" 36 #include "gui/widgets/dialogs/arranger_object_info.h" 37 #include "gui/widgets/editor_ruler.h" 38 #include "gui/widgets/main_window.h" 39 #include "gui/widgets/midi_arranger.h" 40 #include "gui/widgets/midi_editor_space.h" 41 #include "gui/widgets/midi_modifier_arranger.h" 42 #include "gui/widgets/midi_note.h" 43 #include "gui/widgets/piano_roll_keys.h" 44 #include "gui/widgets/region.h" 45 #include "gui/widgets/ruler.h" 46 #include "gui/widgets/timeline_bg.h" 47 #include "gui/widgets/timeline_ruler.h" 48 #include "gui/widgets/track.h" 49 #include "gui/widgets/tracklist.h" 50 #include "project.h" 51 #include "utils/arrays.h" 52 #include "utils/gtk.h" 53 #include "utils/flags.h" 54 #include "utils/math.h" 55 #include "utils/ui.h" 56 #include "utils/resources.h" 57 #include "settings/settings.h" 58 #include "zrythm.h" 59 #include "zrythm_app.h" 60 61 #include <gtk/gtk.h> 62 #include <glib/gi18n.h> 63 64 /** 65 * Called on drag begin in parent when background is 66 * double clicked (i.e., a note is created). 67 * @param pos The absolute position in the piano 68 * roll. 69 */ 70 void 71 midi_arranger_widget_create_note ( 72 ArrangerWidget * self, 73 Position * pos, 74 int note, 75 ZRegion * region) 76 { 77 ArrangerObject * region_obj = 78 (ArrangerObject *) region; 79 80 /* get local pos */ 81 Position local_pos; 82 position_from_ticks ( 83 &local_pos, 84 pos->ticks - 85 region_obj->pos.ticks); 86 87 /* set action */ 88 bool autofilling = 89 self->action == UI_OVERLAY_ACTION_AUTOFILLING; 90 if (!autofilling) 91 { 92 bool drum_mode = 93 arranger_widget_get_drum_mode_enabled ( 94 self); 95 if (drum_mode) 96 self->action = 97 UI_OVERLAY_ACTION_MOVING; 98 else 99 self->action = 100 UI_OVERLAY_ACTION_CREATING_RESIZING_R; 101 } 102 103 /* create midi note */ 104 MidiNote * midi_note = 105 midi_note_new ( 106 ®ion->id, &local_pos, &local_pos, 107 (midi_byte_t) note, 108 VELOCITY_DEFAULT); 109 ArrangerObject * midi_note_obj = 110 (ArrangerObject *) midi_note; 111 112 /* add it to region */ 113 midi_region_add_midi_note ( 114 region, midi_note, 1); 115 116 /*arranger_object_gen_widget (midi_note_obj);*/ 117 118 /* set visibility */ 119 /*arranger_object_set_widget_visibility_and_state (*/ 120 /*midi_note_obj, 1);*/ 121 122 /* set end pos */ 123 Position tmp; 124 position_set_min_size ( 125 &midi_note_obj->pos, 126 &tmp, self->snap_grid); 127 arranger_object_set_position ( 128 midi_note_obj, &tmp, 129 ARRANGER_OBJECT_POSITION_TYPE_END, 130 F_NO_VALIDATE); 131 arranger_object_set_position ( 132 midi_note_obj, &tmp, 133 ARRANGER_OBJECT_POSITION_TYPE_END, 134 F_NO_VALIDATE); 135 136 /* set as start object */ 137 self->start_object = midi_note_obj; 138 139 EVENTS_PUSH ( 140 ET_ARRANGER_OBJECT_CREATED, midi_note); 141 142 /* select it */ 143 arranger_object_select ( 144 midi_note_obj, F_SELECT, 145 autofilling ? F_APPEND : F_NO_APPEND, 146 F_NO_PUBLISH_EVENTS); 147 } 148 149 /** 150 * Called during drag_update in the parent when 151 * resizing the selection. It sets the start 152 * Position of the selected MidiNote's. 153 * 154 * @param pos Absolute position in the arrranger. 155 * @parram dry_run Don't resize notes; just check 156 * if the resize is allowed (check if invalid 157 * resizes will happen) 158 * 159 * @return 0 if the operation was successful, 160 * nonzero otherwise. 161 */ 162 int 163 midi_arranger_widget_snap_midi_notes_l ( 164 ArrangerWidget * self, 165 Position * pos, 166 bool dry_run) 167 { 168 ArrangerObject * r_obj = 169 (ArrangerObject *) 170 clip_editor_get_region (CLIP_EDITOR); 171 172 /* get delta with first clicked note's start 173 * pos */ 174 double delta = 175 pos->ticks - 176 (self->start_object->pos.ticks + 177 r_obj->pos.ticks); 178 g_debug ("delta %f", delta); 179 180 Position new_start_pos, new_global_start_pos; 181 MidiNote * midi_note; 182 ArrangerObject * mn_obj; 183 for (int i = 0; 184 i < MA_SELECTIONS->num_midi_notes; 185 i++) 186 { 187 midi_note = MA_SELECTIONS->midi_notes[i]; 188 mn_obj = 189 (ArrangerObject *) midi_note; 190 191 /* calculate new start pos */ 192 position_set_to_pos ( 193 &new_start_pos, &mn_obj->pos); 194 position_add_ticks ( 195 &new_start_pos, delta); 196 197 /* get the global star pos first to 198 * snap it */ 199 position_set_to_pos ( 200 &new_global_start_pos, 201 &new_start_pos); 202 position_add_ticks ( 203 &new_global_start_pos, 204 r_obj->pos.ticks); 205 206 /* snap the global pos */ 207 ZRegion * clip_editor_region = 208 clip_editor_get_region (CLIP_EDITOR); 209 if (SNAP_GRID_ANY_SNAP ( 210 self->snap_grid) 211 && !self->shift_held 212 && 213 position_is_positive ( 214 &new_global_start_pos)) 215 { 216 position_snap ( 217 &self->earliest_obj_start_pos, 218 &new_global_start_pos, 219 NULL, clip_editor_region, 220 self->snap_grid); 221 } 222 223 /* convert it back to a local pos */ 224 position_set_to_pos ( 225 &new_start_pos, 226 &new_global_start_pos); 227 position_add_ticks ( 228 &new_start_pos, 229 - r_obj->pos.ticks); 230 231 if (!position_is_positive ( 232 &new_global_start_pos) 233 || 234 position_is_after_or_equal ( 235 &new_start_pos, 236 &mn_obj->end_pos)) 237 { 238 return -1; 239 } 240 else if (!dry_run) 241 { 242 arranger_object_pos_setter ( 243 mn_obj, &new_start_pos); 244 } 245 } 246 247 EVENTS_PUSH ( 248 ET_ARRANGER_SELECTIONS_CHANGED, 249 MA_SELECTIONS); 250 251 return 0; 252 } 253 254 /** 255 * Sets the currently hovered note and queues a 256 * redraw if it changed. 257 * 258 * @param pitch The note pitch, or -1 for no note. 259 */ 260 void 261 midi_arranger_widget_set_hovered_note ( 262 ArrangerWidget * self, 263 int pitch) 264 { 265 if (self->hovered_note != pitch) 266 { 267 GdkRectangle rect; 268 arranger_widget_get_visible_rect (self, &rect); 269 double adj_px_per_key = 270 MW_PIANO_ROLL_KEYS->px_per_key + 1.0; 271 if (self->hovered_note != -1) 272 { 273 /* redraw the previous note area to 274 * unhover it */ 275 rect.y = 276 (int) 277 (adj_px_per_key * 278 (127.0 - (double) self->hovered_note) - 279 1.0); 280 rect.height = (int) adj_px_per_key; 281 arranger_widget_redraw_rectangle ( 282 self, &rect); 283 } 284 self->hovered_note = pitch; 285 286 if (pitch != -1) 287 { 288 /* redraw newly hovered note area */ 289 rect.y = 290 (int) 291 (adj_px_per_key * 292 (127.0 - (double) pitch) - 1); 293 rect.height = (int) adj_px_per_key; 294 arranger_widget_redraw_rectangle ( 295 self, &rect); 296 } 297 } 298 } 299 300 /** 301 * Called during drag_update in parent when 302 * resizing the selection. It sets the end 303 * Position of the selected MIDI notes. 304 * 305 * @param pos Absolute position in the arrranger. 306 * @parram dry_run Don't resize notes; just check 307 * if the resize is allowed (check if invalid 308 * resizes will happen) 309 * 310 * @return 0 if the operation was successful, 311 * nonzero otherwise. 312 */ 313 int 314 midi_arranger_widget_snap_midi_notes_r ( 315 ArrangerWidget * self, 316 Position * pos, 317 bool dry_run) 318 { 319 ArrangerObject * r_obj = 320 (ArrangerObject *) 321 clip_editor_get_region (CLIP_EDITOR); 322 323 /* get delta with first clicked notes's end 324 * pos */ 325 double delta = 326 pos->ticks - 327 (self->start_object->end_pos.ticks + 328 r_obj->pos.ticks); 329 g_debug ("delta %f", delta); 330 331 MidiNote * midi_note; 332 Position new_end_pos, new_global_end_pos; 333 for (int i = 0; 334 i < MA_SELECTIONS->num_midi_notes; 335 i++) 336 { 337 midi_note = 338 MA_SELECTIONS->midi_notes[i]; 339 ArrangerObject * mn_obj = 340 (ArrangerObject *) midi_note; 341 342 /* get new end pos by adding delta 343 * to the cached end pos */ 344 position_set_to_pos ( 345 &new_end_pos, &midi_note->base.end_pos); 346 /*&self->start_object->end_pos);*/ 347 position_add_ticks ( 348 &new_end_pos, delta); 349 350 /* get the global end pos first to snap it */ 351 position_set_to_pos ( 352 &new_global_end_pos, 353 &new_end_pos); 354 position_add_ticks ( 355 &new_global_end_pos, 356 r_obj->pos.ticks); 357 358 /* snap the global pos */ 359 if (SNAP_GRID_ANY_SNAP ( 360 self->snap_grid) 361 && !self->shift_held 362 && 363 position_is_positive ( 364 &new_global_end_pos)) 365 { 366 position_snap ( 367 &self->earliest_obj_start_pos, 368 &new_global_end_pos, NULL, 369 (ZRegion *) r_obj, self->snap_grid); 370 } 371 372 /* convert it back to a local pos */ 373 position_set_to_pos ( 374 &new_end_pos, 375 &new_global_end_pos); 376 position_add_ticks ( 377 &new_end_pos, 378 - r_obj->pos.ticks); 379 380 if (position_is_before_or_equal ( 381 &new_end_pos, 382 &mn_obj->pos)) 383 return -1; 384 else if (!dry_run) 385 { 386 arranger_object_end_pos_setter ( 387 mn_obj, &new_end_pos); 388 } 389 } 390 391 EVENTS_PUSH ( 392 ET_ARRANGER_SELECTIONS_CHANGED, 393 MA_SELECTIONS); 394 395 return 0; 396 } 397 398 /** 399 * Called on move items_y setup. 400 * 401 * calculates the max possible y movement 402 */ 403 int 404 midi_arranger_calc_deltamax_for_note_movement ( 405 int y_delta) 406 { 407 MidiNote * midi_note; 408 for (int i = 0; 409 i < MA_SELECTIONS->num_midi_notes; 410 i++) 411 { 412 midi_note = 413 MA_SELECTIONS->midi_notes[i]; 414 /*g_message ("midi note val %d, y delta %d",*/ 415 /*midi_note->val, y_delta);*/ 416 if (midi_note->val + y_delta < 0) 417 { 418 y_delta = 0; 419 } 420 else if (midi_note->val + y_delta >= 127) 421 { 422 y_delta = 127 - midi_note->val; 423 } 424 } 425 /*g_message ("y delta %d", y_delta);*/ 426 return y_delta; 427 /*return y_delta < 0 ? -1 : 1;*/ 428 } 429 430 /** 431 * Listen to the currently selected notes. 432 * 433 * This function either turns on the notes if they 434 * are not playing, changes the notes if the pitch 435 * changed, or otherwise does nothing. 436 * 437 * @param listen Turn notes on if 1, or turn them 438 * off if 0. 439 */ 440 void 441 midi_arranger_listen_notes ( 442 ArrangerWidget * self, 443 bool listen) 444 { 445 /*g_message ("%s: listen: %d", __func__, listen);*/ 446 447 if (!g_settings_get_boolean ( 448 S_UI, "listen-notes")) 449 return; 450 451 ArrangerSelections * sel = 452 arranger_widget_get_selections (self); 453 MidiArrangerSelections * mas = 454 (MidiArrangerSelections *) sel; 455 Position start_pos; 456 arranger_selections_get_start_pos ( 457 sel, &start_pos, F_NOT_GLOBAL); 458 double ticks_cutoff = 459 start_pos.ticks + 460 (double) TRANSPORT->ticks_per_beat; 461 for (int i = 0; i < mas->num_midi_notes; i++) 462 { 463 MidiNote * mn = mas->midi_notes[i]; 464 ArrangerObject * mn_obj = 465 (ArrangerObject *) mn; 466 467 /* only add notes during the first beat of the 468 * selections if listening */ 469 if (!listen || 470 mn_obj->pos.ticks < ticks_cutoff) 471 midi_note_listen (mn, listen); 472 } 473 } 474 475 static void 476 on_view_info ( 477 GtkMenuItem * menuitem, 478 ArrangerObject * mn_obj) 479 { 480 ArrangerObjectInfoDialogWidget * dialog = 481 arranger_object_info_dialog_widget_new ( 482 mn_obj); 483 gtk_dialog_run (GTK_DIALOG (dialog)); 484 gtk_widget_destroy (GTK_WIDGET (dialog)); 485 } 486 487 void 488 midi_arranger_show_context_menu ( 489 ArrangerWidget * self, 490 gdouble x, 491 gdouble y) 492 { 493 GtkWidget *menu; 494 GtkMenuItem * menu_item; 495 496 ArrangerObject * mn_obj = 497 arranger_widget_get_hit_arranger_object ( 498 self, ARRANGER_OBJECT_TYPE_MIDI_NOTE, x, y); 499 MidiNote * mn = (MidiNote *) mn_obj; 500 501 #define APPEND_TO_MENU \ 502 gtk_menu_shell_append ( \ 503 GTK_MENU_SHELL (menu), \ 504 GTK_WIDGET (menu_item)) 505 506 menu = gtk_menu_new(); 507 508 if (mn) 509 { 510 int selected = 511 arranger_object_is_selected ( 512 mn_obj); 513 if (!selected) 514 { 515 arranger_object_select ( 516 mn_obj, 517 F_SELECT, F_NO_APPEND, 518 F_NO_PUBLISH_EVENTS); 519 } 520 521 menu_item = 522 CREATE_CUT_MENU_ITEM ("app.cut"); 523 APPEND_TO_MENU; 524 menu_item = 525 CREATE_COPY_MENU_ITEM ("app.copy"); 526 APPEND_TO_MENU; 527 menu_item = 528 CREATE_PASTE_MENU_ITEM ("app.paste"); 529 APPEND_TO_MENU; 530 menu_item = 531 CREATE_DELETE_MENU_ITEM ("app.delete"); 532 APPEND_TO_MENU; 533 menu_item = 534 CREATE_DUPLICATE_MENU_ITEM ( 535 "app.duplicate"); 536 APPEND_TO_MENU; 537 menu_item = 538 GTK_MENU_ITEM ( 539 gtk_separator_menu_item_new ()); 540 APPEND_TO_MENU; 541 menu_item = 542 GTK_MENU_ITEM ( 543 gtk_menu_item_new_with_label( 544 _("View info"))); 545 g_signal_connect ( 546 menu_item, "activate", 547 G_CALLBACK (on_view_info), mn_obj); 548 APPEND_TO_MENU; 549 } 550 else 551 { 552 arranger_widget_select_all ( 553 (ArrangerWidget *) self, F_NO_SELECT, 554 F_PUBLISH_EVENTS); 555 arranger_selections_clear ( 556 (ArrangerSelections *) MA_SELECTIONS, 557 F_NO_FREE, F_NO_PUBLISH_EVENTS); 558 559 menu_item = 560 CREATE_PASTE_MENU_ITEM ("app.paste"); 561 APPEND_TO_MENU; 562 } 563 menu_item = 564 GTK_MENU_ITEM (gtk_separator_menu_item_new ()); 565 APPEND_TO_MENU; 566 menu_item = 567 CREATE_CLEAR_SELECTION_MENU_ITEM ( 568 "app.clear-selection"); 569 APPEND_TO_MENU; 570 menu_item = 571 CREATE_SELECT_ALL_MENU_ITEM ( 572 "app.select-all"); 573 APPEND_TO_MENU; 574 575 #undef APPEND_TO_MENU 576 577 gtk_menu_attach_to_widget ( 578 GTK_MENU (menu), GTK_WIDGET (self), NULL); 579 gtk_widget_show_all (menu); 580 gtk_menu_popup_at_pointer (GTK_MENU (menu), NULL); 581 } 582 583 /** 584 * Handle ctrl+shift+scroll. 585 */ 586 void 587 midi_arranger_handle_vertical_zoom_scroll ( 588 ArrangerWidget * self, 589 GdkEventScroll * event) 590 { 591 if (!(event->state & GDK_CONTROL_MASK && 592 event->state & GDK_SHIFT_MASK)) 593 return; 594 595 GtkScrolledWindow * scroll = 596 arranger_widget_get_scrolled_window (self); 597 598 /* get current adjustment so we can get the 599 * difference from the cursor */ 600 GtkAdjustment * adj = 601 gtk_scrolled_window_get_vadjustment (scroll); 602 double adj_val = gtk_adjustment_get_value (adj); 603 double size_before = 604 gtk_adjustment_get_upper (adj); 605 double adj_perc = event->y / size_before; 606 607 /* get px diff so we can calculate the new 608 * adjustment later */ 609 double diff = event->y - adj_val; 610 611 /* scroll down, zoom out */ 612 double size_after; 613 float notes_zoom_before = PIANO_ROLL->notes_zoom; 614 double _multiplier = 1.16; 615 double multiplier = 616 event->delta_y > 0 ? 617 1 / _multiplier : _multiplier; 618 piano_roll_set_notes_zoom ( 619 PIANO_ROLL, 620 PIANO_ROLL->notes_zoom * (float) multiplier, 0); 621 size_after = size_before * multiplier; 622 623 if (math_floats_equal ( 624 PIANO_ROLL->notes_zoom, 625 notes_zoom_before)) 626 { 627 size_after = size_before; 628 } 629 630 /* refresh relevant widgets */ 631 midi_editor_space_widget_refresh ( 632 MW_MIDI_EDITOR_SPACE); 633 634 /* get updated adjustment and set its value 635 at the same offset as before */ 636 adj = 637 gtk_scrolled_window_get_vadjustment (scroll); 638 gtk_adjustment_set_value ( 639 adj, adj_perc * size_after - diff); 640 } 641