/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2022 Blender Foundation. All rights reserved. */ /** \file * \ingroup spseq */ #include "MEM_guardedalloc.h" #include "DNA_scene_types.h" #include "DNA_sound_types.h" #include "BLI_blenlib.h" #include "BLI_string_utils.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_image.h" #include "BKE_main.h" #include "SEQ_channels.h" #include "SEQ_iterator.h" #include "SEQ_sequencer.h" #include "SEQ_transform.h" #include "UI_resources.h" #include "UI_view2d.h" #include "GPU_immediate.h" #include "GPU_matrix.h" #include "ED_screen.h" #include "ED_transform.h" #include "IMB_imbuf.h" #include "IMB_imbuf_types.h" #include "WM_api.h" #include "WM_types.h" /* For querying audio files. */ #ifdef WITH_AUDASPACE # include "BKE_sound.h" # include # include #endif /* Own include. */ #include "sequencer_intern.h" typedef struct SeqDropCoords { float start_frame, channel; int strip_len, channel_len; float playback_rate; bool in_use; bool has_read_mouse_pos; bool is_intersecting; bool use_snapping; float snap_point_x; uint8_t type; } SeqDropCoords; /* The current drag and drop API doesn't allow us to easily pass along the * required custom data to all callbacks that need it. Especially when * preloading data on drag start. * Therefore we will for now use a global variable for this. */ static SeqDropCoords g_drop_coords = {.in_use = false, .has_read_mouse_pos = false}; static void generic_poll_operations(const wmEvent *event, uint8_t type) { g_drop_coords.type = type; /* We purposely ignore the snapping tool setting here as currently other drag&drop operators only * snaps when holding down Ctrl. */ g_drop_coords.use_snapping = event->modifier & KM_CTRL; } static bool image_drop_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *event) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, ICON_FILE_IMAGE, ICON_FILE_BLANK)) { /* Rule might not work? */ generic_poll_operations(event, TH_SEQ_IMAGE); return true; } } if (WM_drag_is_ID_type(drag, ID_IM)) { generic_poll_operations(event, TH_SEQ_IMAGE); return true; } return false; } static bool is_movie(wmDrag *drag) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, ICON_FILE_MOVIE, ICON_FILE_BLANK)) { /* Rule might not work? */ return true; } } if (WM_drag_is_ID_type(drag, ID_MC)) { return true; } return false; } static bool movie_drop_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *event) { if (is_movie(drag)) { generic_poll_operations(event, TH_SEQ_MOVIE); return true; } return false; } static bool is_sound(wmDrag *drag) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, ICON_FILE_SOUND, ICON_FILE_BLANK)) { /* Rule might not work? */ return true; } } if (WM_drag_is_ID_type(drag, ID_SO)) { return true; } return false; } static bool sound_drop_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *event) { if (is_sound(drag)) { generic_poll_operations(event, TH_SEQ_AUDIO); return true; } return false; } static float update_overlay_strip_position_data(bContext *C, const int mval[2]) { SeqDropCoords *coords = &g_drop_coords; ARegion *region = CTX_wm_region(C); Scene *scene = CTX_data_scene(C); int hand; View2D *v2d = ®ion->v2d; /* Update the position were we would place the strip if we complete the drag and drop action. */ UI_view2d_region_to_view(v2d, mval[0], mval[1], &coords->start_frame, &coords->channel); coords->start_frame = roundf(coords->start_frame); if (coords->channel < 1.0f) { coords->channel = 1; } float start_frame = coords->start_frame; float end_frame; float strip_len; if (coords->playback_rate != 0.0f) { float scene_playback_rate = (float)scene->r.frs_sec / scene->r.frs_sec_base; strip_len = coords->strip_len / (coords->playback_rate / scene_playback_rate); } else { strip_len = coords->strip_len; } end_frame = coords->start_frame + strip_len; if (coords->use_snapping) { /* Do snapping via the existing transform code. */ int snap_delta; float snap_frame; bool valid_snap; valid_snap = ED_transform_snap_sequencer_to_closest_strip_calc( scene, region, start_frame, end_frame, &snap_delta, &snap_frame); if (valid_snap) { /* We snapped onto something! */ start_frame += snap_delta; coords->start_frame = start_frame; end_frame = start_frame + strip_len; coords->snap_point_x = snap_frame; } else { /* Nothing was snapped to, disable snap drawing. */ coords->use_snapping = false; } } if (strip_len < 1) { /* Only check if there is a strip already under the mouse cursor. */ coords->is_intersecting = find_nearest_seq(scene, ®ion->v2d, &hand, mval); } else { /* Check if there is a strip that would intersect with the new strip(s). */ coords->is_intersecting = false; Sequence dummy_seq = {.machine = coords->channel, .start = coords->start_frame, .len = coords->strip_len, .speed_factor = 1.0f, .media_playback_rate = coords->playback_rate, .flag = SEQ_AUTO_PLAYBACK_RATE}; Editing *ed = SEQ_editing_ensure(scene); for (int i = 0; i < coords->channel_len && !coords->is_intersecting; i++) { coords->is_intersecting = SEQ_transform_test_overlap(scene, ed->seqbasep, &dummy_seq); dummy_seq.machine++; } } return strip_len; } static void sequencer_drop_copy(bContext *C, wmDrag *drag, wmDropBox *drop) { ID *id = WM_drag_get_local_ID_or_import_from_asset(drag, 0); /* ID dropped. */ if (id != NULL) { const ID_Type id_type = GS(id->name); if (id_type == ID_IM) { Image *ima = (Image *)id; PointerRNA itemptr; char dir[FILE_MAX], file[FILE_MAX]; BLI_split_dirfile(ima->filepath, dir, file, sizeof(dir), sizeof(file)); RNA_string_set(drop->ptr, "directory", dir); RNA_collection_clear(drop->ptr, "files"); RNA_collection_add(drop->ptr, "files", &itemptr); RNA_string_set(&itemptr, "name", file); } else if (id_type == ID_MC) { MovieClip *clip = (MovieClip *)id; RNA_string_set(drop->ptr, "filepath", clip->filepath); RNA_struct_property_unset(drop->ptr, "name"); } else if (id_type == ID_SO) { bSound *sound = (bSound *)id; RNA_string_set(drop->ptr, "filepath", sound->filepath); RNA_struct_property_unset(drop->ptr, "name"); } } /* Path dropped. */ else if (drag->path[0]) { if (RNA_struct_find_property(drop->ptr, "filepath")) { RNA_string_set(drop->ptr, "filepath", drag->path); } if (RNA_struct_find_property(drop->ptr, "directory")) { PointerRNA itemptr; char dir[FILE_MAX], file[FILE_MAX]; BLI_split_dirfile(drag->path, dir, file, sizeof(dir), sizeof(file)); RNA_string_set(drop->ptr, "directory", dir); RNA_collection_clear(drop->ptr, "files"); RNA_collection_add(drop->ptr, "files", &itemptr); RNA_string_set(&itemptr, "name", file); } } if (g_drop_coords.in_use) { if (!g_drop_coords.has_read_mouse_pos) { /* We didn't read the mouse position, so we need to do it manually here. */ int xy[2]; wmWindow *win = CTX_wm_window(C); xy[0] = win->eventstate->xy[0]; xy[1] = win->eventstate->xy[1]; ARegion *region = CTX_wm_region(C); int mval[2]; /* Convert mouse coordinates to region local coordinates. */ mval[0] = xy[0] - region->winrct.xmin; mval[1] = xy[1] - region->winrct.ymin; update_overlay_strip_position_data(C, mval); } RNA_int_set(drop->ptr, "frame_start", g_drop_coords.start_frame); RNA_int_set(drop->ptr, "channel", g_drop_coords.channel); RNA_boolean_set(drop->ptr, "overlap_shuffle_override", true); } else { /* We are dropped inside the preview region. Put the strip on top of the * current displayed frame. */ Scene *scene = CTX_data_scene(C); Editing *ed = SEQ_editing_get(scene); ListBase *seqbase = SEQ_active_seqbase_get(ed); ListBase *channels = SEQ_channels_displayed_get(ed); SpaceSeq *sseq = CTX_wm_space_seq(C); SeqCollection *strips = SEQ_query_rendered_strips( scene, channels, seqbase, scene->r.cfra, sseq->chanshown); /* Get the top most strip channel that is in view. */ Sequence *seq; int max_channel = -1; SEQ_ITERATOR_FOREACH (seq, strips) { max_channel = max_ii(seq->machine, max_channel); } if (max_channel != -1) { RNA_int_set(drop->ptr, "channel", max_channel); } SEQ_collection_free(strips); } } static void get_drag_path(wmDrag *drag, char r_path[FILE_MAX]) { ID *id = WM_drag_get_local_ID_or_import_from_asset(drag, 0); /* ID dropped. */ if (id != NULL) { const ID_Type id_type = GS(id->name); if (id_type == ID_IM) { Image *ima = (Image *)id; BLI_strncpy(r_path, ima->filepath, FILE_MAX); } else if (id_type == ID_MC) { MovieClip *clip = (MovieClip *)id; BLI_strncpy(r_path, clip->filepath, FILE_MAX); } else if (id_type == ID_SO) { bSound *sound = (bSound *)id; BLI_strncpy(r_path, sound->filepath, FILE_MAX); } BLI_path_abs(r_path, BKE_main_blendfile_path_from_global()); } else { BLI_strncpy(r_path, drag->path, FILE_MAX); } } static void draw_seq_in_view(bContext *C, wmWindow *UNUSED(win), wmDrag *drag, const int xy[2]) { SeqDropCoords *coords = &g_drop_coords; if (!coords->in_use) { return; } ARegion *region = CTX_wm_region(C); int mval[2]; /* Convert mouse coordinates to region local coordinates. */ mval[0] = xy[0] - region->winrct.xmin; mval[1] = xy[1] - region->winrct.ymin; float strip_len = update_overlay_strip_position_data(C, mval); GPU_matrix_push(); UI_view2d_view_ortho(®ion->v2d); /* Sometimes the active theme is not the sequencer theme, e.g. when an operator invokes the * file browser. This makes sure we get the right color values for the theme. */ struct bThemeState theme_state; UI_Theme_Store(&theme_state); UI_SetTheme(SPACE_SEQ, RGN_TYPE_WINDOW); if (coords->use_snapping) { ED_draw_sequencer_snap_point(C, coords->snap_point_x); } /* Init GPU drawing. */ GPU_line_width(2.0f); GPU_blend(GPU_BLEND_ALPHA); GPU_line_smooth(true); uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); /* Draw strips. The code here is taken from sequencer_draw. */ float x1 = coords->start_frame; float x2 = coords->start_frame + floorf(strip_len); float strip_color[3]; uchar text_color[4] = {255, 255, 255, 255}; float pixelx = BLI_rctf_size_x(®ion->v2d.cur) / BLI_rcti_size_x(®ion->v2d.mask); float pixely = BLI_rctf_size_y(®ion->v2d.cur) / BLI_rcti_size_y(®ion->v2d.mask); for (int i = 0; i < coords->channel_len; i++) { float y1 = floorf(coords->channel) + i + SEQ_STRIP_OFSBOTTOM; float y2 = floorf(coords->channel) + i + SEQ_STRIP_OFSTOP; if (coords->type == TH_SEQ_MOVIE && i == 0 && coords->channel_len > 1) { /* Assume only video strips occupies two channels. * One for video and the other for audio. * The audio channel is added first. */ UI_GetThemeColor3fv(TH_SEQ_AUDIO, strip_color); } else { UI_GetThemeColor3fv(coords->type, strip_color); } immUniformColor3fvAlpha(strip_color, 0.8f); immRectf(pos, x1, y1, x2, y2); if (coords->is_intersecting) { strip_color[0] = 1.0f; strip_color[1] = strip_color[2] = 0.3f; } else { if (coords->channel_len - 1 == i) { text_color[0] = text_color[1] = text_color[2] = 255; UI_GetThemeColor3fv(TH_SEQ_ACTIVE, strip_color); } else { text_color[0] = text_color[1] = text_color[2] = 10; UI_GetThemeColor3fv(TH_SEQ_SELECTED, strip_color); } } /* Draw a 2 pixel border around the strip. */ immUniformColor3fvAlpha(strip_color, 0.8f); /* Left */ immRectf(pos, x1 - pixelx, y1, x1 + pixelx, y2); /* Bottom */ immRectf(pos, x1 - pixelx, y1, x2 + pixelx, y1 + 2 * pixely); /* Right */ immRectf(pos, x2 - pixelx, y1, x2 + pixelx, y2); /* Top */ immRectf(pos, x1 - pixelx, y2 - 2 * pixely, x2 + pixelx, y2); float handle_size = 8.0f; /* SEQ_HANDLE_SIZE */ /* Calculate height needed for drawing text on strip. */ float text_margin_y = y2 - min_ff(0.40f, 20 * U.dpi_fac * pixely); float text_margin_x = 2.0f * (pixelx * handle_size) * U.pixelsize; rctf rect; rect.xmin = x1 + text_margin_x; rect.ymin = text_margin_y; rect.xmax = x2 - text_margin_x; rect.ymax = y2; if (rect.xmax <= rect.xmin) { /* Exit early and skip text drawing if the strip doesn't have any space to put the text * into. */ break; } SpaceSeq *sseq = CTX_wm_space_seq(C); const char *text_sep = " | "; const char *text_array[5]; char text_display[FILE_MAX]; char filename[FILE_MAX]; char path[FILE_MAX]; char strip_duration_text[16]; int len_text_arr = 0; get_drag_path(drag, path); if (sseq->timeline_overlay.flag & SEQ_TIMELINE_SHOW_STRIP_NAME) { BLI_split_file_part(path, filename, FILE_MAX); text_array[len_text_arr++] = filename; } if (sseq->timeline_overlay.flag & SEQ_TIMELINE_SHOW_STRIP_SOURCE) { Main *bmain = CTX_data_main(C); BLI_path_rel(path, BKE_main_blendfile_path(bmain)); text_array[len_text_arr++] = text_sep; text_array[len_text_arr++] = path; } if (sseq->timeline_overlay.flag & SEQ_TIMELINE_SHOW_STRIP_DURATION) { SNPRINTF(strip_duration_text, "%d", (int)(x2 - x1)); text_array[len_text_arr++] = text_sep; text_array[len_text_arr++] = strip_duration_text; } BLI_assert(len_text_arr <= ARRAY_SIZE(text_array)); BLI_string_join_array(text_display, FILE_MAX, text_array, len_text_arr); UI_view2d_text_cache_add_rectf( ®ion->v2d, &rect, text_display, strlen(text_display), text_color); } /* Clean after drawing up. */ UI_Theme_Restore(&theme_state); GPU_matrix_pop(); immUnbindProgram(); GPU_blend(GPU_BLEND_NONE); GPU_line_smooth(false); UI_view2d_text_cache_draw(region); } static bool generic_drop_draw_handling(struct wmDropBox *drop) { SeqDropCoords *coords = drop->draw_data; if (coords && coords->in_use) { return true; } coords = drop->draw_data = &g_drop_coords; coords->in_use = true; return false; } typedef struct DropJobData { char path[FILE_MAX]; bool only_audio; float scene_fps; } DropJobData; static void prefetch_data_fn(void *custom_data, short *UNUSED(stop), short *UNUSED(do_update), float *UNUSED(progress)) { DropJobData *job_data = (DropJobData *)custom_data; if (job_data->only_audio) { #ifdef WITH_AUDASPACE /* Get the sound file length */ AUD_Sound *sound = AUD_Sound_file(job_data->path); if (sound != NULL) { AUD_SoundInfo info = AUD_getInfo(sound); if ((eSoundChannels)info.specs.channels != SOUND_CHANNELS_INVALID) { g_drop_coords.strip_len = max_ii(1, round((info.length) * job_data->scene_fps)); } AUD_Sound_free(sound); return; } #endif } char colorspace[64] = "\0"; /* 64 == MAX_COLORSPACE_NAME length. */ struct anim *anim = openanim(job_data->path, IB_rect, 0, colorspace); if (anim != NULL) { g_drop_coords.strip_len = IMB_anim_get_duration(anim, IMB_TC_NONE); short frs_sec; float frs_sec_base; if (IMB_anim_get_fps(anim, &frs_sec, &frs_sec_base, true)) { g_drop_coords.playback_rate = (float)frs_sec / frs_sec_base; } else { g_drop_coords.playback_rate = 0; } IMB_free_anim(anim); #ifdef WITH_AUDASPACE /* Try to load sound and see if the video has a sound channel. */ AUD_Sound *sound = AUD_Sound_file(job_data->path); if (sound != NULL) { AUD_SoundInfo info = AUD_getInfo(sound); if ((eSoundChannels)info.specs.channels != SOUND_CHANNELS_INVALID) { g_drop_coords.channel_len = 2; } AUD_Sound_free(sound); } #endif } } static void free_prefetch_data_fn(void *custom_data) { DropJobData *job_data = (DropJobData *)custom_data; MEM_freeN(job_data); } static void start_audio_video_job(bContext *C, wmDrag *drag, bool only_audio) { g_drop_coords.strip_len = 0; g_drop_coords.channel_len = 1; wmWindowManager *wm = CTX_wm_manager(C); wmWindow *win = CTX_wm_window(C); Scene *scene = CTX_data_scene(C); wmJob *wm_job = WM_jobs_get( wm, win, NULL, "Load Previews", 0, WM_JOB_TYPE_SEQ_DRAG_DROP_PREVIEW); DropJobData *job_data = (DropJobData *)MEM_mallocN(sizeof(DropJobData), "SeqDragDropPreviewData"); get_drag_path(drag, job_data->path); job_data->only_audio = only_audio; job_data->scene_fps = FPS; WM_jobs_customdata_set(wm_job, job_data, free_prefetch_data_fn); WM_jobs_timer(wm_job, 0.1, NC_WINDOW, NC_WINDOW); WM_jobs_callbacks(wm_job, prefetch_data_fn, NULL, NULL, NULL); WM_jobs_start(wm, wm_job); } static void video_prefetch(bContext *C, wmDrag *drag) { if (is_movie(drag)) { start_audio_video_job(C, drag, false); } } static void audio_prefetch(bContext *C, wmDrag *drag) { if (is_sound(drag)) { start_audio_video_job(C, drag, true); } } static void movie_drop_draw_activate(struct wmDropBox *drop, wmDrag *UNUSED(drag)) { if (generic_drop_draw_handling(drop)) { return; } } static void sound_drop_draw_activate(struct wmDropBox *drop, wmDrag *UNUSED(drag)) { if (generic_drop_draw_handling(drop)) { return; } } static void image_drop_draw_activate(struct wmDropBox *drop, wmDrag *UNUSED(drag)) { if (generic_drop_draw_handling(drop)) { return; } SeqDropCoords *coords = drop->draw_data; coords->strip_len = DEFAULT_IMG_STRIP_LENGTH; coords->channel_len = 1; } static void sequencer_drop_draw_deactivate(struct wmDropBox *drop, wmDrag *UNUSED(drag)) { SeqDropCoords *coords = drop->draw_data; if (coords) { coords->in_use = false; coords->has_read_mouse_pos = false; drop->draw_data = NULL; } } static void nop_draw_droptip_fn(bContext *UNUSED(C), wmWindow *UNUSED(win), wmDrag *UNUSED(drag), const int UNUSED(xy[2])) { /* Do nothing in here. * This is to prevent the default drag and drop mouse overlay to be drawn. */ } /* This region dropbox definition. */ static void sequencer_dropboxes_add_to_lb(ListBase *lb) { struct wmDropBox *drop; drop = WM_dropbox_add( lb, "SEQUENCER_OT_image_strip_add", image_drop_poll, sequencer_drop_copy, NULL, NULL); drop->draw_droptip = nop_draw_droptip_fn; drop->draw_in_view = draw_seq_in_view; drop->draw_activate = image_drop_draw_activate; drop->draw_deactivate = sequencer_drop_draw_deactivate; drop->on_drag_start = audio_prefetch; drop = WM_dropbox_add( lb, "SEQUENCER_OT_movie_strip_add", movie_drop_poll, sequencer_drop_copy, NULL, NULL); drop->draw_droptip = nop_draw_droptip_fn; drop->draw_in_view = draw_seq_in_view; drop->draw_activate = movie_drop_draw_activate; drop->draw_deactivate = sequencer_drop_draw_deactivate; drop->on_drag_start = video_prefetch; drop = WM_dropbox_add( lb, "SEQUENCER_OT_sound_strip_add", sound_drop_poll, sequencer_drop_copy, NULL, NULL); drop->draw_droptip = nop_draw_droptip_fn; drop->draw_in_view = draw_seq_in_view; drop->draw_activate = sound_drop_draw_activate; drop->draw_deactivate = sequencer_drop_draw_deactivate; } static bool image_drop_preview_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *UNUSED(event)) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, ICON_FILE_IMAGE, ICON_FILE_BLANK)) { /* Rule might not work? */ return true; } } return WM_drag_is_ID_type(drag, ID_IM); } static bool movie_drop_preview_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *UNUSED(event)) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, 0, ICON_FILE_MOVIE, ICON_FILE_BLANK)) { /* Rule might not work? */ return true; } } return WM_drag_is_ID_type(drag, ID_MC); } static bool sound_drop_preview_poll(bContext *UNUSED(C), wmDrag *drag, const wmEvent *UNUSED(event)) { if (drag->type == WM_DRAG_PATH) { if (ELEM(drag->icon, ICON_FILE_SOUND, ICON_FILE_BLANK)) { /* Rule might not work? */ return true; } } return WM_drag_is_ID_type(drag, ID_SO); } static void sequencer_preview_dropboxes_add_to_lb(ListBase *lb) { WM_dropbox_add(lb, "SEQUENCER_OT_image_strip_add", image_drop_preview_poll, sequencer_drop_copy, NULL, NULL); WM_dropbox_add(lb, "SEQUENCER_OT_movie_strip_add", movie_drop_preview_poll, sequencer_drop_copy, NULL, NULL); WM_dropbox_add(lb, "SEQUENCER_OT_sound_strip_add", sound_drop_preview_poll, sequencer_drop_copy, NULL, NULL); } void sequencer_dropboxes(void) { ListBase *lb = WM_dropboxmap_find("Sequencer", SPACE_SEQ, RGN_TYPE_WINDOW); sequencer_dropboxes_add_to_lb(lb); lb = WM_dropboxmap_find("Sequencer", SPACE_SEQ, RGN_TYPE_PREVIEW); sequencer_preview_dropboxes_add_to_lb(lb); }