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       &region->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