Documentation/Source example
Example source

samples/basic/DragnDrop.js

// This sample demonstrates a basic drag-n-drop implementation.

// Don't forget to open some playlist viewer to see all the performed actions.

//

// CTRL, SHIFT, ALT and their combination will modify the action

// that will be performed on item (you can try that by moving file in Windows Explorer).

//

// Warning: drag-n-drop is an advanced technique, so beware!

//


window.DefineScript('Drag n drop', { author: 'TheQwertiest', features: { drag_n_drop: true } });

include('docs/Flags.js');

const fso = new ActiveXObject('Scripting.FileSystemObject');
const font = gdi.Font('Segoe Ui', 12);
const g_drop_effect = {
    none:   0,
    copy:   1,
    move:   2,
    link:   4,
    scroll: 0x80000000
};

let ww, wh;

let mouse_in = false;
let mouse_down = false;
let mouse_on_item = false;

let last_pressed_coord = {};

let is_dragging = false;
let is_internal_drag_n_drop_active = false;

let status_text = 'Start dragging to or from the panel';
let status_text_2 = '';


////////////////////////////

// Callbacks

////////////////////////////


function on_paint(gr) {
    gr.SetTextRenderingHint(TextRenderingHint.ClearTypeGridFit);
    
    gr.FillSolidRect(0, 60, ww/2, wh - 60, 0xFF83FFFF);
    gr.FillSolidRect(ww/2, 60, ww/2, wh - 60, 0xFFFF83FF);
    
    gr.DrawString(status_text, font, 0xFF000000, 0, 0, ww, 30, 0x11000000);
    gr.DrawString(status_text_2, font, 0xFF000000, 0, 30, ww, 30, 0x11000000);
    
    gr.DrawString('Drag from here or from outside', font, 0xFF000000, 0, 60, ww/2, wh - 60, 0x11000000);
    gr.DrawString('Drop here or outside', font, 0xFF000000, ww/2, 60, ww/2, wh - 60, 0x11000000);
}

function on_size(w,h) {
    ww = w;
    wh = h;
}

function on_mouse_move(x,y,m){
    if (!mouse_down) {
        return true;
    }

    if (!is_dragging && mouse_on_item) {
        let drag_diff = Math.sqrt((Math.pow(last_pressed_coord.x - x, 2) + Math.pow(last_pressed_coord.y - y, 2)));
        if (drag_diff >= 7) {
            last_pressed_coord = {
                x: undefined,
                y: undefined
            };

            perform_internal_drag_n_drop();
        }
    }

    return true;
}

function on_mouse_lbtn_down(x, y, m) {
    mouse_down = true;
    
    last_pressed_coord.x = x;
    last_pressed_coord.y = y;

    mouse_on_item = trace_start_zone(x,y);

    return true;
}

function on_mouse_lbtn_up(x, y, m) {   
    mouse_down = false;
    
    last_pressed_coord = {
        x: undefined,
        y: undefined
    };

    mouse_on_item = false;

    return true;
}

function on_drag_enter(action, x, y, mask) {
    mouse_in = true;
    mouse_down = true;

    if (!is_dragging) {
        is_dragging = true;
    }

    if (trace_end_zone(x, y)) {
        action.Effect = (action.Effect & g_drop_effect.move)
                || (action.Effect & g_drop_effect.copy)
                || (action.Effect & g_drop_effect.link);
        action.Text = get_text_from_effect(action.Effect, action.IsInternal);
    }
    else {        
        action.Effect = g_drop_effect.none;
    }
}

function on_drag_leave() {
    if (is_dragging) {
        is_dragging = false;
    }

    mouse_in = false;
    mouse_down = false;

    status_text_2 = 'Item was dragged outside or dropped on no-drop zone';
    window.Repaint(); 
}

function on_drag_over(action, x, y, mask) {
    if (trace_end_zone(x, y)) {
        action.Effect = filter_effect_by_modifiers(action.Effect);
        action.Text = get_text_from_effect(action.Effect, action.IsInternal);
    }
    else {
        action.Effect = g_drop_effect.none;
    }
    
    if (status_text_2 !== 'Item is being dragged') {
        status_text_2 = 'Item is being dragged';
        window.Repaint(); 
    }
}

function on_drag_drop(action, x, y, m) {
    this.mouse_down = false; ///< because on_drag_drop suppresses on_mouse_lbtn_up call


    if (!is_dragging || !trace_end_zone(x, y)) {
        is_dragging = false;
        action.Effect = g_drop_effect.none;
        
        return;
    }

    let ctrl_pressed = utils.IsKeyPressed(VK_CONTROL);

    if (action.IsInternal) {        
        let copy_drop = ctrl_pressed && ((action.Effect & 1) || (action.Effect & 4));
        drop(copy_drop);

        // Suppress native drop, since we've handled it ourselves

        action.Effect = g_drop_effect.none;
                
        status_text_2 = 'Item from playlist was dropped and ';
        status_text_2 += copy_drop ? 'copied' : 'moved';            
        window.Repaint();        
    }
    else {
        action.Effect = filter_effect_by_modifiers(action.Effect);
        if (g_drop_effect.none !== action.Effect) {
            external_drop(action);
            status_text_2 = 'Item from outside was dropped and ';
            status_text_2 += g_drop_effect.move & action.Effect ? 'moved' : 'copied';
            window.Repaint();    
        }
        else {
            is_dragging = false;
            
            status_text_2 = 'Item from outside was dropped but the action was forbidden';            
            window.Repaint(); 
        }
    }
}

////////////////////////////

// Methods

////////////////////////////


function trace_start_zone(x,y) {
    return x >= 0 && x < ww/2 && y >= 60 && y < wh;
}

function trace_end_zone(x,y) {
    return x >= ww/2 && x < ww && y >= 60 && y < wh;
}

function perform_internal_drag_n_drop() {
    let cur_selected_index = 0;
    let cur_playlist_idx = plman.ActivePlaylist;
    
    is_dragging = true;
    
    plman.ClearPlaylistSelection(cur_playlist_idx);
    plman.SetPlaylistSelectionSingle(cur_playlist_idx, cur_selected_index, true);
    plman.SetPlaylistFocusItem(cur_playlist_idx, cur_selected_index);
    
    let cur_playlist_size = plman.PlaylistItemCount(cur_playlist_idx);
    let cur_playlist_selection = plman.GetPlaylistSelectedItems(cur_playlist_idx);
    
    let effect = fb.DoDragDrop(0, cur_playlist_selection, g_drop_effect.copy | g_drop_effect.move | g_drop_effect.link);

    if (is_dragging) {
        // If drag operation was not cancelled, then it means that nor on_drag_drop, nor on_drag_leave event handlers

        // were triggered, which means that the items were most likely dropped inside the panel

        // (and relevant methods were not called because of async event processing)        

        return;
    }

    function can_handle_move_drop() {
        // We can handle the 'move drop' properly only when playlist is still in the same state

        return cur_playlist_size === plman.PlaylistItemCount(cur_playlist_idx)
            && plman.IsPlaylistItemSelected(cur_playlist_idx, cur_selected_index);
    }

    if (g_drop_effect.none === effect && can_handle_move_drop()) {
        // DROPEFFECT_NONE needs special handling, because on NT it

        // is returned for some move operations, instead of DROPEFFECT_MOVE.

        // See Q182219 for the details.


        let items_to_remove = [];
        let playlist_items = plman.GetPlaylistItems(cur_playlist_idx);
        [cur_selected_index].forEach(function (idx) {
            let cur_item = playlist_items[idx];
            if (cur_item.RawPath.startsWith('file://') && !fso.FileExists(cur_item.Path)) {
                items_to_remove.push(idx);
            }
        });

        if (items_to_remove.length) {
            plman.ClearPlaylistSelection(cur_playlist_idx);
            plman.SetPlaylistSelection(cur_playlist_idx, items_to_remove, true);
            plman.RemovePlaylistSelection(cur_playlist_idx);
        }
    }
    else if (g_drop_effect.move === effect && can_handle_move_drop()) {
        plman.RemovePlaylistSelection(cur_playlist_idx);
    }
}

function drop(copy_selection) {
    let cur_playlist_idx = plman.ActivePlaylist;
    
    if (!is_dragging) {
        return;
    }

    is_dragging = false;

    let drop_idx = plman.PlaylistItemCount(cur_playlist_idx);
    if (!copy_selection) {
        plman.MovePlaylistSelection(cur_playlist_idx, drop_idx);
        plman.SetPlaylistFocusItem(cur_playlist_idx, drop_idx + 1);
    }
    else {
        plman.UndoBackup(cur_playlist_idx);

        let cur_selection = plman.GetPlaylistSelectedItems(cur_playlist_idx);        
        plman.ClearPlaylistSelection(cur_playlist_idx);
        plman.InsertPlaylistItems(cur_playlist_idx, drop_idx, cur_selection, true);
        plman.SetPlaylistFocusItem(cur_playlist_idx, drop_idx);
    }
}

function external_drop(action) {
    let cur_playlist_idx = plman.ActivePlaylist;
    
    plman.ClearPlaylistSelection(cur_playlist_idx);

    let playlist_idx;
    if (!plman.PlaylistCount) {
        playlist_idx = plman.CreatePlaylist(0, 'Default');
        plman.ActivePlaylist = playlist_idx;
    }
    else {
        playlist_idx = cur_playlist_idx;
        plman.UndoBackup(cur_playlist_idx);
    }

    action.Playlist = playlist_idx;
    action.ToSelect = true;
    action.Base = plman.PlaylistItemCount(cur_playlist_idx);

    is_dragging = false;
}

function filter_effect_by_modifiers(effect) {
    let ctrl_pressed = utils.IsKeyPressed(VK_CONTROL);
    let shift_pressed = utils.IsKeyPressed(VK_SHIFT);
    let alt_pressed = utils.IsKeyPressed(VK_ALT);

    if (ctrl_pressed && shift_pressed && !alt_pressed
        || alt_pressed && !ctrl_pressed && !shift_pressed) {
        // Link only

        return (effect & g_drop_effect.link);
    }

    if (ctrl_pressed && !shift_pressed && !alt_pressed) {
        // Copy (also via link)        

        return (effect & g_drop_effect.copy) || (effect & g_drop_effect.link);
    }

    if (shift_pressed) {
        // Move only

        return (effect & g_drop_effect.move);
    }

    // Move > Copy > Link    

    return (effect & g_drop_effect.move) || (effect & g_drop_effect.copy) || (effect & g_drop_effect.link);
}

function get_text_from_effect(effect, is_internal) {
    switch (effect) {
        case g_drop_effect.move:
        {
            if (is_internal) {
                return 'Moving from inside';
            }
            return 'Moving from outside';
        }
        case g_drop_effect.copy:
        {
            if (is_internal) {
                return 'Copying from inside';
            }
            return 'Copying from outside';
        }
        case g_drop_effect.link:
        {
            return 'Linking from outside';
        }
        default:
            return '';
    }
}