mirror of https://github.com/MidnightCommander/mc
641 lines
16 KiB
C
641 lines
16 KiB
C
/* Drag and Drop functionality for the Midnight Commander
|
|
*
|
|
* Copyright (C) 1998 The Free Software Foundation
|
|
*
|
|
* Authors: Federico Mena <federico@nuclecu.unam.mx>
|
|
* Miguel de Icaza <miguel@nuclecu.unam.mx>
|
|
*/
|
|
|
|
#include <config.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include "global.h"
|
|
#include "x.h"
|
|
#include "file.h"
|
|
#include "fileopctx.h"
|
|
#include "main.h"
|
|
#include "panel.h"
|
|
#include "ext.h"
|
|
#include "gscreen.h"
|
|
#include "../vfs/vfs.h"
|
|
#include "gdesktop.h"
|
|
#include "gdnd.h"
|
|
#include "gmain.h"
|
|
|
|
|
|
/* Atoms for the DnD target types */
|
|
GdkAtom dnd_target_atoms[TARGET_NTARGETS];
|
|
|
|
|
|
/**
|
|
* gdnd_init:
|
|
*
|
|
* Initializes the dnd_target_atoms array by interning the DnD target atom names.
|
|
**/
|
|
void
|
|
gdnd_init (void)
|
|
{
|
|
dnd_target_atoms[TARGET_MC_DESKTOP_ICON] =
|
|
gdk_atom_intern (TARGET_MC_DESKTOP_ICON_TYPE, FALSE);
|
|
|
|
dnd_target_atoms[TARGET_URI_LIST] =
|
|
gdk_atom_intern (TARGET_URI_LIST_TYPE, FALSE);
|
|
|
|
dnd_target_atoms[TARGET_TEXT_PLAIN] =
|
|
gdk_atom_intern (TARGET_TEXT_PLAIN_TYPE, FALSE);
|
|
|
|
dnd_target_atoms[TARGET_URL] =
|
|
gdk_atom_intern (TARGET_URL_TYPE, FALSE);
|
|
}
|
|
|
|
/* The menu of DnD actions */
|
|
static GnomeUIInfo actions[] = {
|
|
GNOMEUIINFO_ITEM_NONE (N_("_Move here"), NULL, NULL),
|
|
GNOMEUIINFO_ITEM_NONE (N_("_Copy here"), NULL, NULL),
|
|
GNOMEUIINFO_ITEM_NONE (N_("_Link here"), NULL, NULL),
|
|
GNOMEUIINFO_SEPARATOR,
|
|
GNOMEUIINFO_ITEM_NONE (N_("Cancel drag"), NULL, NULL),
|
|
GNOMEUIINFO_END
|
|
};
|
|
|
|
/* Pops up a menu of actions to perform on dropped files */
|
|
static GdkDragAction
|
|
get_action (GdkDragContext *context)
|
|
{
|
|
GtkWidget *menu;
|
|
int a;
|
|
GdkDragAction action;
|
|
|
|
/* Create the menu and set the sensitivity of the items based on the
|
|
* allowed actions.
|
|
*/
|
|
|
|
menu = gnome_popup_menu_new (actions);
|
|
|
|
gtk_widget_set_sensitive (actions[0].widget, (context->actions & GDK_ACTION_MOVE) != 0);
|
|
gtk_widget_set_sensitive (actions[1].widget, (context->actions & GDK_ACTION_COPY) != 0);
|
|
gtk_widget_set_sensitive (actions[2].widget, (context->actions & GDK_ACTION_LINK) != 0);
|
|
|
|
a = gnome_popup_menu_do_popup_modal (menu, NULL, NULL, NULL, NULL);
|
|
|
|
switch (a) {
|
|
case 0:
|
|
action = GDK_ACTION_MOVE;
|
|
break;
|
|
|
|
case 1:
|
|
action = GDK_ACTION_COPY;
|
|
break;
|
|
|
|
case 2:
|
|
action = GDK_ACTION_LINK;
|
|
break;
|
|
|
|
default:
|
|
action = GDK_ACTION_ASK; /* Magic value to indicate cancellation */
|
|
}
|
|
|
|
gtk_widget_destroy (menu);
|
|
|
|
return action;
|
|
}
|
|
|
|
/* Performs a drop action on the specified panel. Only supports copy and move
|
|
* operations. The files are moved or copied to the specified destination
|
|
* directory.
|
|
*/
|
|
static void
|
|
perform_action_on_panel (WPanel *source_panel, GdkDragAction action, char *destdir)
|
|
{
|
|
switch (action) {
|
|
case GDK_ACTION_COPY:
|
|
panel_operate (source_panel, OP_COPY, destdir, FALSE);
|
|
break;
|
|
|
|
case GDK_ACTION_MOVE:
|
|
panel_operate (source_panel, OP_MOVE, destdir, FALSE);
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
/* Finish up */
|
|
|
|
update_one_panel_widget (source_panel, FALSE, UP_KEEPSEL);
|
|
|
|
if (action == GDK_ACTION_MOVE)
|
|
panel_update_contents (source_panel);
|
|
}
|
|
|
|
/*
|
|
* Performs handling of symlinks via drag and drop. This should go
|
|
* away when operation windows support links.
|
|
*/
|
|
static void
|
|
perform_links (GList *names, char *destdir)
|
|
{
|
|
char *name;
|
|
char *dest_name;
|
|
|
|
for (; names; names = names->next) {
|
|
name = names->data;
|
|
if (strncmp (name, "file:", 5) == 0)
|
|
name += 5;
|
|
|
|
dest_name = g_concat_dir_and_file (destdir, x_basename (name));
|
|
mc_symlink (name, dest_name);
|
|
g_free (dest_name);
|
|
}
|
|
}
|
|
|
|
/* Performs a drop action manually, by going through the list of files to
|
|
* operate on. The files are copied or moved to the specified directory. This
|
|
* should also encompass symlinking when the file operations window supports
|
|
* links.
|
|
*/
|
|
static void
|
|
perform_action (GList *names, GdkDragAction action, char *destdir)
|
|
{
|
|
struct stat s;
|
|
char *name;
|
|
char *dest_name;
|
|
int result;
|
|
FileOpContext *ctx;
|
|
|
|
ctx = file_op_context_new ();
|
|
|
|
switch (action) {
|
|
case GDK_ACTION_COPY:
|
|
file_op_context_create_ui (ctx, OP_COPY, FALSE);
|
|
break;
|
|
|
|
case GDK_ACTION_MOVE:
|
|
file_op_context_create_ui (ctx, OP_MOVE, FALSE);
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
for (; names; names = names->next) {
|
|
name = names->data;
|
|
if (strncmp (name, "file:", 5) == 0)
|
|
name += 5;
|
|
|
|
dest_name = g_concat_dir_and_file (destdir, x_basename (name));
|
|
|
|
do {
|
|
result = mc_lstat (name, &s);
|
|
|
|
if (result != 0) {
|
|
/* FIXME: this error message sucks */
|
|
if (file_error (_("Could not stat %s\n%s"), dest_name) != FILE_RETRY)
|
|
result = 0;
|
|
} else {
|
|
long count = 0;
|
|
double bytes = 0;
|
|
|
|
if (S_ISDIR (s.st_mode)) {
|
|
if (action == GDK_ACTION_COPY)
|
|
copy_dir_dir (ctx,
|
|
name, dest_name,
|
|
TRUE, FALSE,
|
|
FALSE, FALSE,
|
|
&count, &bytes);
|
|
else
|
|
move_dir_dir (ctx,
|
|
name, dest_name,
|
|
&count, &bytes);
|
|
} else {
|
|
if (action == GDK_ACTION_COPY)
|
|
copy_file_file (ctx,
|
|
name, dest_name,
|
|
TRUE,
|
|
&count, &bytes, 1);
|
|
else
|
|
move_file_file (ctx,
|
|
name, dest_name,
|
|
&count, &bytes);
|
|
}
|
|
}
|
|
} while (result != 0);
|
|
|
|
g_free (dest_name);
|
|
}
|
|
|
|
file_op_context_destroy (ctx);
|
|
}
|
|
|
|
/* Drop a URI list on a directory. If the data comes from a panel, use the nice
|
|
* MC progress display; otherwise `do it by hand'.
|
|
*/
|
|
static void
|
|
drop_uri_list_on_directory (GdkDragContext *context, GtkSelectionData *selection_data,
|
|
GdkDragAction action, char *destdir)
|
|
{
|
|
WPanel *source_panel;
|
|
GtkWidget *source_widget;
|
|
GList *names;
|
|
|
|
source_panel = gdnd_find_panel_by_drag_context (context, &source_widget);
|
|
|
|
/* We cannot use file.c if we are going to symlink or if we are dragging
|
|
* from a tree.
|
|
*/
|
|
if (source_panel && source_widget != source_panel->tree && action != GDK_ACTION_LINK)
|
|
perform_action_on_panel (source_panel, action, destdir);
|
|
else {
|
|
names = gnome_uri_list_extract_uris (selection_data->data);
|
|
|
|
if (action == GDK_ACTION_LINK)
|
|
perform_links (names, destdir);
|
|
else
|
|
perform_action (names, action, destdir);
|
|
|
|
gnome_uri_list_free_strings (names);
|
|
}
|
|
}
|
|
|
|
/* Drop a Netscape URL in a directory */
|
|
static void
|
|
drop_url_on_directory (GdkDragContext *context, GtkSelectionData *selection_data, char *destdir)
|
|
{
|
|
char *template;
|
|
char *icon;
|
|
char *url;
|
|
char *title;
|
|
|
|
template = g_concat_dir_and_file (destdir, "url");
|
|
icon = g_concat_dir_and_file (ICONDIR, "gnome-http-url.png");
|
|
|
|
/* Mozilla uses newline to separate URL from title. */
|
|
url = g_strdup(selection_data->data);
|
|
title = strchr (url, '\n');
|
|
if (title) {
|
|
title[0] = 0;
|
|
title++;
|
|
} else {
|
|
title = url;
|
|
}
|
|
|
|
desktop_create_url (template, title, url, icon, TRUE);
|
|
|
|
g_free (url);
|
|
g_free (icon);
|
|
g_free (template);
|
|
}
|
|
|
|
/* Drop stuff on a directory */
|
|
static int
|
|
drop_on_directory (GdkDragContext *context, GtkSelectionData *selection_data,
|
|
GdkDragAction action, char *directory)
|
|
{
|
|
int retval;
|
|
|
|
retval = FALSE;
|
|
|
|
if (gdnd_drag_context_has_target (context, TARGET_URI_LIST)) {
|
|
drop_uri_list_on_directory (context, selection_data, action, directory);
|
|
retval = TRUE;
|
|
} else if (gdnd_drag_context_has_target (context, TARGET_URL)) {
|
|
drop_url_on_directory (context, selection_data, directory);
|
|
retval = TRUE;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
/* Returns whether a file has the drop-action metadata or MIME property defined
|
|
* for it.
|
|
*/
|
|
static int
|
|
file_has_drop_action (char *filename)
|
|
{
|
|
/* This function should be called with a full name,
|
|
so no full_name is necessary */
|
|
|
|
char *buf;
|
|
int size;
|
|
const char *mime_type;
|
|
|
|
if (gnome_metadata_get (filename, "drop-action", &size, &buf) == 0) {
|
|
g_free (buf);
|
|
return TRUE;
|
|
} else {
|
|
if (use_magic)
|
|
mime_type = gnome_mime_type_or_default_of_file (filename, NULL);
|
|
else
|
|
mime_type = gnome_mime_type_or_default (filename, NULL);
|
|
|
|
if (!mime_type)
|
|
return FALSE;
|
|
|
|
if (gnome_mime_get_value (mime_type, "drop-action") != NULL)
|
|
return TRUE;
|
|
else
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* Drop stuff on a non-directory file. This uses metadata and MIME as well. */
|
|
static int
|
|
drop_on_file (GdkDragContext *context, GtkSelectionData *selection_data,
|
|
char *dest_full_name, file_entry *dest_fe)
|
|
{
|
|
int size;
|
|
char *buf;
|
|
const char *mime_type;
|
|
int retval;
|
|
GList *names, *l;
|
|
int len, i;
|
|
char **drops;
|
|
|
|
retval = FALSE; /* assume we cannot drop */
|
|
|
|
/* Convert the data list into an array of strings */
|
|
|
|
names = gnome_uri_list_extract_uris (selection_data->data);
|
|
len = g_list_length (names);
|
|
drops = g_new (char *, len + 1);
|
|
|
|
for (l = names, i = 0; i < len; i++, l = l->next) {
|
|
char *text = l->data;
|
|
|
|
if (strncmp (text, "file:", 5) == 0)
|
|
text += 5;
|
|
|
|
drops[i] = text;
|
|
}
|
|
drops[i] = NULL;
|
|
|
|
/* 1. Try to use a metadata-based drop action */
|
|
|
|
if (gnome_metadata_get (dest_full_name, "drop-action", &size, &buf) == 0) {
|
|
exec_extension (dest_full_name, buf, drops, NULL, 0, 0);
|
|
g_free (buf);
|
|
retval = TRUE;
|
|
goto out;
|
|
}
|
|
|
|
/* 2. Try a drop action from the MIME-type */
|
|
|
|
if (use_magic)
|
|
mime_type = gnome_mime_type_or_default_of_file (dest_full_name, NULL);
|
|
else
|
|
mime_type = gnome_mime_type_or_default (dest_full_name, NULL);
|
|
|
|
if (mime_type) {
|
|
const char *action;
|
|
|
|
action = gnome_mime_get_value (mime_type, "drop-action");
|
|
if (action) {
|
|
exec_extension (dest_full_name, action, drops, NULL, 0, 0);
|
|
retval = TRUE;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/* 3. If executable, try metadata keys for "open" */
|
|
|
|
if (is_exe (dest_fe->buf.st_mode) && if_link_is_exe (dest_full_name, dest_fe)) {
|
|
/* FIXME: handle the case for Netscape URLs */
|
|
|
|
if (gnome_metadata_get (dest_full_name, "open", &size, &buf) == 0)
|
|
exec_extension (dest_full_name, buf, drops, NULL, 0, 0);
|
|
else
|
|
exec_extension (dest_full_name, "%f %q", drops, NULL, 0, 0);
|
|
|
|
g_free (buf);
|
|
|
|
retval = TRUE;
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
g_free (drops);
|
|
gnome_uri_list_free_strings (names);
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* gdnd_perform_drop:
|
|
* @context: Drag context for operation.
|
|
* @selection_data: Selection data from drag_data_received.
|
|
* @dest_full_name: Complete name of the destination file or directory.
|
|
* @dest_fe: File entry for the destination file or directory.
|
|
*
|
|
* Performs a drop operation on a directory or file.
|
|
*
|
|
* Return value: TRUE if the drop is successful, FALSE otherwise.
|
|
**/
|
|
int
|
|
gdnd_perform_drop (GdkDragContext *context, GtkSelectionData *selection_data,
|
|
char *dest_full_name, file_entry *dest_fe)
|
|
{
|
|
GdkDragAction action;
|
|
|
|
g_return_val_if_fail (context != NULL, FALSE);
|
|
g_return_val_if_fail (selection_data != NULL, FALSE);
|
|
g_return_val_if_fail (dest_full_name != NULL, FALSE);
|
|
g_return_val_if_fail (dest_fe != NULL, FALSE);
|
|
|
|
/* Get action */
|
|
|
|
if (context->action == GDK_ACTION_ASK) {
|
|
action = get_action (context);
|
|
if (action == GDK_ACTION_ASK)
|
|
return FALSE;
|
|
} else
|
|
action = context->action;
|
|
|
|
if (S_ISDIR (dest_fe->buf.st_mode) || dest_fe->f.link_to_dir)
|
|
return drop_on_directory (context, selection_data, action, dest_full_name);
|
|
else
|
|
return drop_on_file (context, selection_data, dest_full_name, dest_fe);
|
|
}
|
|
|
|
/**
|
|
* gdnd_drag_context_has_target:
|
|
* @context: The context to query for a target type
|
|
* @type: The sought target type
|
|
*
|
|
* Tests whether the specified drag context has a target of the specified type.
|
|
*
|
|
* Return value: TRUE if the context has the specified target type, FALSE
|
|
* otherwise.
|
|
**/
|
|
int
|
|
gdnd_drag_context_has_target (GdkDragContext *context, TargetType type)
|
|
{
|
|
GList *l;
|
|
|
|
g_return_val_if_fail (context != NULL, FALSE);
|
|
|
|
for (l = context->targets; l; l = l->next)
|
|
if (dnd_target_atoms[type] == GPOINTER_TO_INT (l->data))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* gdnd_find_panel_by_drag_context:
|
|
* @context: The context by which to find a panel.
|
|
* @source_widget: The source widget is returned here.
|
|
*
|
|
* Looks in the list of panels for the one that corresponds to the specified
|
|
* drag context.
|
|
*
|
|
* Return value: The sought panel, or NULL if no panel corresponds to the
|
|
* context.
|
|
**/
|
|
WPanel *
|
|
gdnd_find_panel_by_drag_context (GdkDragContext *context, GtkWidget **source_widget)
|
|
{
|
|
GtkWidget *source;
|
|
GtkWidget *toplevel;
|
|
GList *l;
|
|
WPanel *panel;
|
|
|
|
g_return_val_if_fail (context != NULL, NULL);
|
|
|
|
source = gtk_drag_get_source_widget (context);
|
|
|
|
if (source_widget)
|
|
*source_widget = source;
|
|
|
|
if (!source)
|
|
return NULL; /* different process */
|
|
|
|
toplevel = gtk_widget_get_toplevel (source);
|
|
|
|
for (l = containers; l; l = l->next) {
|
|
panel = ((PanelContainer *) l->data)->panel;
|
|
|
|
if (panel->xwindow == toplevel)
|
|
return panel;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* gdnd_validate_action:
|
|
* @context: The drag context for this drag operation.
|
|
* @on_desktop: Whether we are dragging onto the desktop or a desktop icon.
|
|
* @same_process: Whether the drag comes from the same process or not.
|
|
* @same_source: If same_process, then whether the source and dest widgets are the same.
|
|
* @dest_full_name: Complete name of the destination file or directory.
|
|
* @dest_fe: File entry for the destination file, or NULL if directory.
|
|
* @dest_selected: If dest is non-NULL, whether it is selected or not.
|
|
*
|
|
* Computes the final drag action based on the suggested action of the specified
|
|
* context and conditions.
|
|
*
|
|
* Return value: The computed action, meant to be passed to gdk_drag_action().
|
|
**/
|
|
GdkDragAction
|
|
gdnd_validate_action (GdkDragContext *context,
|
|
int on_desktop, int same_process, int same_source,
|
|
char *dest_full_name, file_entry *dest_fe, int dest_selected)
|
|
{
|
|
int on_directory = 0;
|
|
int on_exe = 0;
|
|
|
|
g_return_val_if_fail (context != NULL, 0);
|
|
g_return_val_if_fail (dest_full_name != NULL, 0);
|
|
|
|
/* If we are dragging a desktop icon onto the desktop or onto a selected
|
|
* desktop icon, unconditionally specify MOVE.
|
|
*/
|
|
if (on_desktop
|
|
&& gdnd_drag_context_has_target (context, TARGET_MC_DESKTOP_ICON)
|
|
&& (!dest_fe || dest_selected))
|
|
return GDK_ACTION_MOVE;
|
|
|
|
/* See what kind of file the destination is, if any */
|
|
|
|
if (dest_fe) {
|
|
on_directory = S_ISDIR (dest_fe->buf.st_mode) || dest_fe->f.link_to_dir;
|
|
on_exe = is_exe (dest_fe->buf.st_mode) && if_link_is_exe (dest_full_name, dest_fe);
|
|
}
|
|
|
|
if (gdnd_drag_context_has_target (context, TARGET_URI_LIST)) {
|
|
if (dest_fe) {
|
|
if (same_source && dest_selected)
|
|
return 0;
|
|
|
|
if (on_directory) {
|
|
if ((same_source || same_process)
|
|
&& (context->actions & GDK_ACTION_MOVE)
|
|
&& context->suggested_action != GDK_ACTION_ASK)
|
|
return GDK_ACTION_MOVE;
|
|
else
|
|
return context->suggested_action;
|
|
} else if (on_exe) {
|
|
if (context->actions & GDK_ACTION_COPY)
|
|
return GDK_ACTION_COPY;
|
|
} else if (file_has_drop_action (dest_full_name)) {
|
|
if (context->actions & GDK_ACTION_COPY)
|
|
return GDK_ACTION_COPY;
|
|
} else if (same_source)
|
|
return 0;
|
|
else if (same_process
|
|
&& (context->actions & GDK_ACTION_MOVE)
|
|
&& context->suggested_action != GDK_ACTION_ASK)
|
|
return GDK_ACTION_MOVE;
|
|
else
|
|
return context->suggested_action;
|
|
} else {
|
|
if (same_source)
|
|
return 0;
|
|
else if (same_process
|
|
&& (context->actions & GDK_ACTION_MOVE)
|
|
&& context->suggested_action != GDK_ACTION_ASK)
|
|
return GDK_ACTION_MOVE;
|
|
else
|
|
return context->suggested_action;
|
|
}
|
|
}
|
|
|
|
if (gdnd_drag_context_has_target (context, TARGET_URL)) {
|
|
/* FIXME: right now we only allow linking to directories. We
|
|
* should see if we can move or copy stuff instead (for ftp
|
|
* instead of http sites, for example).
|
|
*/
|
|
if (dest_fe) {
|
|
if (on_directory) {
|
|
if (context->actions & GDK_ACTION_LINK)
|
|
return GDK_ACTION_LINK;
|
|
} else if (context->actions & GDK_ACTION_COPY)
|
|
return GDK_ACTION_COPY;
|
|
} else if (context->actions & GDK_ACTION_LINK)
|
|
return GDK_ACTION_LINK;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* gdnd_can_drop_on_file:
|
|
* @full_name: Complete name of the file.
|
|
* @fe: File entry for the file.
|
|
*
|
|
* Computes whether a non-directory file can take drops.
|
|
*
|
|
* Return value: TRUE if the file can take drops, FALSE otherwise.
|
|
**/
|
|
int
|
|
gdnd_can_drop_on_file (char *full_name, file_entry *fe)
|
|
{
|
|
g_return_val_if_fail (full_name != NULL, FALSE);
|
|
g_return_val_if_fail (fe != NULL, FALSE);
|
|
|
|
if ((is_exe (fe->buf.st_mode) && if_link_is_exe (full_name, fe))
|
|
|| file_has_drop_action (full_name))
|
|
return TRUE;
|
|
else
|
|
return FALSE;
|
|
}
|